Skip to main content

Spawn System

helios provides a comprehensive spawn system for managing entity lifecycle at runtime. The system separates concerns into scheduling (when/how many), behavior (where/how), and execution (pool integration). A fluent SpawnSystemFactory builder provides ID-centric configuration where each pool ID is the root for all related profiles and scheduling rules.

Overview

The spawn system consists of several layers working together:

  • SpawnScheduler: Evaluates rules and determines when spawning should occur
  • SpawnProfile: Defines how entities are spawned (pool, placer, initializer)
  • SpawnManager: Processes spawn/despawn commands via pools
  • Commands & Dispatchers: Route commands through the command pipeline
  • SpawnCommandHandler: Interface for managers that process spawn commands
  • SpawnSystemFactory: Fluent builder for ID-centric spawn configuration
┌─────────────────────────────────────────────────────────────────────┐
│ SPAWN PIPELINE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CONFIGURATION LAYER (SpawnSystemFactory) │
│ ┌─────────────────────────────────────────┐ │
│ │ pool(PoolId, prefab, size) │ │
│ │ └─ profile(ProfileId) │ │
│ │ ├─ placement + initializer │ │
│ │ └─ scheduledBy(RuleId) │ │
│ │ └─ condition + amount │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ POLICY LAYER (when/how many) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ SpawnRule │───>│SpawnCondition───>│AmountProvider│ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ SCHEDULING LAYER │
│ ┌─────────────────────────────────────────┐ │
│ │ SpawnScheduler │ │
│ │ ┌──────────────────┐ ┌───────────────┐ │ │
│ │ │DefaultScheduler │ │CyclicScheduler│ │ │
│ │ └──────────────────┘ └───────────────┘ │ │
│ └────────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ COMMAND LAYER │
│ ┌─────────────────────────────────────────┐ │
│ │ ScheduledSpawnPlanCommand │ │
│ └────────────────────┬────────────────────┘ │
│ │ │
│ ▼ │
│ EXECUTION LAYER │
│ ┌─────────────────────────────────────────┐ │
│ │ SpawnManager │ │
│ │ ┌───────────┐ ┌───────────┐ │ │
│ │ │SpawnPlacer│ │Initializer│ │ │
│ │ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Key Concepts

SpawnProfile

A SpawnProfile bundles together everything needed to spawn entities of a particular type:

import helios.engine.runtime.spawn.SpawnProfile;
import helios.engine.runtime.spawn.behavior.placements.RandomSpawnPlacer;
import helios.engine.runtime.spawn.behavior.initializers.RandomDirectionInitializer;

auto enemyProfile = std::make_unique<SpawnProfile>(SpawnProfile{
.gameObjectPoolId = enemyPoolId,
.spawnPlacer = std::make_unique<RandomSpawnPlacer>(),
.spawnInitializer = std::make_unique<RandomDirectionInitializer>()
});
FieldPurpose
gameObjectPoolIdWhich pool to acquire entities from
spawnPlacerStrategy for positioning spawned entities
spawnInitializerStrategy for initializing entity components

SpawnPlacer

Determines where entities spawn. Built-in implementations:

PlacerBehavior
RandomSpawnPlacerRandom position within level bounds
EmitterSpawnPlacerAt the emitter's current position
AxisSpawnPlacerDistributed along an axis from an origin
ColumnSpawnPlacerVertical column formation
DistributedSpawnPlacer<N>Evenly distributed across N anchor points
class RandomSpawnPlacer : public SpawnPlacer {
public:
vec3f getPosition(const SpawnContext& ctx) const override {
const auto& bounds = ctx.level()->bounds();
return vec3f{
Random::range(bounds.min()[0], bounds.max()[0]),
Random::range(bounds.min()[1], bounds.max()[1]),
0.0f
};
}
};

SpawnInitializer

Configures spawned entities with initial state. Built-in implementations:

InitializerBehavior
EmitterInitializerCopies direction/velocity from emitter
MoveInitializerSets movement direction (fixed, random, or point)
RandomDirectionInitializerAssigns random 2D direction
DelayedComponentEnablerInitializer<T>Enables a component after a delay
InitializerList<N>Chains multiple initializers sequentially
class EmitterInitializer : public SpawnInitializer {
public:
void initialize(GameObject& entity, const SpawnContext& ctx) const override {
if (const auto& emitter = ctx.emitterContext()) {
if (auto* dir = entity.get<DirectionComponent>()) {
dir->setDirection(emitter->direction);
}
}
}
};

