Entity Component System¶
Learn how to use Brine2D's hybrid ECS (Entity-Component-System) to build flexible, maintainable game objects.
Quick Start¶
using Brine2D.ECS;
public class GameScene : Scene
{
protected override Task OnLoadAsync(CancellationToken ct)
{
// Create entity
var player = World.CreateEntity("Player");
// Add components (with methods!)
var health = player.AddComponent<HealthComponent>();
health.Current = 100;
health.Max = 100;
var transform = player.AddComponent<TransformComponent>();
transform.Position = new Vector2(400, 300);
// Components can have methods
health.TakeDamage(10); // Current = 90
Logger.LogInformation("Created player with {Health} HP", health.Current);
return Task.CompletedTask;
}
}
Topics¶
Getting Started¶
| Guide | Description | Difficulty |
|---|---|---|
| Getting Started | ECS basics and World access | ⭐ Beginner |
| Entities | Create and manage entities | ⭐ Beginner |
| Components | Build components with data and methods | ⭐ Beginner |
Intermediate¶
| Guide | Description | Difficulty |
|---|---|---|
| Systems | Optional systems for performance | ⭐⭐ Intermediate |
| Queries | Find entities with specific components | ⭐⭐ Intermediate |
Advanced¶
| Guide | Description | Difficulty |
|---|---|---|
| Multi-Threading | Parallel entity processing | ⭐⭐⭐ Advanced |
Key Concepts¶
Hybrid ECS¶
Brine2D uses a hybrid ECS - beginner-friendly with optional performance optimizations:
| Feature | Brine2D | Strict ECS (Unity DOTS) |
|---|---|---|
| Components with methods | ✅ Allowed | ❌ Data only |
| Entity logic | ✅ Allowed | ❌ System only |
| Easy to learn | ✅ Yes | ⚠️ Steep learning curve |
| Performance optimization | Optional systems | Required |
Philosophy: Start simple, optimize when needed.
// ✅ Brine2D - beginner-friendly
public class HealthComponent : Component
{
public int Current { get; set; }
public int Max { get; set; }
// Methods allowed!
public void TakeDamage(int amount)
{
Current = Math.Max(0, Current - amount);
}
}
// ❌ Strict ECS - data only
public struct HealthComponent
{
public int Current;
public int Max;
// No methods allowed!
}
World Property¶
Every scene has a World property - automatically set by the framework:
public class GameScene : Scene
{
// ✅ World available automatically - no injection!
protected override Task OnLoadAsync(CancellationToken ct)
{
// Access World directly
var player = World.CreateEntity("Player");
var enemy = World.CreateEntity("Enemy");
Logger.LogInformation("Created {Count} entities", World.Entities.Count);
return Task.CompletedTask;
}
}
Each scene gets its own isolated World - automatic cleanup!
ECS Architecture¶
graph TB
World["EntityWorld<br/>(Scoped per scene)"]
E1["Entity: Player"]
E2["Entity: Enemy1"]
E3["Entity: Enemy2"]
C1["HealthComponent"]
C2["TransformComponent"]
C3["SpriteComponent"]
S1["RenderSystem<br/>(Optional)"]
S2["PhysicsSystem<br/>(Optional)"]
World --> E1
World --> E2
World --> E3
E1 --> C1
E1 --> C2
E1 --> C3
E2 --> C1
E2 --> C2
E3 --> C1
E3 --> C2
S1 -.-> C3
S2 -.-> C2
style World fill:#2d5016,stroke:#4ec9b0,stroke-width:3px,color:#fff
style E1 fill:#1e3a5f,stroke:#569cd6,stroke-width:2px,color:#fff
style E2 fill:#1e3a5f,stroke:#569cd6,stroke-width:2px,color:#fff
style E3 fill:#1e3a5f,stroke:#569cd6,stroke-width:2px,color:#fff
style S1 fill:#4a2d4a,stroke:#c586c0,stroke-width:2px,stroke-dasharray: 5 5,color:#fff
style S2 fill:#4a2d4a,stroke:#c586c0,stroke-width:2px,stroke-dasharray: 5 5,color:#fff
Key insight: Systems are optional - use for performance optimization when needed.
Common Tasks¶
Create Entity¶
// Simple entity
var player = World.CreateEntity("Player");
// With components
var player = World.CreateEntity("Player");
player.AddComponent<HealthComponent>();
player.AddComponent<TransformComponent>();
player.AddComponent<SpriteComponent>();
Add Component¶
// Add and configure
var health = player.AddComponent<HealthComponent>();
health.Current = 100;
health.Max = 100;
// Chaining
player.AddComponent<HealthComponent>()
.AddComponent<TransformComponent>()
.AddComponent<SpriteComponent>();
Query Entities¶
// Find all entities with HealthComponent
var entities = World.Query<HealthComponent>().Execute();
foreach (var entity in entities)
{
var health = entity.GetComponent<HealthComponent>();
Logger.LogInformation("{Name} has {HP} HP", entity.Name, health.Current);
}
// Complex queries
var weakEnemies = World.Query()
.With<HealthComponent>()
.WithTag("Enemy")
.Where(e => e.GetComponent<HealthComponent>().Current < 50)
.Execute();
Create System (Optional)¶
public class MovementSystem : GameSystem
{
protected override void OnUpdate(GameTime gameTime)
{
var deltaTime = (float)gameTime.DeltaTime;
// Process all entities with required components
foreach (var entity in World.GetEntitiesWithComponents<TransformComponent, VelocityComponent>())
{
var transform = entity.GetComponent<TransformComponent>();
var velocity = entity.GetComponent<VelocityComponent>();
transform.Position += velocity.Value * deltaTime;
}
}
}
// Register system
builder.Services.AddSingleton<MovementSystem>();
Component Examples¶
Health Component¶
public class HealthComponent : Component
{
public int Current { get; set; }
public int Max { get; set; }
public bool IsDead => Current <= 0;
public float HealthPercent => (float)Current / Max;
public void TakeDamage(int amount)
{
Current = Math.Max(0, Current - amount);
if (IsDead && Entity != null)
{
Logger.LogInformation("{Name} died", Entity.Name);
Entity.World?.DestroyEntity(Entity);
}
}
public void Heal(int amount)
{
Current = Math.Min(Max, Current + amount);
}
}
Transform Component¶
public class TransformComponent : Component
{
public Vector2 Position { get; set; }
public float Rotation { get; set; }
public Vector2 Scale { get; set; } = Vector2.One;
public void Move(Vector2 delta)
{
Position += delta;
}
public void Rotate(float degrees)
{
Rotation += degrees;
}
}
Velocity Component¶
public class VelocityComponent : Component
{
public Vector2 Value { get; set; }
protected internal override void OnUpdate(GameTime gameTime)
{
// Components can update themselves!
var transform = GetRequiredComponent<TransformComponent>();
transform.Position += Value * (float)gameTime.DeltaTime;
}
}
More examples: Components Guide
Best Practices¶
✅ DO¶
- Use composition over inheritance - Combine components for behavior
- Keep components focused - One responsibility per component
- Use World property - Access via framework property
- Let automatic cleanup happen - World disposed on scene unload
- Add systems when needed - Optimize bottlenecks only
// ✅ Good - composition
var player = World.CreateEntity("Player");
player.AddComponent<HealthComponent>();
player.AddComponent<TransformComponent>();
player.AddComponent<PlayerInputComponent>();
// Components define behavior through composition
❌ DON'T¶
- Don't inject IEntityWorld - Use framework property
- Don't manually clear World - Automatic on scene unload
- Don't create deep inheritance - Use composition
- Don't optimize prematurely - Systems are optional
- Don't store entities in static fields - Prevents garbage collection
// ❌ Bad - don't inject World
public GameScene(IEntityWorld world)
{
// Framework property is better!
}
// ❌ Bad - deep inheritance
public class Enemy : Character : GameObject
{
// Use composition instead!
}
Performance Tips¶
Use Cached Queries¶
private CachedQuery<TransformComponent, VelocityComponent> _movingEntities;
protected override Task OnLoadAsync(CancellationToken ct)
{
// Create once
_movingEntities = World.CreateCachedQuery<TransformComponent, VelocityComponent>();
return Task.CompletedTask;
}
protected override void OnUpdate(GameTime gameTime)
{
// Reuse - no allocation!
foreach (var (transform, velocity) in _movingEntities)
{
transform.Position += velocity.Value * (float)gameTime.DeltaTime;
}
}
Use Systems for Hot Paths¶
// System - better for performance
public class MovementSystem : GameSystem
{
protected override void OnUpdate(GameTime gameTime)
{
// Batch processing - faster
foreach (var entity in World.GetEntitiesWithComponents<TransformComponent, VelocityComponent>())
{
// Process all at once
}
}
}
Troubleshooting¶
"World is null" Error¶
Symptom: NullReferenceException when accessing World
Cause: Trying to use World in constructor
Solution: Use lifecycle methods
// ❌ Wrong
public GameScene()
{
World.CreateEntity("Player"); // NULL!
}
// ✅ Correct
protected override Task OnLoadAsync(CancellationToken ct)
{
World.CreateEntity("Player"); // Works!
return Task.CompletedTask;
}
Component Not Found¶
Symptom: GetComponent<T>() returns null
Solutions:
- Use
GetRequiredComponent<T>()- Throws if missing
// ❌ May return null
var health = entity.GetComponent<HealthComponent>();
health.Current = 100; // NullReferenceException if not found!
// ✅ Throws if missing
var health = entity.GetRequiredComponent<HealthComponent>();
health.Current = 100; // Safe - component guaranteed to exist
- Check component exists:
if (entity.HasComponent<HealthComponent>())
{
var health = entity.GetComponent<HealthComponent>();
// Safe to use
}
Memory Leak¶
Symptom: Entities not destroyed after scene change
Cause: Storing entities in static fields or singleton services
Solution: Don't store entity references outside scene scope
// ❌ Bad - prevents garbage collection
public static Entity GlobalPlayer;
public class GameState // Singleton service
{
public Entity Player { get; set; } // ❌ Bad
}
// ✅ Good - store in scene
public class GameScene : Scene
{
private Entity? _player; // ✅ Destroyed with scene
}
Related Topics¶
- Getting Started - ECS basics
- Entities - Entity management
- Components - Component guide
- Systems - Optional systems
- Queries - Find entities
- Multi-Threading - Parallel processing
- Fundamentals: ECS Deep Dive - Architecture details
Ready to build with ECS? Start with Getting Started!