Skip to content
This repository was archived by the owner on Dec 20, 2022. It is now read-only.

Events and the Event Queue

Gauthier Billot edited this page Jun 28, 2017 · 11 revisions

EgoCS Systems are by-design isolated from each other. For example, one System cannot and should never call another System's Update() method. Systems indirectly communicate with each other using Events.

A System broadcasts information to others by creating EgoCS Event Objects, and adding them to the EgoCS Event Queue. A System receives information from others by registering Event Handler Methods with EgoCS.

Event Objects

An Event Object, or EgoEvent, contains information about what happened: a new GameObject was created, a Component was attached, a player won the game, etc.

To create your own Event Object type, first create a new class deriving from EgoEvent:

// ExampleEvent.cs

public class ExampleEvent : EgoEvent
{
    
}

Next, add the info you want as public readonly fields to the class:

// ExampleEvent.cs

public class ExampleEvent : EgoEvent
{
    public readonly int i;
    public readonly float f;
}

Since an Event Object can be sent to multiple Event Handlers in its lifetime, all fields should be readonly to prevent accidental modification.

Finally, initialize the Event Object with a suitable constructor:

// ExampleEvent.cs

public class ExampleEvent : EgoEvent
{
    public readonly int i;
    public readonly float f;
    
    public ExampleEvent( int i, float f )
    {
        this.i = i;
        this.f = f;    
    }
}

Events Handlers

Event Handlers run logic when events happen. For example, when a player wins the game, and a WinEvent is created, you'll want an Event Handler somewhere to pop up a "You Win!" message and another one to end the game.

In EgoCS, Event Handlers are always private methods or lambdas in Systems. Event Handler Methods must only accept one argument, an EgoCS Event Object, and must return void:

// ExampleEventSystem.cs

public class ExampleEventSystem : EgoSystem
{
    void Handle( ExampleEvent e )
    {
        // ...
    }
}

Event Handler methods are registered with EgoCS by calling EgoEvents<E>.AddHandler() in the System's Start(), where the type E must be an EgoEvent and match the function argument of Handle():

// ExampleEventSystem.cs

public class ExampleEventSystem : EgoSystem
{
    public override void Start()
    {      
        // Add Event Handlers
        EgoEvents<ExampleEvent>.AddHandler( Handle );
    }
    
    void Handle( ExampleEvent e )
    {
        // ...
    }
}

It's recommended to name all Event Handler methods Handle. They can then be differentiated using their Event Object argument type via C# function overloading:

// ExampleEventSystem.cs

public class ExampleEventSystem : EgoSystem
{
    public override void Start()
    {        
        // Add Event Handlers
        EgoEvents<Example1Event>.AddHandler( Handle );
        EgoEvents<Example2Event>.AddHandler( Handle );
    }
    
    void Handle( Example1Event e )
    {
        // Successfully handles Example1Event events
        // ...
    }
    
    void Handle( Example2Event e )
    {
        // Successfully handles Example2Event events        
        // ...
    }
}

Event Handler methods can of course iterate over the System's Constraint's GameObjects:

// ExampleEventSystem.cs

public class ExampleEventSystem : EgoSystem<
    EgoSystem<Example>
>{
    public override void Start()
    {
        EgoEvents<Example1Event>.AddHandler( Handle );
    }
    
    void Handle( ExampleEvent )
    {
        constraint.ForEachGameObject( ( EgoComponent ego, Example example ) =>
        {
            // ...
        } );
    }
}

Creating Events

Like how Event Handlers can only be methods in Systems, only Systems can create and broadcast events. This is because they contain all your game's logic, some of which is used to determine whether or not a particular event happened.

To create an EgoCS Event, and to broadcast it to other Systems, create a new Event Object and pass it into EgoEvents<E>.AddEvent(), where type E matches the Event Object's type:

// ExampleEventSystem.cs
public class ExampleEventSystem : EgoSystem
{    
    public override void Update()
    {
        var e = new ExampleEvent();
        EgoEvents<UpdateEvent>.AddEvent( e );
    }
}

You can create events in a System's Start(), Update(), FixedUpdate() or even in Event Handler Methods. However, like in other Event-Driven programs, an Event Handler Method should NEVER create Event Object of the same type that it handles, as this might lead to an infinite loop:

// ExampleEventSystem.cs

public class ExampleEventSystem : EgoSystem
{    
    void Handle( ExampleEvent e )
    {
        EgoEvents<ExampleEvent>.AddEvent( new ExampleEvent() ); // DON'T DO THIS
    }
}

Event Queue

In a lot of Event-Driven programs, when an event happens, its handlers are immediately invoked. In EgoCS, Event Objects are added into a EgoCS's Event Queue, and handled all at once after every System has updated.

However, the idea of EgoCS having one Event Queue is incomplete. Thanks to C# generics, every Event Object type has its own Event Queue; EgoCS automatically iterates through each Queue and properly invokes Event Handlers for each Event Object type.

EgoCS uses (an) Event Queue(s) to minimize "temporal coupling" between Systems and Events.

For example, Systems A and B could care about GameObjects with Enemy Components attached. System A immediately destroys a GameObject during an Update(). Then, when System B updates, its references to that enemy are now invalid (null).

Unity3D does something like this when you call Destroy(): Any valid references to a given GameObject or Component will stay valid during the whole frame, because Destroy() will only delete the object at the very end of the frame when everything has finished updating.

You can read more about the motivation to use Event Queues here.

Event Queue Order

By default, the handling order for every remaining event type is undetermined, with some exceptions: AddedGameObject and every AddedComponent<C> events are always handled first, before any other event type. Conversely, DestroyedGameObject and every DestroyComponent<C> events are always handled after every other event type.

You can specify which event types get handled first (immediately after AddedGameObject and AddedComponent<C>) and last (right before DestroyedGameObject and DestroyComponent<C>) in your EgoInterface's Static Constructor:

using UnityEngine;

public class EgoInterface: MonoBehaviour
{
    static BreakoutInterface()
    {
        EgoSystems.Add(
            ...
        );

        EgoEvents.AddFront<ExampleFrontEvent1>();
        EgoEvents.AddFront<ExampleFrontEvent2>();
        EgoEvents.AddFront<ExampleFrontEvent3>();

        EgoEvents.AddEnd<ExampleEndEvent1>();
        EgoEvents.AddEnd<ExampleEndEvent2>();
        EgoEvents.AddEnd<ExampleEndEvent3>();

    }
    ...
}

In this example, ExampleFrontEvent1, then ExampleFrontEvent2, then ExampleFrontEvent3 will be handled first, followed by every unspecified event type, and finally ExampleEndEvent1, then ExampleEndEvent2, then ExampleEndEvent3 will be handled last.