Spawn Rules and Scheduling

SpawnRule

A SpawnRule combines a condition (when to spawn) with an amount provider (how many):

import helios.engine.runtime.spawn.policy.SpawnRule;
import helios.engine.runtime.spawn.policy.conditions.TimerSpawnCondition;
import helios.engine.runtime.spawn.policy.amount.FixedSpawnAmount;

auto rule = std::make_unique<SpawnRule>(
std::make_unique<TimerSpawnCondition>(2.0f), // Every 2 seconds
std::make_unique<FixedSpawnAmount>(3), // Spawn 3 entities
SpawnRuleId{1}
);

SpawnCondition

Determines when spawning should occur:

ConditionBehavior
TimerSpawnConditionTriggers at fixed time intervals
RequestedAmountIsAvailableConditionTriggers only if the pool has enough inactive entities
SpawnConditionAllComposite: all child conditions must be satisfied
class TimerSpawnCondition : public SpawnCondition {
public:
explicit TimerSpawnCondition(float interval) : interval_(interval) {}

bool isSatisfied(size_t amount, const SpawnRuleState& state,
GameObjectPoolSnapshot pool, const UpdateContext& ctx) const override {
return state.sinceLastSpawn() >= interval_
&& pool.inactiveCount >= amount;
}

void onCommit(SpawnRuleState& state, size_t count) const override {
state.setSinceLastSpawn(0.0f);
}
};

SpawnAmountProvider

Determines how many entities to spawn:

ProviderBehavior
FixedSpawnAmountAlways returns a fixed count
SpawnAllReturns the total available count from the pool
SpawnAmountByCallbackDelegates to a user-provided function
// Dynamic amount based on game state
auto dynamicAmount = std::make_unique<SpawnAmountByCallback>(
[](const GameObjectPoolId& poolId, const SpawnRuleState& state,
const UpdateContext& ctx) -> size_t {
return ctx.gameWorld().difficultyLevel() + 1;
}
);

SpawnScheduler

The SpawnScheduler evaluates all registered rules each frame. helios ships with two scheduler implementations:

SchedulerBehavior
DefaultSpawnSchedulerEvaluates all rules every frame; independent triggers
CyclicSpawnScheduler<N>Round-robin through N rules; advances only on successful spawn

DefaultSpawnScheduler

Evaluates all rules independently each frame. Each rule can trigger on its own schedule:

import helios.engine.runtime.spawn.scheduling.DefaultSpawnScheduler;

auto scheduler = std::make_unique<DefaultSpawnScheduler>();
scheduler->addRule(enemyProfileId, std::move(rule));

CyclicSpawnScheduler

Evaluates one rule at a time in a fixed-size ring buffer. Advances to the next rule only when a spawn successfully occurs. Ideal for wave-based or sequential spawn patterns:

import helios.engine.runtime.spawn.scheduling.CyclicSpawnScheduler;

// Cycle: Left → Top → Right → Bottom → Left → ...
auto scheduler = std::make_unique<CyclicSpawnScheduler<4>>();
scheduler->addRule(leftProfileId, std::move(leftRule));
scheduler->addRule(topProfileId, std::move(topRule));
scheduler->addRule(rightProfileId, std::move(rightRule));
scheduler->addRule(bottomProfileId, std::move(bottomRule));

In the game loop, the scheduler produces plans that become commands:

import helios.engine.runtime.spawn.scheduling.SpawnScheduler;

SpawnScheduler scheduler;

scheduler.addRule(enemyProfileId, std::move(rule));

// In GameObjectSpawnSystem::update()
scheduler.evaluate(updateContext);
auto plans = scheduler.drainScheduledPlans();

// Convert plans to commands
for (const auto& plan : plans) {
commandBuffer.add<ScheduledSpawnPlanCommand>(std::move(plan));
}

Command Pipeline Integration

Commands

Spawn operations are represented as commands for deferred execution:

CommandPurpose
SpawnCommandRequest to spawn entities from a profile
DespawnCommandRequest to return an entity to its pool
ScheduledSpawnPlanCommandExecute a scheduled spawn plan

Dispatchers

Commands are routed to managers via typed dispatchers:

import helios.engine.runtime.spawn.dispatcher.SpawnCommandDispatcher;
import helios.engine.runtime.spawn.dispatcher.DespawnCommandDispatcher;
import helios.engine.runtime.spawn.dispatcher.ScheduledSpawnPlanCommandDispatcher;

