Skip to content

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!
}

Learn more: ECS Deep Dive


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!

Learn more: Getting Started


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>();

Full guide: Entities


Add Component

// Add and configure
var health = player.AddComponent<HealthComponent>();
health.Current = 100;
health.Max = 100;

// Chaining
player.AddComponent<HealthComponent>()
      .AddComponent<TransformComponent>()
      .AddComponent<SpriteComponent>();

Full guide: Components


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();

Full guide: Queries


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>();

Full guide: Systems


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

  1. Use composition over inheritance - Combine components for behavior
  2. Keep components focused - One responsibility per component
  3. Use World property - Access via framework property
  4. Let automatic cleanup happen - World disposed on scene unload
  5. 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

  1. Don't inject IEntityWorld - Use framework property
  2. Don't manually clear World - Automatic on scene unload
  3. Don't create deep inheritance - Use composition
  4. Don't optimize prematurely - Systems are optional
  5. 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;
    }
}

Learn more: Queries


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
        }
    }
}

Learn more: Systems


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:

  1. 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
  1. 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
}


Ready to build with ECS? Start with Getting Started!