Events

An event is a mechanism for notifying the modes or the GUI that something has happened.

The SDK documentation describes events in the Mode Layer and the GUI Layer.

This tech tip describes the mode and GUI events with enough implementation details to understand the difference.

Switch and timer events are well documented in the SDK guide, so there is little to add. They will not be discussed further here.

Event Format

An event consists of the event name (string) and the event data (object). The event name is required. The event data is also required, but you can pass null if it is not used.

By convention, the event name starts with Evt_, such as Evt_BallSaveStart. This is not enforced, but highly recommended. An event that does not start with Evt_ always feels like an oversight.

Sending an Event

A mode event is an event sent by a mode. A GUI event is an event sent by the GUI. The two kinds of events have the same form (event name and event data). This point is important enough to repeat. The kind of event is determined by its origin, not by its destination or format.

A mode can send a mode event to the modes or to the GUI. To send an event to both the modes and the GUI, two separate calls are needed.

PostModeEventToModes(eventName, eventData);

PostModeEventToGUI(eventName, eventData);

The GUI can only send a GUI event to the modes.

PostGUIEventToModes(eventName, eventData);

There is no call to send a GUI event to the GUI. In a pinch, you could send a GUI-to-Modes event and resend it as a Mode-to-GUI event, but that’s a hack. Take the time to design a better solution within Unity.

The eventName argument is often expressed as a string literal, like "Evt_BallSaveStart".

All three Post methods above return void.

The eventData serves as the event's argument. The documentation for an event will say what kind of data is expected by that event. Most events accept a specific data type, but some can accept multiple data types.

Only one argument value can be passed, but that’s not a severe limitation. To pass multiple values, it is always possible to define a data class that holds multiple fields. It is also possible to pass an array holding multiple instances of the same data type.

Mode-to-Modes Event Handler

To handle an event, a mode must subscribe to the event. The event will be delivered only if the mode is started.

It is common to subscribe to an event in the constructor or in mode_started(). These subscriptions are active whenever the mode is active. The subscriptions can also be made at any time to control exactly when the subscription is active.

To subscribe to a Mode-to-Modes event, the mode must call:

AddModeEventHandler("Evt_ModeEventName", ModeEventHandler, Priority);

Replace Evt_ModeEventName with the name of the event. The call returns void.

The event handler method is implemented like this:

private bool ModeEventHandler(string eventName, object eventData)

{

    // Cast the event data to the type that is expected

    int data = (int)eventData;

    // Do something

    // Return EVENT_CONTINUE or EVENT_STOP;

    return EVENT_CONTINUE;

}

The event handler method can be named anything. It is customary to name it EventNameEventHandler without the Evt_ prefix.

Return EVENT_CONTINUE to let the event handlers at lower priority be called for this event. Return EVENT_STOP if you don’t want the event handlers at lower priority to be called after this event handler. It is preferable to return EVENT_CONTINUE unless you really need to stop the event propagation.

Sometimes the constant SWITCH_CONTINUE or SWITCH_STOP is returned by an event handler. Those constants have the same value as EVENT_CONTINUE and EVENT_STOP respectively. I find the constants EVENT_CONTINUE and EVENT_STOP to be clearer.

You can subscribe the event handler at any priority, but in practice, you will always want to use the same priority as the mode. The expression Priority above returns the mode priority through a property getter method.

It is possible to register multiple event handlers for the same event. You will likely never do this within a mode class, but it is conceivable that a base class registered an event handler for the same event and both will be called (unless stopped with EVENT_STOP).

GUI-to-Modes Event Handler

To subscribe to a GUI-to-Modes event, the mode must call:

AddGUIEventHandler("Evt_GUIEventName", GUIEventHandler);

Replace Evt_GUIEventName with the name of the GUI event. The call returns void.

The event handler method is implemented like this:

private void GUIEventHandler(string eventName, object eventData)

{

    // Cast the event data to the type that is expected

    bool data = (bool)eventData;

    // Do something

}

Notice AddGUIEventHandler() does not take a priority argument and the GUIEventHandler method returns void. For GUI-to-Modes events, all registered event handlers will be called (for active modes), and the order in which the handlers will be called is unspecified. If you need execution behavior similar to Mode-to-Modes events, the obvious solution is to register a single GUI event handler for a particular GUI event and repost the same or a similar event as a Mode-to-Modes event.

