03 - Messages

Docs / Samples / 03 - Messages

Use the message bus for event-driven architecture. View publishes messages, systems consume them.

Prerequisites

  • .NET SDK 8.0+ or Unity 2021.3+
  • ZenECS Core package installed
  • Understanding of Message Bus

Run It

.NET Standalone

cd Packages/com.zenecs.core/Samples~/03-Messages
dotnet run

Unity

  1. Open Unity project
  2. Open scene: Packages/com.zenecs.core/Samples~/03-Messages/Scene.unity
  3. Press Play

Code Walkthrough

Step 1: Define Message

public struct DamageMessage
{
    public Entity Target;
    public float Amount;
}

Key Points:

  • Messages are struct (value types)
  • No methods, just data
  • Immutable by convention

Step 2: Publish Message

// Publish from anywhere
var world = KernelLocator.CurrentWorld;
world.Publish(new DamageMessage
{
    Target = entity,
    Amount = 10
});

Step 3: Consume Messages in System

[FixedGroup]
public class HealthSystem : ISystem
{
    public void Run(IWorld world, float deltaTime)
    {
        using var cmd = world.BeginWrite();
        
        // Consume all damage messages
        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);
                }
            }
        }
    }
}

Complete Example

using ZenECS.Core;
using ZenECS.Core.Systems;

// Message
public struct DamageMessage
{
    public Entity Target;
    public float Amount;
}

// Component
public struct Health
{
    public float Current;
    public float Max;
}

// 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 { Current = 100, Max = 100 });
}

// Publish damage
world.Publish(new DamageMessage { Target = entity, Amount = 25 });

// Process messages (happens during frame update)
kernel.PumpAndLateFrame(dt, fixedDelta, maxSubStepsPerFrame: 4);

Message Pump Timing

Messages are processed during frame updates:

// Frame structure
kernel.BeginFrame(dt);        // Messages pumped here
kernel.FixedStep(fixedDelta);  // Systems run here
kernel.LateFrame(dt, alpha);  // Presentation here

Key Points:

  • Messages are pumped in BeginFrame
  • Systems consume messages during FixedStep or LateFrame
  • Messages are deterministic (struct-based)

Patterns

Request-Response Pattern

// Request message
public struct GetHealthRequest
{
    public Entity Target;
}

// Response message
public struct HealthResponse
{
    public Entity Target;
    public float Current;
    public float Max;
}

// System handles request
[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
                });
            }
        }
    }
}

Event Pattern

// Event message
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}");
        }
    }
}

What to Try Next

Experiment 1: Multiple Message Types

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

Experiment 2: Message Chains

Chain messages together:

// First message triggers second
world.Publish(new DamageMessage { Target = entity, Amount = 10 });

// System processes and publishes result
[FixedGroup]
public class DamageSystem : ISystem
{
    public void Run(IWorld world, float deltaTime)
    {
        foreach (var msg in world.ConsumeMessages<DamageMessage>())
        {
            // Process damage
            world.Publish(new HealthChangedEvent { Entity = msg.Target });
        }
    }
}

Next Samples

See Also