System
System is a type-erased wrapper for game logic processors. Concrete systems are plain classes that provide an update(UpdateContext&) method — they do not inherit from System. The wrapper uses the Concept/Model pattern to erase the concrete type at compile time.
Overview
Systems follow the ECS pattern of separating behavior from data:
- Components hold data
- Systems implement behavior
class PhysicsSystem {
public:
void update(UpdateContext& ctx) noexcept {
for (auto [entity, transform, velocity, active] : ctx.view<
TransformComponent,
VelocityComponent,
Active
>().whereEnabled()) {
transform->position += velocity->direction * ctx.deltaTime();
}
}
};
Systems are registered via SystemRegistry::add<T>() which wraps them in a System internally.
Architecture
┌────────────────────────────────────────────────────────────────┐
│ SystemRegistry │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ System │ │ System │ ... │
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│ │ │PhysicsSys │ │ │ │CollisionSys│ │ (plain classes) │
│ │ └────────────┘ │ │ └────────────┘ │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └─────────┬───────────┘ │
│ ▼ │
│ GameWorld Data │
│ (Entities, Components) │
└────────────────────────────────────────────────────────────────┘
Type Erasure Design
System (value type, owns erased Concept via unique_ptr)
└── Concept (internal virtual interface)
└── Model<T> (typed wrapper, owns T by value)
└── T (plain class, provides update())
User code defines plain classes. System is infrastructure-only and erases the concrete type.
API Reference
System (Type-Erased Wrapper)
namespace helios::engine::runtime::world {
template<typename T>
concept HasInit = requires(T& t, GameWorld& gw) {
{ t.init(gw) } -> std::same_as<void>;
};
template<typename T>
concept HasUpdate = requires(T& t, UpdateContext& ctx) {
{ t.update(ctx) } -> std::same_as<void>;
};
class System {
class Concept { /* virtual update(), init(), underlying() */ };
template<typename T>
class Model final : public Concept {
T system_; // owned by value
};
std::unique_ptr<Concept> pimpl_;
public:
template<typename T>
requires HasUpdate<T>
explicit System(T system);
void update(UpdateContext&) noexcept;
void init(GameWorld&) noexcept;
[[nodiscard]] void* underlying() noexcept;
[[nodiscard]] const void* underlying() const noexcept;
};
}
System erases the concrete type via the Concept/Model pattern:
update()delegates toT::update(UpdateContext&)(required viaHasUpdate<T>).init()conditionally callsT::init(GameWorld&)ifHasInit<T>is satisfied.underlying()returns a type-erased pointer to the owned system instance.
System is move-only (non-copyable).
SystemRegistry
SystemRegistry is a type alias for ConceptModelRegistry<System, SystemTypeId> (from helios.core.container):
import helios.core.container.ConceptModelRegistry;
import helios.engine.runtime.world.System;
import helios.engine.runtime.world.types.SystemTypeId;
namespace helios::engine::runtime::world {
using SystemRegistry = helios::core::container::ConceptModelRegistry<System, types::SystemTypeId>;
// Inherited API from ConceptModelRegistry:
// template<typename T, typename... Args> T& add(Args&&...);
// template<typename T> [[nodiscard]] T* item() const;
// template<typename T> [[nodiscard]] bool has() const;
// [[nodiscard]] std::span<System* const> items() const noexcept;
}
SystemRegistry creates and owns System wrappers. add<T>() returns a reference to the underlying T instance. items() returns a span in insertion order for deterministic iteration.
Required: update()
Every concrete system must provide:
void update(UpdateContext& ctx) noexcept {
// Per-frame logic
}
Optional: init()
Systems that need one-time initialization can optionally define:
void init(GameWorld& world) {
// Called once when the system is initialized
}
Detection uses the HasInit concept — no runtime cost if omitted.
Creating Systems
Basic System
class GravitySystem {
public:
void update(UpdateContext& ctx) noexcept {
for (auto [entity, velocity, gravity, active] : ctx.view<
VelocityComponent,
GravityComponent,
Active
>().whereEnabled()) {
velocity->velocity.y -= gravity->strength * ctx.deltaTime();
}
}
};
System with State
class SpawnTimerSystem {
float spawnTimer_ = 0.0f;
float spawnInterval_ = 2.0f;
public:
void update(UpdateContext& ctx) noexcept {
spawnTimer_ += ctx.deltaTime();
if (spawnTimer_ >= spawnInterval_) {
spawnTimer_ = 0.0f;
ctx.queueCommand<SpawnCommand>(position, enemyType);
}
}
};
System with Initialization
class CollisionSystem {
std::unique_ptr<SpatialGrid> grid_;
public:
void init(GameWorld& gameWorld) {
grid_ = std::make_unique<SpatialGrid>(worldBounds_, cellSize_);
}
void update(UpdateContext& ctx) noexcept {
grid_->clear();
// Populate grid and detect collisions
}
};
Registration
Systems are added to game loop passes via addSystem<T>():
gameLoop.addPhase("gameplay")
.addPass<AlwaysRunPass>()
.addSystem<GravitySystem>()
.addSystem<CollisionSystem>();
Best Practices
1. Keep Systems Focused
Each system should have a single responsibility:
// Good: Focused systems
class MovementSystem { void update(UpdateContext&) noexcept; };
class CollisionSystem { void update(UpdateContext&) noexcept; };
class DamageSystem { void update(UpdateContext&) noexcept; };
2. Use Views for Queries
for (auto [e, t, v, a] : ctx.view<Transform, Velocity, Active>().whereEnabled()) { }
3. Defer Structural Changes
Use commands for entity creation/destruction during update:
void update(UpdateContext& ctx) noexcept {
for (auto [entity, health, active] : ctx.view<
HealthComponent, Active
>().whereEnabled()) {
if (health->isDead()) {
ctx.queueCommand<DespawnCommand>(entity.entityHandle());
}
}
}
4. noexcept Guarantee
System updates must not throw exceptions.
See Also
- View - Component-based entity queries
- Game Loop Architecture - How systems execute
- Command System - Deferred operations
- Component System - ECS overview