Skip to main content

ECS Systems Reference

Systems in Olympe Engine process entities with specific component combinations. Each system runs every frame and updates component data based on game logic.

System Architecture

Systems follow a consistent pattern:

class ExampleSystem {
public:
static void Process(float deltaTime);
};

Systems:

  • Are stateless (use static methods)
  • Process entities with specific component combinations
  • Run in a defined order each frame
  • Have access to World for entity queries

Core Systems

Movement System

File: Source/ECS_Systems.cpp

Processes entities with Position and Movement components.

// Processes: Position_data + Movement_data
void MovementSystem::Process(float deltaTime) {
auto entities = World::Get().GetEntitiesWithComponents<
Position_data, Movement_data
>();

for (EntityID entity : entities) {
auto* pos = World::Get().GetComponent<Position_data>(entity);
auto* mov = World::Get().GetComponent<Movement_data>(entity);

// Apply velocity
pos->position.x += mov->velocity.x * deltaTime;
pos->position.y += mov->velocity.y * deltaTime;

// Apply friction
float frictionFactor = 1.0f - (mov->friction * deltaTime);
mov->velocity.x *= frictionFactor;
mov->velocity.y *= frictionFactor;

// Clamp to max speed
float speed = Vector::Length(mov->velocity);
if (speed > mov->maxSpeed) {
mov->velocity = Vector::Normalize(mov->velocity) * mov->maxSpeed;
}
}
}

Purpose: Updates entity positions based on velocity and handles friction.

Physics System

Applies physics simulation to entities.

// Processes: Position_data + PhysicsBody_data
void PhysicsSystem::Process(float deltaTime) {
auto entities = World::Get().GetEntitiesWithComponents<
Position_data, PhysicsBody_data
>();

for (EntityID entity : entities) {
auto* pos = World::Get().GetComponent<Position_data>(entity);
auto* physics = World::Get().GetComponent<PhysicsBody_data>(entity);

// Apply gravity
if (physics->useGravity) {
physics->acceleration.y += 980.0f * deltaTime; // 9.8 m/s²
}

// Apply acceleration to velocity
physics->velocity.x += physics->acceleration.x * deltaTime;
physics->velocity.y += physics->acceleration.y * deltaTime;

// Apply drag
physics->velocity.x *= (1.0f - physics->drag);
physics->velocity.y *= (1.0f - physics->drag);

// Update position
pos->position.x += physics->velocity.x * deltaTime;
pos->position.y += physics->velocity.y * deltaTime;

// Reset acceleration
physics->acceleration = Vector(0, 0, 0);
}
}

Purpose: Simulates physics forces like gravity and drag.

Rendering Systems

Sprite Rendering System

Renders all visible sprites in the world.

// Processes: Position_data + VisualSprite_data
void SpriteRenderSystem::Process(SDL_Renderer* renderer) {
auto entities = World::Get().GetEntitiesWithComponents<
Position_data, VisualSprite_data
>();

// Sort by render layer (Z-order)
std::vector<EntityID> sortedEntities(entities.begin(), entities.end());
std::sort(sortedEntities.begin(), sortedEntities.end(),
[](EntityID a, EntityID b) {
auto* spriteA = World::Get().GetComponent<VisualSprite_data>(a);
auto* spriteB = World::Get().GetComponent<VisualSprite_data>(b);
return spriteA->renderLayer < spriteB->renderLayer;
});

// Render in layer order
for (EntityID entity : sortedEntities) {
auto* pos = World::Get().GetComponent<Position_data>(entity);
auto* sprite = World::Get().GetComponent<VisualSprite_data>(entity);

// Create source and destination rectangles
SDL_Rect src = {
sprite->sourceX, sprite->sourceY,
sprite->width, sprite->height
};

SDL_FRect dest = {
pos->position.x,
pos->position.y,
sprite->width * sprite->scale,
sprite->height * sprite->scale
};

// Apply flip
SDL_FlipMode flip = SDL_FLIP_NONE;
if (sprite->flipHorizontal) flip |= SDL_FLIP_HORIZONTAL;
if (sprite->flipVertical) flip |= SDL_FLIP_VERTICAL;

// Render
SDL_Texture* texture = TextureManager::Get(sprite->texturePath);
SDL_RenderTextureEx(renderer, texture, &src, &dest, 0, nullptr, flip);
}
}

Purpose: Renders all sprite components with proper Z-ordering.

Animation System

Updates sprite animations.