Mode-to-GUI Event Handler

To receive a Mode-to-GUI event, a GUI script must be a subclass of P3Aware. The script must override the CreateEventHandlers() method and subscribe to the events by calling AddModeEventHandler().

protected override void CreateEventHandlers() {

    // You must call the base class so all parent subscriptions are made

    base.CreateEventHandlers();

    AddModeEventHandler("Evt_ModeToGUIEvent", ModeToGUIEventHandler);

    // more AddModeEventHandler() calls here if applicable

}

 

public void ModeToGUIEventHandler(string eventName, object eventData) {

    // Cast the event data to the type that is expected

    string data = (string)eventData;

    // Do something.

}

Notice the subscriptions do not take a priority and the event handler returns void. All registered event handlers for this event will be called in an unspecified order.

Removing Mode Event Handlers

A mode can unsubscribe from an event. The event will no longer be delivered to that mode.

RemoveModeEventHandler("Evt_ModeEventName", ModeEventHandler);

RemoveGUIEventHandler("Evt_GUIEventName", GUIEventHandler);

This is usually done when the mode must remain active but should not respond to the event anymore.

Event subscriptions are automatically removed when the mode is stopped. The subscriptions are remembered and registered again if the mode is restarted. This is what makes it possible to subscribe to events in the mode constructor and not receive the events when the mode is inactive.

Removing GUI Event Handlers

A GUI script can unsubscribe from an event. The event will no longer be delivered to that script.

RemoveModeEventHandler("Evt_ModeToGUIEvent", ModeToGUIEventHandler);

Event subscriptions are automatically removed when the script is destroyed. A destroyed script cannot be restarted.

Execution Model

The modes in a P3 application run on the mode thread. The GUI runs on the GUI thread.

A Mode-to-Modes event is sent and handled on the same thread. This enables synchronous execution. The event is posted by PostModeEventToModes() and the event has been fully processed before the call returns. The event is delivered to the modes in priority order. An event handler can let the event propagate to lower priority modes by returning EVENT_CONTINUE, or it can return EVENT_STOP to stop further execution of the event.

A Mode-to-GUI event must cross a thread boundary. The mode thread cannot wait for the GUI thread to be available to handle the event. Instead, the mode event is put into a queue which is regularly processed by the GUI thread. It is almost always the case that PostModeEventToGUI() returns before the event is executed. GUI scripts do not have an inherent priority system like modes do. For this reason, the event handlers will be called in an unspecified order. If the order is unknown, it is impractical to have a GUI script block further propagation to other GUI scripts. This explains why the Mode-to-GUI event handlers return void and all the subscribed event handlers for the event will be called.

A GUI-to-Modes event is the same situation in reverse. The GUI event is put into a queue which is regularly processed by the mode thread. It is almost always the case that PostGUIEventToModes() returns before the event is executed. Here the SDK authors could have chosen to respect the mode priorities. I think they chose not to for symmetry with the Mode-to-GUI events. Therefore, the GUI-to-Modes event handlers return void and all the subscribed event handlers for the event will be called.

Request and Response Events

It is common for an event to be sent and no response is sent back to the sender.

A request event is a regular event, but the sender expects a response.

This technique works as follows:

You will likely want to remove the response event handler as soon as the response is received.

The documentation of the request event will specify the name of the response event and its event data format. For a Mode-to-Modes request event, the response will also be a Mode-to-Modes event. For a Mode-to-GUI request event, the response will be a GUI-to-Modes event, and vice-versa.

For Mode-to-Modes events, the event is handled before PostModeEventToModes() returns. The response will already be available when the call returns. The mode can choose to process the response directly in the response event handler, or it can save the response and process it when the post call returns.

For Mode-to-GUI or GUI-to-Modes request events, the execution is asynchronous. The post will return before the request event is handled. The response must be handled in the response event handler.

There are a few examples of request/response events in the SDK guide. For example, Evt_GameAttributesForServiceMode is the response to Evt_PostGameAttributesForServiceMode, and Evt_CustomLaunchPrepComplete is the response to Evt_CustomLaunchPrep.

EventManager

