Message Bus
Docs / Core / Message Bus
Struct-based pub/sub messaging system for event-driven architecture.
Overview
The Message Bus enables:
- Event-Driven Architecture: Systems react to messages
- Decoupling: Systems don't know about each other
- Deterministic Delivery: Struct-based, predictable
- Type-Safe: Compile-time type checking
Key Concepts
- Messages: Struct-based event data
- Publish: Send messages to the bus
- Consume: Systems consume messages
- Pump: Process messages during frame updates
How It Works
Message Flow
Publisher → Message Bus → Subscribers
- Publish:
world.Publish(new Message()) - Pump: Messages processed in
BeginFrame - Consume: Systems consume messages during execution
Message Structure
public struct DamageMessage
{
public Entity Target;
public float Amount;
}
Key Points:
- Messages are
struct(value types) - No methods, just data
- Immutable by convention
API Surface
Publishing Messages
// Publish message
world.Publish(new DamageMessage
{
Target = entity,
Amount = 10
});
Consuming Messages
// Consume messages in system
foreach (var msg in world.ConsumeMessages<DamageMessage>())
{
// Process message
}
Message Pumping
Messages are automatically pumped during BeginFrame:
kernel.BeginFrame(dt); // Messages pumped here
kernel.FixedStep(fixedDelta); // Systems consume here
Examples
Basic Message Pattern
using ZenECS.Core;
using ZenECS.Core.Systems;
// Message
public struct DamageMessage
{
public Entity Target;
public float Amount;
}
// System
[FixedGroup]
public sealed class HealthSystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
using var cmd = world.BeginWrite();
foreach (var msg in world.ConsumeMessages<DamageMessage>())
{
if (world.HasComponent<Health>(msg.Target))
{
ref var health = ref world.Ref<Health>(msg.Target);
health.Current -= msg.Amount;
if (health.Current <= 0)
{
cmd.DestroyEntity(msg.Target);
}
}
}
}
}
// Usage
var world = kernel.CreateWorld(null, "GameWorld");
world.AddSystems([new HealthSystem()]);
Entity entity;
using (var cmd = world.BeginWrite())
{
entity = cmd.CreateEntity();
cmd.AddComponent(entity, new Health(100, 100));
}
// Publish damage
world.Publish(new DamageMessage { Target = entity, Amount = 25 });
// Process (messages pumped in BeginFrame)
kernel.PumpAndLateFrame(dt, fixedDelta, maxSubStepsPerFrame: 4);
Multiple Message Types
[FixedGroup]
public class MultiMessageSystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
// Process damage
foreach (var msg in world.ConsumeMessages<DamageMessage>())
{
// Handle damage
}
// Process healing
foreach (var msg in world.ConsumeMessages<HealMessage>())
{
// Handle healing
}
}
}
Best Practices
✅ Do
- Use structs: Messages should be value types
- Keep immutable: Use readonly fields
- Consume in systems: Process messages during execution
- Type carefully: Choose appropriate message types
❌ Don't
- Don't store state: Messages are data only
- Don't mutate messages: Keep them immutable
- Don't publish in systems: Publish from external code
- Don't overuse: Use for events, not for data storage
Common Patterns
Event Pattern
public struct EntityDestroyedEvent
{
public Entity Entity;
public string Reason;
}
// Publish event
world.Publish(new EntityDestroyedEvent
{
Entity = entity,
Reason = "Health depleted"
});
// Subscribe to event
[FixedGroup]
public class EventHandlerSystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
foreach (var evt in world.ConsumeMessages<EntityDestroyedEvent>())
{
Debug.Log($"Entity {evt.Entity.Id} destroyed: {evt.Reason}");
}
}
}
Request-Response Pattern
// Request
public struct GetHealthRequest
{
public Entity Target;
}
// Response
public struct HealthResponse
{
public Entity Target;
public float Current;
public float Max;
}
// Handler
[FixedGroup]
public class HealthQuerySystem : ISystem
{
public void Run(IWorld world, float deltaTime)
{
foreach (var req in world.ConsumeMessages<GetHealthRequest>())
{
if (world.HasComponent<Health>(req.Target))
{
var health = world.Get<Health>(req.Target);
world.Publish(new HealthResponse
{
Target = req.Target,
Current = health.Current,
Max = health.Max
});
}
}
}
}
See Also
- Systems - System design
- Input → Intent - Input pattern
- World - World API