An event is a way to tell the modes or the GUI that something happened.
Events are documented in the SDK in file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/_mode_layer.html#ModeEvents and file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/_g_u_i_layer.html#GUIEvents
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 and there is not much to add. They will not be discussed further here.
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_ like Evt_BallSaveStart. This is not enforced but it is highly recommended. An event that does not start with Evt_ always feels like an oversight.
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 is important so I will repeat it. 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 crunch, you could send a GUI to modes event and send it back 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 data is the argument of the event. 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 even accept a choice of data types.
There is only one event data, 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.
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 following this event handler. It is preferable to return EVENT_CONTINUE unless you really need to stop the event execution.
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 though 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 a base class registered an event handler for the same event and both will be called (unless stopped with EVENT_STOP).
To subscribe to a GUI to mode 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 mode 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 similar execution behavior than 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.
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.
A mode can unsubscribe to 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.
A GUI script can unsubscribe to 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.
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 is fully executed 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. The GUI scripts have no fundamental priority 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 mode 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 mode event handlers return void and all the subscribed event handlers for the event will be called.
It is common for an event to be sent and there is no response to the sender.
A request event is a regular event but the sender expects a response in return.
This technique works as follows: the sender subscribes to the response event, the sender sends the request event, the request event handler sends a response event with the response value stored in the event data, the original sender handles the response event and extracts the response from the event data.
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 mode 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 returns.
For mode to GUI or GUI to mode 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.
The class responsible to deliver mode to modes events is EventManager. A mode should not call the EventManager directly because that by-passes 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 to queue and service the mode to GUI or GUI to mode 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);
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 will be 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.
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);
The event logging feature is great but it has a major drawback. What if you don’t know the name of the events you need to debug? There is no option to log every event posted until you can decide which events you truly need.
This can be solved by patching the Post methods with the help of Harmony.
The idea is to decorate 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)); |
Start the application and now every posted event appears 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().
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); } } } } |
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 architecture 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.