commandBuffer.addDispatcher<SpawnCommandDispatcher>();
commandBuffer.addDispatcher<DespawnCommandDispatcher>();
commandBuffer.addDispatcher<ScheduledSpawnPlanCommandDispatcher>();

SpawnManager

The SpawnManager processes spawn/despawn commands:

import helios.engine.runtime.spawn.SpawnManager;

auto spawnManager = std::make_unique<SpawnManager>();

// Register profiles
spawnManager->addSpawnProfile(enemyProfileId, std::move(enemyProfile));
spawnManager->addSpawnProfile(bulletProfileId, std::move(bulletProfile));

// Add to GameWorld
gameWorld.addManager(std::move(spawnManager));

SpawnSystemFactory (Builder)

The SpawnSystemFactory provides a fluent, ID-centric builder for configuring the entire spawn system. Each pool ID serves as the entry point for its associated profiles and scheduling rules. This co-locates related configuration and eliminates the risk of mismatched IDs.

Builder Hierarchy

SpawnSystemFactory::configure(poolManager, spawnManager)  →  SpawnSystemConfigurator

└─ pool(PoolId, prefab, size) → SpawnPoolConfig

├─ profile(ProfileId) → SpawnProfileConfig
│ │
│ ├─ emitterPlacement() / randomPlacement() / axisPlacement(...)
│ ├─ moveInitializer(...) / randomDirectionInitializer()
│ ├─ placer(custom) / initializer(custom)
│ │
│ ├─ scheduledBy(RuleId) → SpawnRuleConfig
│ │ ├─ timerCondition(interval)
│ │ ├─ timerWithAvailabilityCondition(interval)
│ │ ├─ fixedAmount(count)
│ │ └─ done() → back to SpawnProfileConfig
│ │
│ └─ done() → back to SpawnPoolConfig

├─ profile(...) → additional profiles on the same pool

├─ commit() → SpawnSystemConfigurator (per-profile schedulers)
├─ commitCyclic<N>() → SpawnSystemConfigurator (one CyclicSpawnScheduler)
└─ commitProfilesOnly() → SpawnSystemConfigurator (no schedulers)

commit(), commitCyclic<N>(), and commitProfilesOnly() all return a SpawnSystemConfigurator - a lightweight handle holding the manager references. This enables chained pool configuration without intermediate variables.

Design: Callbacks vs. done()-Chaining

The SpawnSystemFactory uses done()-chaining rather than the callback pattern used by GameObjectFactory. This is because spawn configuration has a natural hierarchy (pool → profile → rule), where done()-chaining expresses parent-child relationships clearly.

The GameObjectFactory uses callbacks because its domains (motion, collision, rendering) are flat and independent - callbacks scope each domain without implying hierarchy. See GameObject Builder for details.

PatternBest ForUsed By
done()-chainingHierarchical config (parent → child → grandchild)SpawnSystemFactory
Callback-basedFlat, independent domains with external capturesGameObjectFactory

commit() vs. commitCyclic<N>() vs. commitProfilesOnly()

MethodScheduler CreatedReturnsUse Case
commit()One DefaultSpawnScheduler per profile with rulesSpawnSystemConfiguratorIndependent spawn timers
commitCyclic<N>()One CyclicSpawnScheduler<N> for all profilesSpawnSystemConfiguratorSequential wave patterns
commitProfilesOnly()NoneSpawnSystemConfiguratorProfiles without scheduling (attach rules separately)

Example: Chained Multi-Pool Configuration

The complete spawn system can be configured in a single chained expression. Each commit() / commitCyclic<N>() returns a SpawnSystemConfigurator that starts the next pool():

using namespace helios::engine::builder::spawnSystem;

SpawnSystemFactory::configure(poolManager, spawnManager)

// 1. Projectile pool - emitter-relative, no scheduling
.pool(ProjectilePoolId, projectilePrefab, 50)
.profile(ProjectileProfileId)
.emitterPlacement()
.done()
.commit()

// 2. Enemy pool - random placement, timer-scheduled
.pool(EnemyPoolId, enemyPrefab, 200)
.profile(EnemyProfileId)
.randomPlacement()
.randomDirectionInitializer()
.scheduledBy(EnemyRuleId)
.timerCondition(5.0f)
.fixedAmount(1)
.done()
.done()
.commit()