// Processes: VisualSprite_data + Animation_data
void AnimationSystem::Process(float deltaTime) {
auto entities = World::Get().GetEntitiesWithComponents<
VisualSprite_data, Animation_data
>();

for (EntityID entity : entities) {
auto* sprite = World::Get().GetComponent<VisualSprite_data>(entity);
auto* anim = World::Get().GetComponent<Animation_data>(entity);

if (!anim->playing) continue;

// Update timer
anim->frameTimer += deltaTime;

// Advance frame
if (anim->frameTimer >= anim->frameDuration) {
anim->frameTimer -= anim->frameDuration;
anim->currentFrame++;

// Handle loop/completion
if (anim->currentFrame >= anim->totalFrames) {
if (anim->loop) {
anim->currentFrame = 0;
} else {
anim->currentFrame = anim->totalFrames - 1;
anim->playing = false;
}
}

// Update sprite source rect
sprite->sourceX = (anim->currentFrame % 8) * sprite->width;
sprite->sourceY = (anim->currentFrame / 8) * sprite->height;
}
}
}

Purpose: Advances sprite animations frame by frame.

Input Systems

Input Event System

Processes keyboard and mouse input events.

// Processes: PlayerController_data + Controller_data
void InputEventSystem::Process(const SDL_Event& event) {
auto entities = World::Get().GetEntitiesWithComponents<
PlayerController_data, Controller_data
>();

for (EntityID entity : entities) {
auto* controller = World::Get().GetComponent<Controller_data>(entity);

if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.key) {
case SDLK_W: case SDLK_UP:
controller->inputDirection.y = -1.0f;
break;
case SDLK_S: case SDLK_DOWN:
controller->inputDirection.y = 1.0f;
break;
case SDLK_A: case SDLK_LEFT:
controller->inputDirection.x = -1.0f;
break;
case SDLK_D: case SDLK_RIGHT:
controller->inputDirection.x = 1.0f;
break;
case SDLK_SPACE:
controller->actionPressed = true;
break;
}
}
else if (event.type == SDL_EVENT_KEY_UP) {
switch (event.key.key) {
case SDLK_W: case SDLK_UP:
case SDLK_S: case SDLK_DOWN:
controller->inputDirection.y = 0.0f;
break;
case SDLK_A: case SDLK_LEFT:
case SDLK_D: case SDLK_RIGHT:
controller->inputDirection.x = 0.0f;
break;
case SDLK_SPACE:
controller->actionPressed = false;
break;
}
}
}
}

Purpose: Translates SDL input events to controller component state.

Player Movement System

Converts input to movement.

// Processes: PlayerController_data + Controller_data + Movement_data
void PlayerMovementSystem::Process(float deltaTime) {
auto entities = World::Get().GetEntitiesWithComponents<
PlayerController_data, Controller_data, Movement_data
>();

for (EntityID entity : entities) {
auto* playerCtrl = World::Get().GetComponent<PlayerController_data>(entity);
auto* controller = World::Get().GetComponent<Controller_data>(entity);
auto* movement = World::Get().GetComponent<Movement_data>(entity);

if (!playerCtrl->canMove) continue;

// Normalize input direction
Vector input = controller->inputDirection;
float inputMagnitude = Vector::Length(input);
if (inputMagnitude > 1.0f) {
input = Vector::Normalize(input);
}

// Apply acceleration in input direction
float targetSpeed = inputMagnitude * playerCtrl->moveSpeed;
Vector targetVelocity = input * targetSpeed;

// Lerp current velocity towards target
float accelRate = movement->acceleration * deltaTime;
movement->velocity.x += (targetVelocity.x - movement->velocity.x) * accelRate;
movement->velocity.y += (targetVelocity.y - movement->velocity.y) * accelRate;
}
}

Purpose: Converts player input to movement acceleration.

Collision Systems

Collision Detection System

Detects collisions between entities.

// Processes: Position_data + BoundingBox_data
void CollisionSystem::Process() {
auto entities = World::Get().GetEntitiesWithComponents<
Position_data, BoundingBox_data
>();

std::vector<EntityID> entityList(entities.begin(), entities.end());

// Check all pairs
for (size_t i = 0; i < entityList.size(); i++) {
for (size_t j = i + 1; j < entityList.size(); j++) {
EntityID entityA = entityList[i];
EntityID entityB = entityList[j];

auto* posA = World::Get().GetComponent<Position_data>(entityA);
auto* bboxA = World::Get().GetComponent<BoundingBox_data>(entityA);
auto* posB = World::Get().GetComponent<Position_data>(entityB);
auto* bboxB = World::Get().GetComponent<BoundingBox_data>(entityB);

// Create world-space rectangles
SDL_FRect rectA = {
posA->position.x + bboxA->boundingBox.x,
posA->position.y + bboxA->boundingBox.y,
bboxA->boundingBox.w,
bboxA->boundingBox.h
};

SDL_FRect rectB = {
posB->position.x + bboxB->boundingBox.x,
posB->position.y + bboxB->boundingBox.y,
bboxB->boundingBox.w,
bboxB->boundingBox.h
};

// Test intersection
if (SDL_RectsIntersect(&rectA, &rectB)) {
// Handle collision
OnCollision(entityA, entityB);
}
}
}
}

