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.runtime.world.UpdateContext;

class PhysicsSystem {
public:
void update(UpdateContext& ctx) noexcept {
// Iterate all active entities with required components
for (auto [entity, move, transform, active] : ctx.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);

// Register managers for deferred processing
world.registerManager<SpawnManager>();

// Query entities by component using views
for (auto [entity, move, collision, active] : world.view<
Move2DComponent,
CollisionComponent,
Active
>().whereEnabled()) {
// 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

Components are plain data classes — they do not inherit from a base class. Components should be placed in your project's modules namespace:

export module myproject.components.Inventory;

export class InventoryComponent {
std::vector<Item> items_;
bool isEnabled_ = true;

public:
// Required: Copy and Move constructors
InventoryComponent(const InventoryComponent&) = default;
InventoryComponent(InventoryComponent&&) 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; }

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

Note: Components must provide both a copy constructor (used by the blueprint/pool system) and a noexcept move constructor for efficient storage in SparseSets. See Component Structure for full requirements.

Creating Custom Systems

Define a plain class with an update(UpdateContext&) method. Systems are registered with the GameLoop via addSystem<T>():

export module myproject.systems.Spawner;

import helios.engine.runtime.world.UpdateContext;

export class SpawnerSystem {
float timer_ = 0.0f;

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

Querying Entities

Systems query entities via UpdateContext::view<>(), which returns a View over all entities possessing the requested components. The Active tag component and .whereEnabled() filter replace the former GameObjectFilter API.

import helios.engine.runtime.world.UpdateContext;
import helios.engine.mechanics.lifecycle.components.Active;

// In a system's update():
void update(UpdateContext& ctx) noexcept {

// Iterate all active entities with specific components
for (auto [entity, move, node, active] : ctx.view<
Move2DComponent,
SceneNodeComponent,
Active
>().whereEnabled()) {
// entity is the EntityHandle
// move, node are pointers to the components
}
}

Active Filtering

Include the Active tag component in the view to restrict results to active entities. Call .whereEnabled() to additionally skip entities whose components report isEnabled() == false:

// Active entities with enabled components
for (auto [entity, health, active] : ctx.view<
HealthComponent,
Active
>().whereEnabled()) {
// Only entities that are active AND whose components are enabled
}

Unfiltered Iteration

Omitting the Active tag and .whereEnabled() iterates all entities with the requested components, regardless of activation state:

// All entities (active or inactive)
for (auto [entity, collision] : ctx.view<CollisionComponent>()) {
// Includes inactive entities and disabled components
}

Excluding Components

Use .exclude<T>() to skip entities that have a specific component:

for (auto [entity, move, active] : ctx.view<
Move2DComponent,
Active
>().exclude<FrozenTag>().whereEnabled()) {
// Entities with Move2DComponent but without FrozenTag
}

Initialization-Time Queries

Outside of a system's update() (e.g. during setup), queries go through GameWorld::view<>() directly:

for (auto [entity, transform, velocity, active] : gameWorld.view<
TransformComponent,
VelocityComponent,
Active
>().whereEnabled()) {
transform->position += velocity->direction * deltaTime;
}

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 deep inheritance hierarchies:

// Do this:
auto player = gameWorld.addGameObject();
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.