The class responsible for delivering Mode-to-Modes events is EventManager. A mode should not call the EventManager directly because that bypasses the auto-subscribe/unsubscribe when the mode is started/stopped.

A lesser-known fact is that a non-mode class running on the mode thread can also send a Mode-to-Modes event by calling the public static method EventManager.Post(). The SDK does this in some cases.

EventManager.Post(eventName, eventData);

I suspect the non-mode class could even subscribe to an event, but at this point, might as well create a real mode.

The class responsible for queuing and processing the Mode-to-GUI or GUI-to-Modes events is SafeEventManager. The functionality is implemented in instance methods. Static methods would not work because the P3Controller has 3 instances of SafeEventManager that work independently:

p3.GUIToModesEventManager

p3.ModesToGUIEventManager

p3.BallTrackingToGUIEventManager

Since SafeEventManager crosses thread boundaries, it must be thread-safe. It should be possible to call it from any thread. It would be interesting to see if the GUI could send events to itself by calling:

p3.ModesToGUIEventManager.Post(eventName, eventData);

Event Logging

EventManager and SafeEventManager have a feature to log events that are posted. The logging must be enabled on a per event basis by calling LogEventName():

Multimorphic.NetProcMachine.EventManager.LogEventName("Evt_ModeEventName", true);

p3.ModesToGUIEventManager.LogEventName("Evt_GUIEventName", true);

p3.GUIToModesEventManager.LogEventName("Evt_ModeToGUIEvent", true);

The slight syntax difference is because LogEventName() is a static method in EventManager but is an instance method in SafeEventManager.

You can also log the ball tracking events if you are interested:

p3.BallTrackingToGUIEventManager.LogEventName(eventName, true);

The last argument is true to start logging the specified event, or false to stop logging it.

The trace messages will appear in the log with this format:

Event: <eventName> posted by: <method>

Event: <eventName> received by: <method>

New event subscription to <eventName>: <handler>

The <method> following 'posted by' is always one of the Post methods (PostModeEventToModes, PostModeEventToGUI, PostGUIEventToModes), indicating the type of event sent. However, to determine where the call was made, you need to examine the stack trace.

The <method> after 'received by' precisely indicates where the event was handled. Conversely, the stack trace is generally unhelpful, as it originates from the SDK.

The <handler> is the name of the method that will be called when the event is received. The stack trace indicates where the event handler was added.

There is a trace message when adding an event handler, but there is no message when removing the handler.

Obfuscated Event Log

If you look in the event log, you will notice the <method> is often obfuscated when the sender is within the SDK. Here the token <private> appears as-is in the log. It does not represent a variable.

Event: Evt_ShowHighScores posted by: <private>

This can be defeated with the following code:

EventManager.FilterFromLog("Multimorphic.P3App.", false);

EventManager.FilterFromLog("Multimorphic.P3.", false);

EventManager.FilterFromLog("Multimorphic.NetProcMachine.", false);

p3.GUIToModesEventManager.FilterFromLog("Multimorphic.P3App.", false);

p3.GUIToModesEventManager.FilterFromLog("Multimorphic.P3.", false);

p3.GUIToModesEventManager.FilterFromLog("Multimorphic.NetProcMachine.", false);

p3.ModesToGUIEventManager.FilterFromLog("Multimorphic.P3App.", false);

p3.ModesToGUIEventManager.FilterFromLog("Multimorphic.P3.", false);

p3.ModesToGUIEventManager.FilterFromLog("Multimorphic.NetProcMachine.", false);

Event Discovery

The event logging feature is great, but it has a major drawback. What if you don’t know the names of the events you need to debug? There is no option to log every event posted before you can decide which events you truly need.

This can be solved with the help of Harmony.

The idea is to patch the Post methods with a prefix method that enables logging for the event being posted.

To install Harmony:

Edit Assets\Scripts\Modes\P3SABaseGameMode.cs

Add these statements at the top of the file.

using HarmonyLib;

using Multimorphic.NetProcMachine;

using Multimorphic.P3.Events;

Add this class immediately after the namespace directive.

    public class EventManagerPatches

    {

        public static void EventManagerPostPrefix(string eventName, object eventData)

        {

            EventManager.LogEventName(eventName, true);

        }

        public static void SafeEventManagerPostPrefix(SafeEventManager __instance, string eventName, object eventData)

        {

            __instance.LogEventName(eventName, true);

        }

    }

