Skip to main content

Component System

helios implements a composition-based component architecture that separates data from behavior, enabling flexible and reusable game logic without deep inheritance hierarchies.

Overview

The component system follows the "composition over inheritance" principle:

  • Components store data for entities (plain data classes, no base class required)
  • Systems operate on groups of entities with specific component configurations
  • GameWorld manages entity lifecycle via EntityRegistry and EntityManager
  • GameLoop orchestrates Systems across Phases and Passes

This design allows you to build complex entities by mixing and matching components rather than creating specialized subclasses.

Key Classes

Components

Components are plain data classes - they store state but should not contain complex update logic. Unlike traditional ECS frameworks, helios components do not inherit from a base class.

class HealthComponent {
float health_ = 100.0f;
float maxHealth_ = 100.0f;
bool isEnabled_ = true;

public:
// Required: Copy and Move constructors
HealthComponent(const HealthComponent&) = default;
HealthComponent(HealthComponent&&) noexcept = default;

// Optional: Enable/Disable for view filtering
[[nodiscard]] bool isEnabled() const noexcept { return isEnabled_; }
void enable() noexcept { isEnabled_ = true; }
void disable() noexcept { isEnabled_ = false; }

// Business logic
void takeDamage(float amount) { health_ -= amount; }
[[nodiscard]] float health() const { return health_; }
};

Note: Components must have a noexcept move constructor for efficient storage in SparseSets. See Component Structure for full requirements.

Component Enabled State

Components can implement isEnabled() / enable() / disable() for view filtering:

[[nodiscard]] bool isEnabled() const noexcept;
void enable() noexcept;
void disable() noexcept;
StateMeaning
isEnabled() == trueComponent is active and included in filtered views
isEnabled() == falseComponent is skipped by .whereEnabled() but remains attached

This allows fine-grained control over individual components. For example, temporarily disabling a CollisionComponent makes an entity pass through walls.

Important: Disabling a component does not deactivate the entire entity. Use GameObject::setActive(false) to exclude an entity from all processing.

GameObject

A lightweight wrapper (~16 bytes) for entity manipulation. Pass by value, not by reference.

// Create entity via GameWorld
auto entity = gameWorld.addGameObject();

// Add components
entity.add<SceneNodeComponent>(sceneNode);
entity.add<Move2DComponent>(speed);
entity.add<HealthComponent>(100.0f);

// Retrieve components by type (O(1) lookup)
auto* health = entity.get<HealthComponent>();

// Check component presence
if (entity.has<CollisionComponent>()) {
// ...
}

Each entity is identified by a versioned EntityHandle for safe reference tracking.

Component Storage

Components are stored in type-specific SparseSet<T> containers managed by EntityManager. This provides O(1) access:

OperationComplexityDescription
get<T>()O(1)Direct SparseSet lookup via type ID
has<T>()O(1)SparseSet contains check
add<T>()O(1) amortizedSparseSet emplace

System

Global logic processors that operate on the entire GameWorld. Systems are registered with the GameLoop and executed within Phases and Passes.

import helios.engine.ecs.System;

class PhysicsSystem : public helios::engine::ecs::System {
public:
void update(UpdateContext& ctx) noexcept override {
// Iterate all active entities with required components
for (auto [entity, move, transform, active] : gameWorld_->view<
Move2DComponent,
TranslationStateComponent,
Active
>().whereEnabled()) {
// Apply physics simulation...
transform->translateBy(move->velocity() * ctx.deltaTime());
}
}
};

Systems are organized into Phases (Pre, Main, Post) and Passes within each Phase. See Game Loop Architecture for details.

GameWorld

The root container managing entities, Managers, and Pools. Located in helios::engine::runtime::world.

import helios.engine.runtime.world.GameWorld;

helios::engine::runtime::world::GameWorld world;

// Create entities
auto player = world.addGameObject();
player.add<Move2DComponent>(speed);

// Add managers for deferred processing
world.addManager<SpawnManager>();

// Query entities by component using views
for (auto [entity, move, collision] : world.view<
Move2DComponent,
CollisionComponent
>()) {
// Process matching entities
}

GameLoop

The orchestrator for system execution, located in helios::engine::runtime::gameloop. Systems are added to Phases and Passes:

import helios.engine.runtime.gameloop.GameLoop;

helios::engine::runtime::gameloop::GameLoop gameLoop;

