What is ECS?
Docs / Overview / What is ECS
Entity-Component-System (ECS) is a data-oriented architecture that separates data (components) from behavior (systems). This pattern enables scalable, maintainable, and performant game logic.
Overview
Traditional object-oriented programming uses classes that combine data and behavior:
// OOP approach
class Player
{
public Vector3 Position { get; set; }
public float Health { get; set; }
public void Update(float deltaTime)
{
// Behavior mixed with data
Position += Velocity * deltaTime;
}
}
ECS separates these concerns:
// ECS approach
// Component: Pure data
public struct Position { public float X, Y, Z; }
public struct Velocity { public float X, Y, Z; }
public struct Health { public float Current, Max; }
// System: Pure behavior
public class MovementSystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
foreach (var (entity, pos, vel) in world.Query<Position, Velocity>())
{
// Transform data
world.ReplaceComponent(entity, new Position(
pos.X + vel.X * deltaTime,
pos.Y + vel.Y * deltaTime,
pos.Z + vel.Z * deltaTime
));
}
}
}
How It Works
The Three Pillars
1. Entities
Entities are unique identifiers that represent game objects. They have no data or behavior—they're just IDs.
// Create entity using command buffer
Entity entity;
using (var cmd = world.BeginWrite())
{
entity = cmd.CreateEntity(); // Just an ID
}
2. Components
Components are pure data structures (structs) that define properties. They have no methods or behavior.
public struct Position
{
public float X, Y, Z;
}
public struct Velocity
{
public float X, Y, Z;
}
// Attach components to entities using command buffer
using (var cmd = world.BeginWrite())
{
cmd.AddComponent(entity, new Position(0, 0, 0));
cmd.AddComponent(entity, new Velocity(1, 0, 0));
}
3. Systems
Systems are pure logic that operate on entities with specific component combinations. They query entities and transform their components.
public class MovementSystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
// Query all entities with Position AND Velocity
foreach (var (entity, pos, vel) in world.Query<Position, Velocity>())
{
// Transform data
var newPos = new Position(
pos.X + vel.X * deltaTime,
pos.Y + vel.Y * deltaTime,
pos.Z + vel.Z * deltaTime
);
world.ReplaceComponent(entity, newPos);
}
}
}
Data Flow
┌─────────────┐
│ Component │ Data structure (struct)
│ Definition │
└──────┬──────┘
│
↓
┌─────────────┐
│ Entity │ ID + Component references
│ │
└──────┬──────┘
│
↓
┌─────────────┐
│ System │ Query entities → Transform components
│ │
└─────────────┘
API Surface
Core Concepts
World: A container for entities, components, and systems
var world = kernel.CreateWorld(null, "GameWorld");
Entity: A unique identifier
// Create entity using command buffer
Entity entity;
using (var cmd = world.BeginWrite())
{
entity = cmd.CreateEntity();
}
Component: Pure data structure
public struct Position { public float X, Y, Z; }
using (var cmd = world.BeginWrite())
{
cmd.AddComponent(entity, new Position(0, 0, 0));
}
System: Logic that processes entities
public class MovementSystem : ISystem
{
public void Run(IWorld world, float deltaTime) { }
}
world.AddSystems([new MovementSystem()]);
Query: Find entities with specific components
foreach (var (entity, pos, vel) in world.Query<Position, Velocity>())
{
// Process entities with both Position and Velocity
}
Examples
Basic Example: Moving Entities
using ZenECS.Core;
// 1. Create world
var kernel = new Kernel();
var world = kernel.CreateWorld(null, "GameWorld");
// 2. Define components
public struct Position { public float X, Y; }
public struct Velocity { public float X, Y; }
// 3. Create system
[FixedGroup]
public class MovementSystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
using var cmd = world.BeginWrite();
foreach (var (entity, pos, vel) in world.Query<Position, Velocity>())
{
cmd.ReplaceComponent(entity, new Position(
pos.X + vel.X * deltaTime,
pos.Y + vel.Y * deltaTime
));
}
}
}
// 4. Register system
world.AddSystems([new MovementSystem()]);
// 5. Create entities
using (var cmd = world.BeginWrite())
{
var player = cmd.CreateEntity();
cmd.AddComponent(player, new Position(0, 0));
cmd.AddComponent(player, new Velocity(1, 0));
}
// 6. Run simulation
const float fixedDelta = 1f / 60f;
kernel.PumpAndLateFrame(Time.deltaTime, fixedDelta, maxSubStepsPerFrame: 4);
Advanced Example: Health System
public struct Health
{
public float Current;
public float Max;
}
public struct DamageMessage
{
public Entity Target;
public float Amount;
}
[FixedGroup]
public class HealthSystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
// Process damage messages
foreach (var msg in world.ConsumeMessages<DamageMessage>())
{
if (world.HasComponent<Health>(msg.Target))
{
var health = world.ReadComponent<Health>(msg.Target);
var newHealth = new Health(health.Current - msg.Amount, health.Max);
using (var cmd = world.BeginWrite())
{
if (newHealth.Current <= 0)
{
cmd.DestroyEntity(msg.Target);
}
else
{
cmd.ReplaceComponent(msg.Target, newHealth);
}
}
}
}
}
}
Benefits of ECS
1. Performance
- Cache-friendly: Components stored in contiguous arrays
- Parallel processing: Systems can run independently
- Zero allocation: Queries use struct enumerators
2. Scalability
- Add components dynamically: Entities gain capabilities by adding components
- Composition over inheritance: Mix and match components freely
- System isolation: Systems don't depend on each other
3. Maintainability
- Separation of concerns: Data, logic, and presentation are separate
- Testability: Systems are pure functions, easy to test
- Flexibility: Change behavior by adding/removing systems
4. Clarity
- Explicit data flow: Easy to see what data systems use
- No hidden state: Components are explicit
- Clear dependencies: Systems declare what they need
ECS vs OOP
| Aspect | OOP | ECS |
|---|---|---|
| Data & Behavior | Combined in classes | Separated (components vs systems) |
| Inheritance | Class hierarchies | Composition (add components) |
| Performance | Cache misses common | Cache-friendly arrays |
| Flexibility | Rigid hierarchies | Dynamic composition |
| Testing | Mock dependencies | Pure functions |
FAQ
Q: Do I need to use ECS for everything?
A: No. ECS is great for game logic, but UI, file I/O, and other tasks can use traditional patterns.
Q: Is ECS harder to learn?
A: Initially yes, but the separation of concerns makes complex systems easier to manage long-term.
Q: Can I mix ECS with OOP?
A: Yes! ZenECS integrates with Unity's GameObject system and other OOP code.
Q: What about performance?
A: ECS is designed for performance, but ZenECS prioritizes clarity and maintainability. For maximum performance, consider Unity DOTS.
See Also
- ZenECS at a Glance - Overview of ZenECS
- Architecture - ZenECS architecture details
- Quick Start - Get started with ZenECS
- Core Concepts - Deep dive into core concepts