Add this static member declaration before the P3SABaseGameMode constructor.

       private static Harmony harmony;

Add these statements to the P3SABaseGameMode constructor. I couldn’t make the Harmony annotations work for some unknown reason, so I chose to Patch() the methods manually.

        harmony = new Harmony("com.example.patch");

        var eventManagerPost = typeof(EventManager).GetMethod("Post");

        var eventManagerPostPrefix = typeof(EventManagerPatches).GetMethod("EventManagerPostPrefix");

        harmony.Patch(eventManagerPost, prefix: new HarmonyMethod(eventManagerPostPrefix));

        var safeEventManagerPost = typeof(SafeEventManager).GetMethod("Post");

        var safeEventManagerPostPrefix = typeof(EventManagerPatches).GetMethod("SafeEventManagerPostPrefix");

        harmony.Patch(safeEventManagerPost, prefix: new HarmonyMethod(safeEventManagerPostPrefix));

Once you start the application, every posted event will appear in the log.

You can get a report by grep’ing into the Editor.log

"c:\Program Files\Git\usr\bin\grep.exe" "posted by" %USERPROFILE%\AppData\Local\Unity\Editor\Editor.log

Once you know the names of the events, you can replace this code with calls to LogEventName().

Optimizing Event Discovery

The code from the previous section logs every event. This will overburden the system and significantly slow down execution in Unity. You can improve performance by ignoring some high-frequency events.

Edit Assets\Scripts\Modes\P3SABaseGameMode.cs and replace the EventManagerPatches class with this new implementation:

    public class EventManagerPatches

    {

        private static HashSet<string> eventManagerEvents = new HashSet<string>();

        private static Dictionary<SafeEventManager, HashSet<string>> safeEventManagerEvents =

            new Dictionary<SafeEventManager, HashSet<string>>();

        private static List<string> verboseEvents = new List<string> {

            "AccelerometerEvent", "Grid Event", "Evt_GUIBurstEvent", "Evt_GameBurstEvent",

            "Evt_ShowLocationText", "Evt_VerticalLevelValue", "Evt_HorizontalLevelValue"

        };

        private static bool IsLoggableEvent(string eventName)

        {

            return !eventName.StartsWith("Evt_RunGUIInsertCommand") &&

                !eventName.StartsWith("Evt_SetLED") &&

                !eventName.StartsWith("Evt_AddGUIInsertScript") &&

                !eventName.StartsWith("Evt_RemoveGUIInsertScript") &&

                !eventName.StartsWith("Evt_AddLEDToSimulator") &&

                !verboseEvents.Contains(eventName);

        }

        public static void EventManagerPostPrefix(string eventName, object eventData)

        {

            if (IsLoggableEvent(eventName))

            {

                if (!eventManagerEvents.Contains(eventName))

                {

                    eventManagerEvents.Add(eventName);

                    EventManager.LogEventName(eventName, true);

                }

            }

        }

        public static void SafeEventManagerPostPrefix(SafeEventManager __instance, string eventName, object eventData)

        {

            if (!safeEventManagerEvents.ContainsKey(__instance))

            {

                safeEventManagerEvents.Add(__instance, new HashSet<string>());

            }

            if (IsLoggableEvent(eventName))

            {

                if (!safeEventManagerEvents[__instance].Contains(eventName))

                {

                    safeEventManagerEvents[__instance].Add(eventName);

                    __instance.LogEventName(eventName, true);

                }

            }

        }

    }

Special Considerations

Michael Ocean warns us against a common mistake when coding the GUI. When instantiating a prefab with a P3Aware component attached, you cannot send an event to the component immediately. If you do, nothing bad happens but the message will not be delivered to the component. The newly created component did not have time to subscribe to its events because CreateEventHandlers() is called in the Start() method. You have to wait until the component is started, which is likely to happen in the next frame.

There is an architectural choice between a permanent event handler in the SceneController or event handlers in individual components. As the number of objects receiving events grows, it is more tempting to distribute the event handling. When considering performance alone, it might be better to handle the events centrally and iterate over the relevant GameObjects.