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 |
|---|---|
| Getting Started | ECS basics and World access |
| Entities | Create and manage entities |
| Components | Build components with data and methods |
Intermediate
| Guide | Description |
|---|---|
| Systems | Optional systems for performance |
| Queries | Find entities with specific components |
Advanced
| Guide | Description |
|---|---|
| Multi-Threading | Parallel entity processing |
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 |
Start with simple per-entity logic; switch to systems only when profiling shows a need.
// ? 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 assigned 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!