Purpose: Broad-phase collision detection using AABB tests.

AI Systems

Behavior Tree System

Executes behavior trees for AI entities.

// Processes: Position_data + AIBlackboard_data + BehaviorTree_data
void BehaviorTreeSystem::Process(float deltaTime) {
auto entities = World::Get().GetEntitiesWithComponents<
Position_data, AIBlackboard_data
>();

for (EntityID entity : entities) {
// Update behavior tree
BehaviorTree::UpdateEntity(entity, deltaTime);
}
}

Purpose: Runs AI behavior logic for NPCs and enemies.

System Execution Order

Systems execute in a specific order each frame:

void GameEngine::Update(float deltaTime) {
// 1. Input
InputEventSystem::Process(events);

// 2. AI
BehaviorTreeSystem::Process(deltaTime);

// 3. Player Control
PlayerMovementSystem::Process(deltaTime);

// 4. Physics
PhysicsSystem::Process(deltaTime);
MovementSystem::Process(deltaTime);

// 5. Collision
CollisionSystem::Process();

// 6. Animation
AnimationSystem::Process(deltaTime);

// 7. Health/Combat
HealthSystem::Process(deltaTime);

// 8. Cleanup
LifetimeSystem::Process(deltaTime);
}

void GameEngine::Render(SDL_Renderer* renderer) {
// Render systems
SpriteRenderSystem::Process(renderer);
DebugRenderSystem::Process(renderer);
}

Creating Custom Systems

System Template

// MyCustomSystem.h
#pragma once
#include "World.h"

class MyCustomSystem {
public:
static void Process(float deltaTime);
};

// MyCustomSystem.cpp
#include "MyCustomSystem.h"

void MyCustomSystem::Process(float deltaTime) {
// Query entities with required components
auto entities = World::Get().GetEntitiesWithComponents<
ComponentA, ComponentB
>();

// Process each entity
for (EntityID entity : entities) {
auto* compA = World::Get().GetComponent<ComponentA>(entity);
auto* compB = World::Get().GetComponent<ComponentB>(entity);

// Update component data
compA->value += compB->delta * deltaTime;
}
}

Registering System

Add to game loop in GameEngine.cpp:

void GameEngine::Update(float deltaTime) {
// ... other systems

MyCustomSystem::Process(deltaTime);

// ... more systems
}

System Best Practices

Performance

  • Query once - Store entity query results, don't re-query
  • Early exit - Skip inactive entities early
  • Batch operations - Process similar operations together
  • Avoid allocations - Reuse containers

Design

  • Single responsibility - One system, one purpose
  • Component combinations - Query specific component sets
  • Stateless - Use static methods, store no state
  • Order matters - Consider system dependencies

Example: Optimized System

void OptimizedSystem::Process(float deltaTime) {
// Cache entity query (only query once)
static std::vector<EntityID> entityCache;
entityCache.clear();

auto entities = World::Get().GetEntitiesWithComponents<
Position_data, Movement_data
>();

// Copy to vector for indexed access
entityCache.assign(entities.begin(), entities.end());

// Process in batches
const size_t batchSize = 64;
for (size_t start = 0; start < entityCache.size(); start += batchSize) {
size_t end = std::min(start + batchSize, entityCache.size());

for (size_t i = start; i < end; i++) {
EntityID entity = entityCache[i];
// Process entity...
}
}
}

Debugging Systems

Enable System Logging

#define DEBUG_SYSTEMS 1

void MovementSystem::Process(float deltaTime) {
#ifdef DEBUG_SYSTEMS
std::cout << "[MovementSystem] Processing "
<< entities.size() << " entities\n";
#endif

// ... system logic
}

Profile System Performance

#include <chrono>

void PhysicsSystem::Process(float deltaTime) {
auto start = std::chrono::high_resolution_clock::now();

// ... system logic

auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(
end - start
);

std::cout << "[PhysicsSystem] Time: " << duration.count() << "μs\n";
}

See Also