Spawning Waves of Enemies From Tuned Templates
The spawner code cannot call `new ConcreteMonster` when it only holds a base `Monster*` or `EnemyPrototype*` template configured from level data. Naively re-reading disk JSON configs and parsing stats on every spawn triggers severe frame hitches and prevents dynamic runtime mutation of enemy archetypes.
Parsing JSON, allocating lookup maps, and reading disk configurations dozens of times per second inside the game loop causes massive CPU spikes and frame drops.
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
We define an abstract EnemyPrototype interface with virtual std::unique_ptr<EnemyPrototype> clone() const = 0;. Concrete classes (GoblinScout, EliteOrc, IceMage) implement clone() via standard copy construction. MonsterSpawnerRegistry loads and tunes base prototypes once during game boot. When a wave triggers, the spawner simply clones the pre-tuned prototype in O(1) memory copy time, sets position coordinates, and dispatches the enemy immediately.
In Sprint 3 (C3 Β· Memento), we built an encapsulated checkpoint and rollback mechanism protecting hero state across dungeon boss wipes. With the player save loop robust, the dungeon system now needs to populate chambers and arenas with hordes of enemy encounters.
We need a monster spawning system (MonsterSpawnerRegistry) that can rapidly instantiate waves of pre-configured monster variants (GoblinScout, EliteOrc, IceMage) from tuned templates at runtime, allowing designers to spawn customized mini-bosses without hardcoding new classes.
The spawner code cannot call new ConcreteMonster when it only holds a base Monster* or EnemyPrototype* template configured from level data. Naively re-reading disk JSON configs and parsing stats on every spawn triggers severe frame hitches and prevents dynamic runtime mutation of enemy archetypes.
The Prototype Pattern (clone()) solves polymorphic copying in O(1) memory time, allowing the registry to stamp out hundreds of enemies from pre-tuned templates instantly. Next in Sprint 5 (C5 Β· Flyweight), where spawning 50,000 dungeon tiles, trees, and particle effects crashes RAM because each instance duplicates multi-megabyte mesh and texture data.
The product ticket
During dungeon horde battles, waves of dozens or hundreds of monsters spawn rapidly: GoblinScout (fast, low HP, dagger), EliteOrc (high HP, armor, battle axe), and IceMage (frost spells, elemental resistances). Each monster archetype has complex pre-tuned statistics, including weapon tables, AI behavior weights, and loot drop distributions.
- β’
Support spawning hundreds of monsters in rapid succession with O(1) overhead without re-parsing configuration files.
- β’
Implement a polymorphic cloning mechanism via
virtual std::unique_ptr<EnemyPrototype> clone() const = 0;. - β’
Provide a registry (
MonsterSpawnerRegistry) that holds pre-tuned archetype templates and clones them on demand. - β’
Allow post-clone customization: set coordinates
(x, y)and adjust stats without mutating the prototype template.
Designers want to create new mini-boss variants at runtime β such as cloning an EliteOrc, buffing its HP to 450, equipping a 'Doombringer Greatsword', and registering it as orc_warlord β without creating a new C++ class or restarting the engine.
Step 1 β The naive code & why it breaks
The naive spawner uses a factory function with a switch statement that re-reads disk JSON configs, parses strings, and reconstructs monster state from scratch on every single spawn.
#include <chrono>
#include <print>
#include <string>
#include <thread>
#include <vector>
// Naive: Every spawn parses config strings and rebuilds complex structures from scratch!
struct MonsterConfig {
std::string type;
int hp;
int attack;
std::string weapon;
std::vector<std::string> lootTable;
};
// Simulates expensive parsing/loading from disk or network
MonsterConfig loadConfigFromDisk(const std::string& type) {
// In production game engines, disk I/O and JSON parsing take substantial CPU cycles
if (type == "goblin") {
return {"Goblin Scout", 45, 12, "Rusty Dagger", {"10 Gold", "Cloth Scrap"}};
} else if (type == "orc") {
return {"Elite Orc", 180, 28, "Battle Axe", {"Heavy Belt", "Ruby"}};
}
return {"Unknown", 10, 5, "Fist", {}};
}
class NaiveMonster {
MonsterConfig cfg_;
int x_{0};
int y_{0};
public:
explicit NaiveMonster(MonsterConfig cfg) : cfg_(std::move(cfg)) {}
void setPosition(int x, int y) { x_ = x; y_ = y; }
void print() const {
std::println("Spawned {} at ({}, {}) [HP: {}, Atk: {}, Weapon: {}]",
cfg_.type, x_, y_, cfg_.hp, cfg_.attack, cfg_.weapon);
}
};
class NaiveSpawner {
public:
NaiveMonster spawn(const std::string& type, int x, int y) {
// Flaw: Disk/JSON config parsing repeated for every single spawn!
MonsterConfig cfg = loadConfigFromDisk(type);
NaiveMonster m(std::move(cfg));
m.setPosition(x, y);
return m;
}
};
int main() {
NaiveSpawner spawner;
std::println("--- Naive Horde Spawning (Re-parsing config on every spawn) ---");
for (int i = 0; i < 3; ++i) {
auto goblin = spawner.spawn("goblin", 10 * i, 20 * i);
goblin.print();
}
auto orc = spawner.spawn("orc", 100, 100);
orc.print();
}Parsing JSON, allocating lookup maps, and reading disk configurations dozens of times per second inside the game loop causes massive CPU spikes and frame drops.
Adding a new enemy type forces developers to add a new case branch and pass 6+ constructor arguments, violating the Open-Closed Principle (OCP).
If a level designer wants an 'Orc Warlord' with tweaked stats, they cannot simply clone and modify an existing template; they must create a new config file and add new factory code.
Step 2 β The fix: Prototype Pattern
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
We define an abstract EnemyPrototype interface with virtual std::unique_ptr<EnemyPrototype> clone() const = 0;. Concrete classes (GoblinScout, EliteOrc, IceMage) implement clone() via standard copy construction. MonsterSpawnerRegistry loads and tunes base prototypes once during game boot. When a wave triggers, the spawner simply clones the pre-tuned prototype in O(1) memory copy time, sets position coordinates, and dispatches the enemy immediately.
EnemyPrototype β declares clone(), spawnAt(x, y), takeDamage(dmg), and inspection methods.
GoblinScout, EliteOrc, IceMage β implement clone() via std::make_unique<Derived>(*this) and manage archetype stats.
MonsterSpawnerRegistry β maintains a dictionary of named prototypes (prototypes_) and spawns clones on demand.
Unlock Full UML Diagram & C++23 Solution
The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.