// 3. Boss pool - mass spawn with custom placer/initializer
.pool(BossPoolId, bossPrefab, 10)
.profile(BossProfileId)
.placer(std::make_unique<DistributedSpawnPlacer<2>>(left, right))
.initializer(std::make_unique<InitializerList<2>>(
std::make_unique<DelayedComponentEnablerInitializer<Move2DComponent>>(0.5f, 25),
std::make_unique<MoveInitializer>(vec3f{0}, DirectionType::Point)
))
.scheduledBy(BossRuleId)
.timerWithAvailabilityCondition(30.0f)
.fixedAmount(2)
.done()
.done()
.commit()

// 4. Edge enemy pool - cyclic 4-direction spawning
.pool(EdgeEnemyPoolId, edgePrefab, 100)
.profile(LeftProfileId)
.axisPlacement(vec3f(0, -1, 0).normalize(), topLeft)
.moveInitializer(X_AXISf)
.scheduledBy(LeftRuleId)
.timerWithAvailabilityCondition(15.0f)
.fixedAmount(19)
.done()
.done()
.profile(TopProfileId)
.axisPlacement(vec3f(1, 0, 0).normalize(), topLeft)
.moveInitializer(Y_AXISf * -1.0f)
.scheduledBy(TopRuleId)
.timerWithAvailabilityCondition(15.0f)
.fixedAmount(29)
.done()
.done()
.profile(RightProfileId)
.axisPlacement(vec3f(0, -1, 0).normalize(), topRight)
.moveInitializer(X_AXISf * -1.0f)
.scheduledBy(RightRuleId)
.timerWithAvailabilityCondition(15.0f)
.fixedAmount(19)
.done()
.done()
.profile(BottomProfileId)
.axisPlacement(vec3f(1, 0, 0).normalize(), bottomLeft)
.moveInitializer(Y_AXISf)
.scheduledBy(BottomRuleId)
.timerWithAvailabilityCondition(15.0f)
.fixedAmount(29)
.done()
.done()
.commitCyclic<4>();

Example: Separate Profile and Scheduling Configuration

When the same profiles need different scheduling strategies (e.g., per-level difficulty), use commitProfilesOnly() to register pool and profiles without schedulers, then attach rules separately via SchedulerBuilder:

using namespace helios::engine::builder::spawnSystem;
using namespace helios::engine::builder::spawnSystem::builders::configs;

// 1. Register pool + profiles (no schedulers created)
SpawnSystemFactory::configure(poolManager, spawnManager)
.pool(EdgeEnemyPoolId, edgePrefab, 100)
.profile(LeftProfileId)
.axisPlacement(vec3f(0, -1, 0).normalize(), topLeft)
.moveInitializer(X_AXISf)
.done()
.profile(RightProfileId)
.axisPlacement(vec3f(0, -1, 0).normalize(), topRight)
.moveInitializer(X_AXISf * -1.0f)
.done()
.commitProfilesOnly();

// 2. Attach scheduling independently - can vary by level/difficulty
builders::SchedulerBuilder sb(spawnManager);
sb.cyclicScheduler(
SchedulerConfig(LeftProfileId, LeftRuleId)
.timerWithAvailabilityCondition(15.0f)
.fixedAmount(19),
SchedulerConfig(RightProfileId, RightRuleId)
.timerWithAvailabilityCondition(15.0f)
.fixedAmount(19)
);

This pattern is useful when:

  • The same entity types are shared across multiple levels
  • Scheduling parameters change per difficulty or game mode
  • Profile configuration (placement, initialization) remains constant

Complete Setup (Manual)

For cases where the builder is not suitable, the spawn system can be configured manually:

// 1. Register managers
auto& poolManager = gameWorld.addManager<GameObjectPoolManager>();
auto& spawnManager = gameWorld.addManager<SpawnManager>();

// 2. Configure pool
poolManager.addPoolConfig(std::make_unique<GameObjectPoolConfig>(
enemyPoolId, enemyPrefab, 50
));

// 3. Create and register profile
auto profile = std::make_unique<SpawnProfile>(SpawnProfile{
.gameObjectPoolId = enemyPoolId,
.spawnPlacer = std::make_unique<RandomSpawnPlacer>(),
.spawnInitializer = std::make_unique<MoveInitializer>(DirectionType::Random)
});
spawnManager.addSpawnProfile(enemyProfileId, std::move(profile));

// 4. Create scheduler with rules
auto scheduler = std::make_unique<DefaultSpawnScheduler>();
scheduler->addRule(enemyProfileId, std::make_unique<SpawnRule>(
std::make_unique<TimerSpawnCondition>(3.0f),
std::make_unique<FixedSpawnAmount>(2),
enemyRuleId
));
spawnManager.addScheduler(std::move(scheduler));