// Pre Phase: Input processing
gameLoop.phase(PhaseType::Pre)
.addPass()
.addSystem<InputSystem>(gameWorld);

// Main Phase: Gameplay logic
gameLoop.phase(PhaseType::Main)
.addPass()
.addSystem<Move2DSystem>(gameWorld)
.addSystem<CollisionSystem>(gameWorld);

// Post Phase: Synchronization
gameLoop.phase(PhaseType::Post)
.addPass()
.addSystem<SceneSyncSystem>(gameWorld, scene);

// Initialize and run
gameLoop.init(gameWorld);
gameLoop.update(gameWorld, deltaTime, inputSnapshot);

Built-in Components

helios provides several ready-to-use components organized by domain:

Spatial/Transform

ComponentPurpose
ComposeTransformComponentStores local transform with dirty tracking
ScaleStateComponentUnit-based sizing using helios units (meters)
TranslationStateComponentTranslation delta for frame-based movement

Physics/Motion

ComponentPurpose
Move2DComponent2D movement parameters (max speed, acceleration, dampening)
HeadingComponentRotation state and target angle
DirectionComponentNormalized movement direction
SpinComponentContinuous rotation (spin speed)

Physics/Collision

ComponentPurpose
AabbColliderComponentAxis-aligned bounding box for collision
CollisionComponentCollision configuration (layer, mask, response type)

Gameplay

ComponentPurpose
LevelBoundsBehaviorComponentArena boundary interaction (bounce, clamp, wrap, despawn)
ShootComponentProjectile firing with cooldown timer
Aim2DComponentAiming direction for twin-stick controls

Spawn/Pool

ComponentPurpose
SpawnedByProfileComponentTracks which spawn profile created this entity

Scene

ComponentPurpose
SceneNodeComponentLinks a GameObject to a scene graph node

Input

ComponentPurpose
TwinStickInputComponentDual analog stick input mapping

Built-in Systems

Systems are organized by their typical Phase placement:

Pre Phase Systems

SystemPurpose
Input processing systemsRead input, generate Commands

Main Phase Systems

SystemPurpose
Move2DSystemApplies velocity/acceleration to ComposeTransformComponent
HeadingSystemSmoothly rotates entities toward target angle
SpinSystemApplies continuous rotation
BoundsUpdateSystemUpdates AABB colliders from transforms
GridCollisionDetectionSystemSpatial partitioning collision detection
LevelBoundsBehaviorSystemHandles boundary collision behaviors
ComposeTransformSystemComposes transform from translation/rotation/scale
ScaleSystemApplies scale changes
GameObjectSpawnSystemEvaluates spawn rules, creates spawn commands
ProjectileSpawnSystemHandles projectile spawning from shoot commands

Post Phase Systems

SystemPurpose
SceneSyncSystemSyncs transforms from gameplay to scene graph
TransformClearSystemClears dirty flags after frame

Creating Custom Components

Define a class inheriting from Component. Components should be placed in your project's modules namespace:

export module myproject.components.Inventory;

import helios.engine.ecs.Component;

export class InventoryComponent : public helios::engine::ecs::Component {
std::vector<Item> items_;

public:
void addItem(Item item) { items_.push_back(std::move(item)); }
const std::vector<Item>& items() const { return items_; }
};

For components that need cloning (e.g., for object pools), implement CloneableComponent:



class HealthComponent : public helios::engine::ecs::CloneableComponent {
int maxHealth_ = 100;
int currentHealth_ = 100;

public:
std::unique_ptr<Component> clone() const override {
auto copy = std::make_unique<HealthComponent>();
copy->maxHealth_ = maxHealth_;
copy->currentHealth_ = maxHealth_; // Reset to max on clone
return copy;
}
};

Creating Custom Systems

Define a class inheriting from System. Systems are registered with the GameLoop and operate on the GameWorld:

export module myproject.systems.Spawner;

import helios.engine.ecs.System;
import helios.engine.runtime.world.GameWorld;

export class SpawnerSystem : public helios::engine::ecs::System {
float timer_ = 0.0f;

public:
explicit SpawnerSystem(helios::engine::runtime::world::GameWorld& world)
: System(world) {}

void update(helios::engine::runtime::world::UpdateContext& ctx) noexcept override {
timer_ += ctx.deltaTime();
if (timer_ > 5.0f) {
timer_ = 0.0f;
// Queue spawn via CommandBuffer
ctx.commandBuffer().add<SpawnCommand>(position, enemyType);
}
}
};

