ZenECS Core Architecture Overview
Comprehensive guide to ZenECS Core's internal structure and design principles
This document is for developers who want to deeply understand ZenECS Core's architecture. It covers internal structure, data flow, execution model, and extensibility.
Table of Contents
- Overall Overview
- Architecture Layers
- Core Components
- Data Flow
- Execution Loop
- Threading Model
- Dependency Injection and Service Composition
- Extensibility and Plugins
Overall Overview
ZenECS Core adopts a layered architecture to provide clear separation of concerns and high modularity.
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (Unity Adapter, Game Logic, Systems) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Public API Layer │
│ (IWorld, IKernel, IMessageBus, etc.) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Core Runtime Layer │
│ (World, Kernel, SystemRunner, ComponentPoolRepository) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ (ServiceContainer, BitSet, Worker, Scheduling) │
└─────────────────────────────────────────────────────────────┘
Design Principles
Single Responsibility Principle: Each module has one clear role
Dependency Inversion: Depend on interfaces rather than concrete implementations
Zero Allocation: Queries and enumeration operations minimize heap allocations
Deterministic Execution: Fixed-step simulation ensures reproducible results
Engine Independence: No dependency on Unity or specific game engines
Architecture Layers
1. Application Layer
The topmost layer where user code and game engine adapters reside.
Responsibilities:
- Implement game logic (systems, component definitions)
- Integration with Unity/other engines
- Game loop and frame management
Example:
// Code written in the application layer
[SimulationGroup]
public sealed class MoveSystem : IFixedRunSystem
{
public void Run(IWorld world, float fixedDelta)
{
foreach (var entity in world.Query<Position, Velocity>())
{
// Game logic
}
}
}
2. Public API Layer
The layer that defines interfaces exposed to external code.
Key Interfaces:
IKernel: Multi-world management and frame tick coordinationIWorld: Unified ECS API (entities, components, systems, queries, messages, etc.)IMessageBus: Pub/Sub messagingISystemRunner: System execution orchestration
Characteristics:
- Hides implementation details (encapsulation)
- Enhanced testability (mockable)
- Version compatibility maintenance
3. Core Runtime Layer
The layer that implements the core ECS logic.
Key Components:
Kernel
- Multi-world lifecycle management
- Frame tick coordination (BeginFrame → FixedStep×N → LateFrame)
- World indexing (lookup by ID/name/tag)
World
- Entity storage management (BitSet-based)
- Component pool repository
- System execution pipeline
- Message bus
- Binding router
- Permission hooks and validators
SystemRunner
- System group execution planning
- Execution order guarantee
- Lifecycle management (Initialize/Shutdown)
ComponentPoolRepository
- Type-specific component pool management
- Lazy initialization
- Pool cleanup on entity removal
4. Infrastructure Layer
The bottommost layer providing basic utilities and services.
Key Components:
ServiceContainer
- Lightweight DI container
- Hierarchical scope support (Root → World)
- Singleton and factory patterns
BitSet
- Entity alive flag management
- Memory-efficient set operations
Worker
- Asynchronous job scheduling
- Job queue management
Core Components
Kernel
Role: Top-level manager that manages multiple worlds and coordinates the game loop
Key Features:
public sealed class Kernel : IKernel
{
// World indexes
private readonly ConcurrentDictionary<WorldId, IWorld> _byId;
private readonly ConcurrentDictionary<string, HashSet<WorldId>> _byName;
private readonly ConcurrentDictionary<string, HashSet<WorldId>> _byTag;
// Time tracking
private float _simulationAccumulatorSeconds;
private long _frameCount;
private long _fixedFrameCount;
// Frame ticks
public void BeginFrame(float dt);
public void FixedStep(float fixedDelta);
public void LateFrame(float alpha);
public void PumpAndLateFrame(float dt, float fixedDelta, int maxSubStepsPerFrame);
}
Lifecycle:
Kernelcreation → Root ServiceContainer setupCreateWorld()→ Per-world ServiceContainer creation- Game loop:
BeginFrame→FixedStep×N→LateFrame Dispose()→ Cleanup all worlds
World
Role: ECS host representing a single simulation space
Internal Structure:
internal sealed partial class World : IWorld
{
private readonly IKernel _kernel;
private readonly ServiceContainer _scope; // DI scope
// Services (resolved from DI)
private readonly ISystemRunner _runner;
private readonly IComponentPoolRepository _componentPoolRepository;
private readonly IMessageBus _bus;
private readonly IBindingRouter _bindingRouter;
private readonly IPermissionHook _permissionHook;
// Entity storage
private BitSet _alive; // Alive flags
private int[] _generation; // Generation counter (prevents stale handles)
private Stack<int> _freeIds; // Recyclable IDs
private int _nextId; // Next ID to issue
}
Partial Class Separation:
The World class is split into 15 partial files by functionality:
World.cs: Basic structure and constructorWorldEntityApi.cs: Entity creation/deletionWorldComponentApi.cs: Component add/remove/queryWorldQueryApi.cs: Query and filteringWorldSystemsApi.cs: System registration/removalWorldMessagesApi.cs: Message busWorldBinderApi.cs: BindingWorldSnapshot.cs: Save/load- Others...
ComponentPoolRepository
Role: Repository that manages type-specific component pools
Structure:
internal sealed class ComponentPoolRepository : IComponentPoolRepository
{
// Type-specific pool storage
private Dictionary<Type, IComponentPool> _pools;
// Factory cache (minimizes reflection)
private static readonly ConcurrentDictionary<Type, Func<IComponentPool>> _poolFactories;
public IComponentPool GetPool<T>() where T : struct
{
// Lazy initialization: create pool on first use
if (!_pools.TryGetValue(typeof(T), out var pool))
{
pool = new ComponentPool<T>();
_pools.Add(typeof(T), pool);
}
return pool;
}
}
Characteristics:
- Type-specific separation: Each component type stored in independent arrays
- Lazy initialization: Pool created on first use (memory efficient)
- Cached factories: Minimizes reflection overhead
SystemRunner
Role: Orchestrates system execution pipeline
Execution Flow:
internal sealed class SystemRunner : ISystemRunner
{
private SystemPlan _plan; // Execution plan
public void BeginFrame(IWorld w, float dt)
{
// 1. Pump messages
_bus.PumpAll();
// 2. Execute FrameInput systems
RunGroup(SystemGroup.FrameInput, w, dt);
_worker.RunScheduledJobs(w);
// 3. Execute FrameSync systems
RunGroup(SystemGroup.FrameSync, w, dt);
_worker.RunScheduledJobs(w);
}
public void FixedStep(IWorld w, float fixedDelta)
{
// Fixed-step pipeline:
// FixedInput → FixedDecision → FixedSimulation → FixedPost
RunFixedGroup(SystemGroup.FixedInput, w, fixedDelta);
RunFixedGroup(SystemGroup.FixedDecision, w, fixedDelta);
RunFixedGroup(SystemGroup.FixedSimulation, w, fixedDelta);
RunFixedGroup(SystemGroup.FixedPost, w, fixedDelta);
}
public void LateFrame(IWorld w, float dt, float alpha)
{
// Presentation pipeline:
// FrameView → FrameUI → Apply binders
RunLateGroup(SystemGroup.FrameView, w, dt, alpha);
RunLateGroup(SystemGroup.FrameUI, w, dt, alpha);
_router.ApplyAll(w); // View binding
}
}
System Groups:
| Group | Execution Point | Purpose |
|---|---|---|
FixedInput |
FixedStep | Player input sampling |
FixedDecision |
FixedStep | AI, pathfinding, decisions |
FixedSimulation |
FixedStep | Physics, gameplay state updates |
FixedPost |
FixedStep | Cleanup, events, bookkeeping |
FrameInput |
BeginFrame | Device input, window events |
FrameSync |
BeginFrame | Camera, client prediction |
FrameView |
LateFrame | Interpolation, transforms, animation |
FrameUI |
LateFrame | UI, HUD, debug overlays |
Data Flow
Component Data Flow
┌─────────────┐
│ Component │ struct Position { float X, Y; }
│ Definition │
└──────┬──────┘
│
↓
┌──────────────────────────┐
│ ComponentPool<T> │ Stored as type-specific arrays
│ - Array: T[] │
│ - Index: Entity.Id │
└──────┬───────────────────┘
│
↓
┌──────────────────────────┐
│ World │ Unified API
│ - Get<T>(entity) │
│ - Ref<T>(entity) │
│ - Query<T...>() │
└──────┬───────────────────┘
│
↓
┌──────────────────────────┐
│ System │ Query and modify
│ foreach (var e in │
│ world.Query<T...>()) │
└──────────────────────────┘
Message Flow
┌──────────────┐
│ Publisher │ world.Publish(new DamageMessage(...))
└──────┬───────┘
│
↓
┌──────────────────┐
│ MessageBus │ Stored in message queue
│ - Queue<T> │
└──────┬───────────┘
│
↓ (On PumpAll call)
┌──────────────────┐
│ Subscribers │ Execute registered handlers
│ - Action<T> │
└──────────────────┘
Binding Flow (View ↔ Data)
┌──────────────────┐
│ World │ ECS data
│ - Components │
└──────┬───────────┘
│ Delta occurs
↓
┌──────────────────┐
│ BindingRouter │ Track changes
│ - DeltaTracker │
└──────┬───────────┘
│ On ApplyAll
↓
┌──────────────────┐
│ Binder │ Update view
│ OnDelta() │ sprite.transform.position = ...
└──────────────────┘
Execution Loop
Frame Structure
ZenECS uses a 3-phase frame structure:
┌─────────────────────────────────────────────────────────┐
│ BeginFrame(dt) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FrameInput Systems │ │
│ │ - Device input, window events │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FrameSync Systems │ │
│ │ - Camera, client prediction │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ FixedStep(fixedDelta) × N │
│ (Convert dt to fixed steps using accumulator) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FixedInput Systems │ │
│ │ - Player input sampling │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FixedDecision Systems │ │
│ │ - AI, pathfinding, decisions │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FixedSimulation Systems │ │
│ │ - Physics, gameplay state updates │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FixedPost Systems │ │
│ │ - Cleanup, events, bookkeeping │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ LateFrame(dt, alpha) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FrameView Systems │ │
│ │ - Interpolation, transforms, animation │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ FrameUI Systems │ │
│ │ - UI, HUD, debug overlays │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ BindingRouter.ApplyAll() │ │
│ │ - Apply view bindings │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Accumulator-Based Fixed-Step
To ensure fixed steps, ZenECS uses the Time Accumulator pattern:
public void PumpAndLateFrame(float dt, float fixedDelta, int maxSubStepsPerFrame)
{
BeginFrame(dt);
// Add dt to accumulator
_simulationAccumulatorSeconds += dt;
// Execute fixed steps (up to maxSubStepsPerFrame times)
int subSteps = 0;
while (_simulationAccumulatorSeconds >= fixedDelta && subSteps < maxSubStepsPerFrame)
{
FixedStep(fixedDelta);
_simulationAccumulatorSeconds -= fixedDelta;
subSteps++;
}
// Calculate interpolation factor (predict next frame)
float alpha = _simulationAccumulatorSeconds / fixedDelta;
LateFrame(dt, alpha);
}
Advantages:
- ✅ Deterministic simulation (fixed step)
- ✅ Frame drop handling (accumulated time processing)
- ✅ Spike protection (maxSubStepsPerFrame limit)
Threading Model
Single-Threaded Default Model
By default, ZenECS assumes a single-threaded model:
- System execution is sequential
- Component access is synchronous
- Message processing is single-threaded
Thread Safety Guarantees
Designed to be safely used in multi-threaded environments:
1. World Indexing (Kernel)
// Use ConcurrentDictionary
private readonly ConcurrentDictionary<WorldId, IWorld> _byId;
private readonly ConcurrentDictionary<string, HashSet<WorldId>> _byName;
2. Snapshots and Locks
- Thread-safe snapshots provided on world lookup
- Internal lock usage (read performance optimized)
3. Command Buffer
// Buffer structural changes during system execution
using (var cmd = world.BeginWrite())
{
cmd.CreateEntity();
cmd.AddComponent(entity, component);
} // Applied at safe boundaries
Worker and Job Scheduling
Jobs can be scheduled via the IWorker interface:
public interface IWorker
{
void ScheduleJob(IWorld world, Action job);
void RunScheduledJobs(IWorld world);
}
Usage Example:
// Schedule job from system
_worker.ScheduleJob(world, () =>
{
// Background work
});
// Execute jobs at end of frame
_worker.RunScheduledJobs(world);
Dependency Injection and Service Composition
Hierarchical DI Scopes
ZenECS uses hierarchical DI scopes:
Root ServiceContainer (App lifetime)
└─ KernelOptions
└─ Other app-global services
World ServiceContainer (World lifetime)
├─ IWorker
├─ IMessageBus
├─ IComponentPoolRepository
├─ IBindingRouter
├─ IContextRegistry
├─ IPermissionHook
└─ ISystemRunner
Bootstrap Process
// 1. Create root scope
var root = CoreBootstrap.BuildRoot(kernelOptions);
// 2. Create Kernel (using root scope)
var kernel = new Kernel(options, root);
// 3. Create world scope
var worldScope = CoreBootstrap.BuildWorldScope(worldConfig, root);
// 4. Create World
var world = new World(cfg, id, name, tags, kernel, worldScope);
ServiceContainer Characteristics
Lightweight DI Container:
- No external dependencies (pure C#)
- Singleton and factory pattern support
- Hierarchical scopes (Parent-Child)
- Seal functionality (immutability after composition)
- Reverse Dispose (child → parent)
Example:
internal static class CoreBootstrap
{
internal static ServiceContainer BuildWorldScope(WorldConfig cfg, ServiceContainer root)
{
var world = root.CreateChildScope();
// Register services
world.RegisterFactory<IWorker>(_ => new Worker(), asSingleton: true);
world.RegisterFactory<IMessageBus>(_ => new MessageBus(), asSingleton: true);
world.RegisterFactory<IComponentPoolRepository>(
_ => new ComponentPoolRepository(cfg.InitialPoolBuckets),
asSingleton: true);
world.Seal(); // No further registration
return world;
}
}
Extensibility and Plugins
Adapter Extension
ZenECS integrates with game engines through the adapter pattern:
Unity Adapter Example:
// Unity adapter extends World scope
var unityScope = worldScope.CreateChildScope();
unityScope.RegisterFactory<IUnityViewService>(
_ => new UnityViewService(),
asSingleton: true);
// Add Unity-specific services
Custom System Groups
To add a new system group:
- Add to
SystemGroupenum - Add group processing logic to
SystemPlanner - Add execution method to
SystemRunner
Custom Component Formatters
Custom formatters for snapshot save/load:
public interface IComponentFormatter
{
void WriteComponent(Stream stream, Type componentType, object component);
object ReadComponent(Stream stream, Type componentType);
}
// Use Binary formatter
world.Save(stream, new BinaryComponentFormatter());
// Or implement custom JSON formatter
world.Save(stream, new JsonComponentFormatter());
Hook and Validator Extensions
Add custom hooks to control component writes:
// Write permission hook
world.Hooks.AddWritePermission((entity, componentType) =>
{
// Custom logic
return true;
});
// Value validator
world.Hooks.AddValidator<Health>(health =>
{
// Custom validation logic
return health.Current >= 0 && health.Current <= health.Max;
});
Performance Optimization Strategies
1. Component Pooling
- Type-specific separation: Each component type in independent arrays
- Lazy initialization: Pool created on first use
- Capacity management: Auto-expansion when needed
2. Zero-Allocation Queries
// Struct-based enumerator (no heap allocation)
public struct QueryEnumerator<T> : IEnumerator<Entity>
{
// Value type enumerator
}
3. BitSet Optimization
Dense bitset for entity alive tracking:
- Fast lookup (O(1))
- Efficient memory usage
- Vectorizable operations
4. Filter Caching
// Cache filter interpretation results
private static readonly ConcurrentDictionary<FilterSignature, Filter> _cachedFilters;
5. System Plan Caching
// Rebuild plan only when systems are added/removed
private SystemPlan _plan;
private bool _dirty;
public void AddSystem(ISystem system)
{
_pendingAdd.Add(system);
_dirty = true; // Replan next frame
}
Architecture Diagrams
Overall Structure
┌────────────────────────────────────────────────────────────┐
│ Application │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Systems │ │ Components │ │ Adapters │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────┘
↓ uses
┌────────────────────────────────────────────────────────────┐
│ Public API │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ IKernel │ │ IWorld │ │ IMessageBus │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────┘
↓ implements
┌────────────────────────────────────────────────────────────┐
│ Core Runtime │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Kernel │ │ World │ │ SystemRunner │ │
│ │ │ │ │ │ │ │
│ │ - World mgmt │ │ - Entities │ │ - Exec plan │ │
│ │ - Frame ticks│ │ - Components │ │ - Group exec │ │
│ │ - Indexing │ │ - Systems │ │ - Lifecycle │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ComponentPoolRepository │ │
│ │ - Type-specific pool mgmt │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
↓ uses
┌────────────────────────────────────────────────────────────┐
│ Infrastructure │
│ ┌────────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ServiceContainer│ │ BitSet │ │ Worker │ │
│ └────────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────┘
World Internal Structure
┌────────────────────────────────────────────────────────────┐
│ World │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Entity Storage │ │
│ │ - BitSet _alive (Alive flags) │ │
│ │ - int[] _generation (Generation counter) │ │
│ │ - Stack<int> _freeIds (Recyclable IDs) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ComponentPoolRepository │ │
│ │ Dictionary<Type, IComponentPool> │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Pool<T1> │ │ Pool<T2> │ │ Pool<T3> │ ... │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SystemRunner │ │
│ │ - SystemPlan _plan │ │
│ │ - Group execution │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ MessageBus │ │
│ │ Dictionary<Type, Queue<IMessage>> │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ BindingRouter │ │
│ │ - DeltaTracker │ │
│ │ - Binder management │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
Summary
ZenECS Core has the following architectural characteristics:
✅ Layered Architecture: Clear separation of concerns
✅ DI-based Composition: Hierarchical service scopes
✅ Deterministic Execution: Fixed-step simulation
✅ Performance Optimization: Zero-allocation queries, component pooling
✅ Extensible: Adapter pattern, custom hooks
✅ Engine Independence: No dependency on Unity or specific game engines
This design enables ZenECS to be used flexibly and efficiently in various game projects.
Made with ❤️ by Pippapips Limited