Entity Component System (ECS)¶
The Entity Component System (ECS) in Evoker Engine provides a data-oriented architecture for managing game objects. It separates data (Components) from behavior and allows for flexible, composable game entities.
Overview¶
The ECS architecture consists of three main concepts:
- Entities: Unique identifiers for game objects
- Components: Data containers attached to entities
- Systems: Logic that operates on entities with specific components
Core Components¶
Entity¶
An entity is simply a unique identifier (uint ID):
Entities are lightweight and only serve as a handle to reference components.
Component¶
The base class for all components:
Components store data and are attached to entities.
ECSRegistry¶
The registry manages all entities and components:
public class ECSRegistry
{
public Entity CreateEntity();
public void DestroyEntity(Entity entity);
public T AddComponent<T>(Entity entity) where T : Component, new();
public T? GetComponent<T>(Entity entity) where T : Component;
public bool HasComponent<T>(Entity entity) where T : Component;
public void RemoveComponent<T>(Entity entity) where T : Component;
public IEnumerable<T> GetAllComponents<T>() where T : Component;
public IEnumerable<Entity> GetAllEntities();
}
Creating Entities¶
Basic Entity Creation¶
using EvokerEngine.ECS;
using EvokerEngine.Core;
// Get the scene's ECS registry
var scene = Application.Instance.ActiveScene;
var registry = scene.Registry;
// Create an entity
var entity = registry.CreateEntity();
// Add components
var transform = registry.AddComponent<TransformComponent>(entity);
transform.Position = new Vector3(0, 0, 0);
transform.Scale = Vector3.One;
Using Scene Helper¶
The Scene class provides a convenient method:
var scene = Application.Instance.ActiveScene;
// Creates entity and adds a NameComponent
var entity = scene.CreateEntity("MyEntity");
Built-in Components¶
TransformComponent¶
Stores position, rotation, and scale.
public class TransformComponent : Component
{
public Vector3 Position { get; set; }
public Vector3 Rotation { get; set; } // Euler angles in degrees
public Vector3 Scale { get; set; }
public Matrix4x4 GetTransformMatrix();
}
Example:
var transform = registry.AddComponent<TransformComponent>(entity);
transform.Position = new Vector3(5, 0, 10);
transform.Rotation = new Vector3(0, 45, 0); // Rotate 45° around Y
transform.Scale = new Vector3(2, 2, 2); // Double size
// Get transform matrix for rendering
var matrix = transform.GetTransformMatrix();
MeshRendererComponent¶
Defines what mesh and material to render.
public class MeshRendererComponent : Component
{
public int MeshId { get; set; }
public int MaterialId { get; set; }
}
Example:
var renderer = registry.AddComponent<MeshRendererComponent>(entity);
renderer.MeshId = 0; // Cube mesh
renderer.MaterialId = 1; // Red material
CameraComponent¶
Camera properties for rendering.
public class CameraComponent : Component
{
public float FieldOfView { get; set; } // In degrees
public float NearPlane { get; set; }
public float FarPlane { get; set; }
public bool IsPrimary { get; set; }
public Matrix4x4 GetProjectionMatrix(float aspectRatio);
}
Example:
var camera = registry.AddComponent<CameraComponent>(entity);
camera.FieldOfView = 60f;
camera.NearPlane = 0.1f;
camera.FarPlane = 1000f;
camera.IsPrimary = true; // Make this the active camera
// Get projection matrix
var projMatrix = camera.GetProjectionMatrix(16f / 9f);
InventoryComponent¶
Inventory system integration for entities.
Example:
// Player with inventory
var player = scene.CreateEntity("Player");
var inventory = registry.AddComponent<InventoryComponent>(player);
inventory.Inventory.AddItem(sword, 1);
inventory.Inventory.AddItem(potion, 5);
Creating Custom Components¶
Simple Component¶
using EvokerEngine.ECS;
public class HealthComponent : Component
{
public float MaxHealth { get; set; } = 100f;
public float CurrentHealth { get; set; } = 100f;
public bool IsAlive => CurrentHealth > 0f;
public void TakeDamage(float damage)
{
CurrentHealth = Math.Max(0f, CurrentHealth - damage);
}
public void Heal(float amount)
{
CurrentHealth = Math.Min(MaxHealth, CurrentHealth + amount);
}
}
Usage:
var health = registry.AddComponent<HealthComponent>(enemy);
health.MaxHealth = 200f;
health.CurrentHealth = 200f;
health.TakeDamage(50f);
if (health.IsAlive)
{
Logger.Info($"Enemy health: {health.CurrentHealth}");
}
Component with Complex Data¶
public class VelocityComponent : Component
{
public Vector3 Linear { get; set; } = Vector3.Zero;
public Vector3 Angular { get; set; } = Vector3.Zero;
public float Drag { get; set; } = 0.1f;
public void ApplyForce(Vector3 force, float mass)
{
Linear += force / mass;
}
}
Component with References¶
public class ParentComponent : Component
{
public Entity Parent { get; set; }
public List<Entity> Children { get; set; } = new();
public void AddChild(Entity child)
{
if (!Children.Contains(child))
{
Children.Add(child);
}
}
public void RemoveChild(Entity child)
{
Children.Remove(child);
}
}
Component Operations¶
Adding Components¶
var transform = registry.AddComponent<TransformComponent>(entity);
var health = registry.AddComponent<HealthComponent>(entity);
var velocity = registry.AddComponent<VelocityComponent>(entity);
Getting Components¶
// Get component (returns null if not found)
var transform = registry.GetComponent<TransformComponent>(entity);
if (transform != null)
{
transform.Position += Vector3.UnitY;
}
// Check if entity has component
if (registry.HasComponent<HealthComponent>(entity))
{
var health = registry.GetComponent<HealthComponent>(entity);
health.TakeDamage(10f);
}
Removing Components¶
// Remove a specific component
registry.RemoveComponent<VelocityComponent>(entity);
// Remove all components by destroying entity
registry.DestroyEntity(entity);
Querying Components¶
Get All Components of a Type¶
// Get all health components
var allHealth = registry.GetAllComponents<HealthComponent>();
foreach (var health in allHealth)
{
Logger.Info($"Entity {health.Entity.Id} has {health.CurrentHealth} HP");
}
Get All Entities¶
var allEntities = registry.GetAllEntities();
foreach (var entity in allEntities)
{
Logger.Info($"Entity: {entity.Id}");
}
System Pattern¶
While Evoker Engine doesn't enforce a specific system architecture, here's a recommended pattern:
Example: Movement System¶
public class MovementSystem
{
private readonly ECSRegistry _registry;
public MovementSystem(ECSRegistry registry)
{
_registry = registry;
}
public void Update(float deltaTime)
{
// Process all entities with Transform and Velocity
var velocities = _registry.GetAllComponents<VelocityComponent>();
foreach (var velocity in velocities)
{
var transform = _registry.GetComponent<TransformComponent>(velocity.Entity);
if (transform != null)
{
// Apply velocity
transform.Position += velocity.Linear * deltaTime;
transform.Rotation += velocity.Angular * deltaTime;
// Apply drag
velocity.Linear *= (1f - velocity.Drag * deltaTime);
velocity.Angular *= (1f - velocity.Drag * deltaTime);
}
}
}
}
Example: Health System¶
public class HealthSystem
{
private readonly ECSRegistry _registry;
public HealthSystem(ECSRegistry registry)
{
_registry = registry;
}
public void Update(float deltaTime)
{
var allHealth = _registry.GetAllComponents<HealthComponent>();
var entitiesToDestroy = new List<Entity>();
foreach (var health in allHealth)
{
if (!health.IsAlive)
{
// Mark for destruction
entitiesToDestroy.Add(health.Entity);
OnEntityDied(health.Entity);
}
}
// Clean up dead entities
foreach (var entity in entitiesToDestroy)
{
_registry.DestroyEntity(entity);
}
}
private void OnEntityDied(Entity entity)
{
Logger.Info($"Entity {entity.Id} died");
// Spawn death effects, drop loot, etc.
}
}
Example: Rendering System¶
public class RenderingSystem
{
private readonly ECSRegistry _registry;
public RenderingSystem(ECSRegistry registry)
{
_registry = registry;
}
public void Render()
{
// Get primary camera
CameraComponent? primaryCamera = null;
TransformComponent? cameraTransform = null;
var cameras = _registry.GetAllComponents<CameraComponent>();
foreach (var camera in cameras)
{
if (camera.IsPrimary)
{
primaryCamera = camera;
cameraTransform = _registry.GetComponent<TransformComponent>(camera.Entity);
break;
}
}
if (primaryCamera == null || cameraTransform == null)
return;
// Render all mesh renderers
var renderers = _registry.GetAllComponents<MeshRendererComponent>();
foreach (var renderer in renderers)
{
var transform = _registry.GetComponent<TransformComponent>(renderer.Entity);
if (transform != null)
{
RenderMesh(renderer, transform, primaryCamera, cameraTransform);
}
}
}
private void RenderMesh(MeshRendererComponent renderer,
TransformComponent transform,
CameraComponent camera,
TransformComponent cameraTransform)
{
// Rendering implementation
}
}
Complete Example¶
using EvokerEngine.Core;
using EvokerEngine.ECS;
using System.Numerics;
public class GameLayer : Layer
{
private MovementSystem _movementSystem;
private HealthSystem _healthSystem;
private List<Entity> _enemies = new();
public override void OnAttach()
{
var scene = Application.Instance.ActiveScene;
var registry = scene.Registry;
// Initialize systems
_movementSystem = new MovementSystem(registry);
_healthSystem = new HealthSystem(registry);
// Create player
var player = scene.CreateEntity("Player");
var playerTransform = registry.AddComponent<TransformComponent>(player);
playerTransform.Position = new Vector3(0, 0, 0);
var playerHealth = registry.AddComponent<HealthComponent>(player);
playerHealth.MaxHealth = 100f;
playerHealth.CurrentHealth = 100f;
var playerVelocity = registry.AddComponent<VelocityComponent>(player);
// Create enemies
for (int i = 0; i < 5; i++)
{
var enemy = scene.CreateEntity($"Enemy_{i}");
var enemyTransform = registry.AddComponent<TransformComponent>(enemy);
enemyTransform.Position = new Vector3(i * 5, 0, 10);
var enemyHealth = registry.AddComponent<HealthComponent>(enemy);
enemyHealth.MaxHealth = 50f;
enemyHealth.CurrentHealth = 50f;
_enemies.Add(enemy);
}
}
public override void OnUpdate(float deltaTime)
{
// Update systems
_movementSystem.Update(deltaTime);
_healthSystem.Update(deltaTime);
}
public override void OnEvent(Event e)
{
if (e is KeyPressedEvent keyEvent)
{
var scene = Application.Instance.ActiveScene;
var registry = scene.Registry;
// Find player
var allHealth = registry.GetAllComponents<HealthComponent>();
foreach (var health in allHealth)
{
if (health.MaxHealth == 100f) // Simple player identification
{
if (keyEvent.KeyCode == Key.Number1)
{
health.TakeDamage(10f);
}
}
}
}
}
}
Best Practices¶
✅ Do's¶
- Keep components as pure data containers
- Put logic in systems, not components
- Use composition over inheritance
- Query components efficiently
- Clean up destroyed entities
❌ Don'ts¶
- Don't store references to other registries in components
- Don't perform heavy computation in component properties
- Don't forget to null-check GetComponent() results
- Don't create circular component dependencies
Performance Tips¶
- Cache component queries - Don't query every frame if possible
- Batch operations - Process similar entities together
- Use component pools - Reuse components when possible
- Minimize component size - Keep components small and focused
// Bad: Query every frame
public override void OnUpdate(float deltaTime)
{
var health = registry.GetComponent<HealthComponent>(_player);
}
// Good: Cache the component
private HealthComponent _playerHealth;
public override void OnAttach()
{
_playerHealth = registry.GetComponent<HealthComponent>(_player);
}
public override void OnUpdate(float deltaTime)
{
// Use cached reference
if (_playerHealth != null)
{
// ...
}
}
See Also¶
- Scene Management - Scene and entity creation
- Application - Application lifecycle
- Layers - Layer system