Querying GameObjects

GameWorld provides efficient component-based queries:

// Find all GameObjects with specific components
for (auto* obj : gameWorld.find<Move2DComponent, SceneNodeComponent>()) {
// obj has both components
}

// Using structured bindings for direct component access
for (auto [obj, move, node] : gameWorld.find<Move2DComponent, SceneNodeComponent>().each()) {
// move and node are references to the components
}

// Filter by active state
for (auto* obj : gameWorld.find<HealthComponent>(GameObjectFilter::Active)) {
// Only active GameObjects
}

GameObjectFilter

The GameObjectFilter enum controls which GameObjects are included in query results:

import helios.engine.ecs.query.GameObjectFilter;

using helios::engine::ecs::query::GameObjectFilter;
FilterMeaning
GameObjectFilter::NoneNo filtering (default)
GameObjectFilter::ActiveOnly obj->isActive() == true
GameObjectFilter::InactiveOnly obj->isActive() == false
GameObjectFilter::ComponentEnabledOnly objects with enabled queried components
GameObjectFilter::ComponentDisabledOnly objects with disabled queried components
GameObjectFilter::AllAll GameObjects regardless of state

Filters can be combined using bitwise OR:

// Find inactive objects with disabled components
auto filter = GameObjectFilter::Inactive | GameObjectFilter::ComponentDisabled;
for (auto* obj : gameWorld.find<CollisionComponent>(filter)) {
// Process matching objects
}

Filtering Examples

Active GameObjects with enabled components:

void update(UpdateContext& ctx) noexcept override {
auto filter = GameObjectFilter::Active | GameObjectFilter::ComponentEnabled;

for (auto [obj, move, collision] : gameWorld_->find<Move2DComponent, CollisionComponent>(filter).each()) {
// Both GameObject is active AND components are enabled
// No manual checks required
}
}

Manual filtering (alternative approach):

void update(UpdateContext& ctx) noexcept override {
for (auto [obj, move, collision] : gameWorld_->find<Move2DComponent, CollisionComponent>().each()) {

// Skip inactive GameObjects
if (!obj->isActive()) {
continue;
}

// Skip disabled components
if (collision.isDisabled()) {
continue; // Movement still applies, but no collision
}

// Process with collision enabled...
}
}

Finding disabled components (e.g., for re-enabling):

// Find objects where invulnerability expired
for (auto [obj, invuln] : gameWorld_->find<InvulnerabilityComponent>(
GameObjectFilter::Active | GameObjectFilter::ComponentDisabled).each()) {
// Re-enable collision after invulnerability ends
obj->get<CollisionComponent>()->enable();
}

Update Order

The GameLoop executes updates in three phases:

┌─────────────────────────────────────────────────────────────────┐
│ FRAME │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PRE PHASE ────────────────────────────────────────────────── │
│ Pass 1: Input systems │
│ [Phase Commit: Commands flush, Managers process] │
│ │
│ MAIN PHASE ───────────────────────────────────────────────── │
│ Pass 1: Movement, Physics │
│ Pass 2: Collision Detection │
│ Pass 3: Gameplay Systems │
│ [Phase Commit: Commands flush, Managers process] │
│ │
│ POST PHASE ───────────────────────────────────────────────── │
│ Pass 1: Scene Sync │
│ Pass 2: Cleanup (clear dirty flags) │
│ [Phase Commit: Commands flush, Managers process] │
│ │
│ RENDER │
└─────────────────────────────────────────────────────────────────┘

See Game Loop Architecture for detailed phase/pass event handling.

Best Practices

Keep Components as Data Containers: Components should store state, not complex logic. Move update logic to Systems.

Use Systems for Cross-Cutting Concerns: Physics simulation, collision detection, and scene synchronization belong in Systems.

Prefer Composition: Configure entities by attaching different component combinations rather than creating specialized subclasses.

// Instead of: class Player : public GameObject { ... }

// Do this:
auto player = std::make_unique<GameObject>();
player->add<SceneNodeComponent>(node);
player->add<Move2DComponent>();
player->add<HeadingComponent>();
player->add<HealthComponent>();
player->add<TwinStickInputComponent>();

Use Commands for State Mutations: Instead of mutating state directly in Systems, emit Commands to the CommandBuffer for deterministic execution.