// 5. Register dispatchers (always required)
commandBuffer.addDispatcher<ScheduledSpawnPlanCommandDispatcher>();
commandBuffer.addDispatcher<SpawnCommandDispatcher>();
commandBuffer.addDispatcher<DespawnCommandDispatcher>();

// 6. Add spawn system to game loop
gameLoop.phase(PhaseType::Pre)
.addPass<GameState>(GameState::Running)
.addSystem<GameObjectSpawnSystem>(spawnManager);

Projectile Spawning

For projectiles (bullets, missiles), use the EmitterContext to spawn relative to the firing entity:

// In ShootComponent or similar
void fire(const UpdateContext& ctx, const vec3f& position, const vec3f& direction) {
SpawnContext spawnCtx;
spawnCtx.emitterContext = EmitterContext{
.position = position,
.direction = direction,
.velocity = direction * bulletSpeed_
};

ctx.commandBuffer().add<SpawnCommand>(bulletProfileId, spawnCtx, 1);
}

The EmitterSpawnPlacer and EmitterInitializer then use this context:

auto bulletProfile = std::make_unique<SpawnProfile>(SpawnProfile{
.gameObjectPoolId = bulletPoolId,
.spawnPlacer = std::make_unique<EmitterSpawnPlacer>(),
.spawnInitializer = std::make_unique<EmitterInitializer>()
});

Despawning

To return entities to their pool, use DespawnCommand:

// When entity should be removed (e.g., out of bounds, destroyed)
ctx.commandBuffer().add<DespawnCommand>(entity->guid(), profileId);

The LevelBoundsBehaviorComponent with BoundsBehavior::Despawn handles this automatically for entities leaving the level bounds.

Module Structure

helios.engine.runtime.spawn/
├── SpawnManager.ixx # Manager processing commands
├── SpawnProfile.ixx # Profile configuration
├── SpawnContext.ixx # Context for spawn operations
├── EmitterContext.ixx # Emitter state for projectiles
├── SpawnCommandHandler.ixx # Interface for command handlers
├── SpawnCommandHandlerRegistry.ixx # Registry mapping profiles to handlers
├── behavior/
│ ├── SpawnPlacer.ixx # Placement interface
│ ├── SpawnInitializer.ixx # Initialization interface
│ ├── placements/ # Concrete placers
│ │ ├── RandomSpawnPlacer.ixx
│ │ ├── EmitterSpawnPlacer.ixx
│ │ ├── AxisSpawnPlacer.ixx
│ │ ├── ColumnSpawnPlacer.ixx
│ │ └── DistributedSpawnPlacer.ixx
│ └── initializers/ # Concrete initializers
│ ├── EmitterInitializer.ixx
│ ├── MoveInitializer.ixx
│ ├── RandomDirectionInitializer.ixx
│ ├── DelayedComponentEnablerInitializer.ixx
│ └── InitializerList.ixx
├── policy/
│ ├── SpawnCondition.ixx # Condition interface
│ ├── SpawnConditionAll.ixx # Composite condition (AND)
│ ├── SpawnRule.ixx # Rule combining condition + amount
│ ├── amount/ # Amount providers
│ │ ├── FixedSpawnAmount.ixx
│ │ ├── SpawnAll.ixx
│ │ └── SpawnAmountByCallback.ixx
│ └── conditions/ # Concrete conditions
│ ├── TimerSpawnCondition.ixx
│ └── RequestedAmountIsAvailableCondition.ixx
├── scheduling/
│ ├── SpawnScheduler.ixx # Rule evaluation interface
│ ├── DefaultSpawnScheduler.ixx # Independent rule evaluation
│ ├── CyclicSpawnScheduler.ixx # Round-robin rule evaluation
│ ├── SpawnPlan.ixx # Planned spawn data
│ └── ScheduledSpawnPlan.ixx # Plan with profile ID
├── commands/ # Spawn/Despawn commands
├── dispatcher/ # Command dispatchers
└── events/ # Frame events

helios.engine.builder.spawnSystem/
├── SpawnSystemFactory.ixx # Fluent entry point
├── builders/
│ ├── SchedulerBuilder.ixx # Standalone scheduler registration
│ └── configs/
│ ├── SpawnPoolConfig.ixx # Pool → profile → rule hierarchy
│ └── SchedulerConfig.ixx # Standalone rule config (for SchedulerBuilder)

See Also