C# event management system with in-house events and event managers. It can be used for any C# projects, but originally aimed for Unity.
CSEMS is currently highly unstable and still in evolution.
This document provides information about CSEMS' core classes and also provides examples based on a hexfall (aka Hexic by Alexey Pajitnov) game (user-defined event managers example).
First, we will talk about the core classes, then give examples about the event managers, continue with in-house events, and conclude by showing how you can add your own events and event managers.
- Information About Base Classes
- Event Managers
- Events
- Event Handlers
- An Example: HexfallGameController
Here, you will find some information about the core classes you should be aware of for constructing your own code.
Each class should be either inherited from LB_BaseGameObject, which is inherited from LB_BaseObject, or at least implement LB_IBaseGameObjectFunctionality interface. Otherwise, they cannot receive essential calls, such as LB_Awake(), LB_Start(), LB_Update(), and so on.
If you are familiar with Unity, these methods have the same purpose with MonoBehaviour.Awake(), MonoBehaviour.Start(), and MonoBehaviour.Update().
Base class for any objects in the project. This class includes some common members and properties, such as id, name, etc.
Interface that provides the esential methods for a game object class, such as update, fixed update, destroy, awake, start, and so on.
Base class for the classes that will take a part in the game related tasks, but will not need to be inherited from MonoBehaviour of Unity, such as game managers, event managers, and so on.
LB_BaseGameObject implements LB_IBaseGameObjectFunctionality interface and provides the common blue print methods, needed members, and common functionality.
CSEMS is designed based on observer design pattern. Event managers are initiated with Subject class and event handlers are initiated with Observer class.
Generic base Subject and Observer classes for an event-driven system.
Subject object maintains a list of its dependents, called observers. The observers register with the subject, so they can get notified when a predefined condition, event, or state change occurs. The subject automatically notifies all observers by calling one of their methods (callbacks).
Any number of observers can observe a subject. So, this is one-to-many relationship.
All event managers (including user-defined ones) are inherited from generic BaseEventManager and automatically register themselves to MainEventManager to receive frame calls (Update(), callOnFrame(), and so on).
MainEventManager is a singleton class and it manages all event managers in the project. Subevent managers operate via MainEventManager.
Here, we will talk about event managers provided by CSEMS. They all define their own related events and inherit themselves from BaseEventManager. Event-handling classes, which are the classes listening a particular event, implement Observer interface and register themselves to related event manager.
ButtonEventManager is a singleton event manager for handling LB_Button events. LB_Button events are in-house events for handling Unity buttons better and easier. We will cover LB_Button events later detailed.
SystemEventManager is a singleton system event manager for handling system related events, such as quitting.
MouseEventManager is a singleton event manager for handling mouse events. This manager works via Unity's Input class. It also provides an in-house mouse hold-and-move event.
TouchEventManager is a singleton event manager for handling touch events. This manager works via Unity's Input class.
HexfallUIEventManager is a singleton event manager for managing Hexfall game UI events.
HexfallEventManager is a singleton event manager for Hexfall related event management.
Here, making all event managers singleton is a design choice and you do not need to define your event managers as singleton.
All events are inherited from LB_Event class and define their own event type and define other specialities if needed. Now, we will talk about system and user-defined events.
LB_Event is the base event class both for system and user-defined events. It defines parent event information (system, touch, mouse, user-defined, and so on), event type (mouse down, touch up, etc.), and an object type member in case you need to hold extra information.
LB_ButtonEvent is an in-house button event for handling click, button down, and button up events. LB_Button script is added to the buttons in the project and related callbacks (click, button down, and button up) are assigned via Unity editor. These callbacks automatically creates related events and register them to ButtonEventManager.
LB_SystemEvent is used for handling system related actions, such as quit request, processing quit request and so on.
LB_MouseEvent is an in-house mouse event used for handling mouse down, mouse up, and mouse hold and move events. It also provides information about mouse x and y positions in pixel coordinates.
LB_TouchEvent is an in-house touch event used for handling touch down, touch up, and touch and move events. It also provides information about touch x and y positions in pixel coordinates.
HexfallUIEvent provides events such as main menu, go game preparation, go in game, and so on. These events are used to change between game menus.
HexfallEvent provides events such as start game, quit game, score, generate bomb, generate hex, and so on. These events are used to handle actions in the game, mostly by various game controllers.
Event handlers are interfaces based on generic LB_Observer interface mentioned in Subject and Observer classes section. Each event handler inherits LB_Observer by defining its event type. Each class aiming to handle one or more events implements HandleEvent(EventType e) method provides by the related event handler.
For example, LB_IButtonHandler is inherited from LB_Observer interface with LB_ButtonEvent event. A class aiming to handle LB_Button events needs to use LB_IButtonHandler interface and to implement HandleEvent(LB_ButtonEvent e) method defined by LB_IButtonHandler interface.
You can find more examples in the list below:
- LB_IButtonEventHandler inherites
LB_Observer
withLB_ButtonEvent
, providesvoid HandleEvent(LB_ButtonEvent e)
method - LB_ISystemEventHandler inherites
LB_Observer
withLB_SystemEvent
, providesvoid HandleEvent(LB_SystemEvent e)
method - LB_IMouseEventHandler inherites
LB_Observer
withLB_MouseEvent
, providesvoid HandleEvent(LB_MouseEvent e)
method - LB_ITouchEventHandler inherites
LB_Observer
withLB_TouchEvent
, providesvoid HandleEvent(LB_TouchEvent e)
method - IHexfallUIEventHandler inherites
LB_Observer
withHexfallUIEvent
, providesvoid HandleEvent(HexfallUIEvent e)
method - IHexfallEventHandler inherites
LB_Observer
withHexfallEvent
, providesvoid HandleEvent(HexfallEvent e)
method
Let us assume we need a game controller that needs to handle mouse and touch events. Here, we will show how to create these events, event managers, and related event handlers.
For constructing such system;
- We need to define the related events based on LB_Event,
- Its event handler based on LB_Observer,
- Its event manager based on BaseEventManager,
- and implement the related HandleEvent(EventType e) methods in the game controller.
Let's assume we want to design a mouse event providing mouse down, mouse up, and mouse hold and move events along mouse x and y positions in pixel coordinates. We can achieve this goal by the following steps:
- Inherit from LB_Event
- Define event types and event type field
- Define other specifications needed
- Define constructors
These are the general steps we need to follow whenever we need to introduce a new user-defined event.
When we follow the general steps, we get a mouse event class as below:
// Step 1: Inherit from LB_Event
public class LB_MouseEvent : LB_Event
{
// Step 2: Define event types
public enum MOUSE_EVENTS : uint
{
MOUSE_DOWN, MOUSE_UP, MOUSE_HOLD_MOVE
}
// Step 2: Define event type field
public MOUSE_EVENTS EventType
{
get { return (MOUSE_EVENTS)EVENT_TYPE; }
set { EVENT_TYPE = (uint)value; }
}
// Step 3: Define other specifications
public float MOUSE_POS_X;
public float MOUSE_POS_Y;
// Step 4: Define constructors
public LB_MouseEvent() : base(LB_EVENTS.MOUSE_EVENT)
{
MOUSE_POS_X = -1;
MOUSE_POS_Y = -1;
}
public LB_MouseEvent(
MOUSE_EVENTS eventType,
float mousePosX, float mousePosY,
string extraInfo = ""
)
: base(LB_EVENTS.MOUSE_EVENT, (uint)eventType, extraInfo)
{
MOUSE_POS_X = mousePosX;
MOUSE_POS_Y = mousePosY;
}
}
We would define touch event as in the same way. You can refer to other event examples provided if you need more detail.
Since, we now have our events, we can define our event handlers. Event handlers are interfaces based on generic LB_Observer interface. Each event handler defines itself by an associated event and provides an event handler method via LB_Observer interface.
Let's consider mouse and touch event handlers based on what we've covered so far:
// Interface for mouse events
public interface LB_IMouseEventHandler : LB_Observer<LB_MouseEvent>
{ }
// Interface for touch events
public interface LB_ITouchEventHandler : LB_Observer<LB_TouchEvent>
{ }
C'est tout! Defining event handlers in CSEMS is very straightforward.
Event managers are basically the subjects in observer pattern. CSEMS encapsulates the subject class via BaseEventManager. You can still create your own subject classes based on LB_Subject if you need for different purposes, such as a messaging system, but for creating an event manager that will work in CSEMS, you need to inherit your user-defined event manager from BaseEventManager.
Generic BaseEventManager manager requires two generic types; an EventType based on LB_Event and an EventHandlerType based on LB_Observer. Thus, when we create a mouse event manager MouseEventManager
, we will inherit it from BaseEventManager
as public class MouseEventManager : BaseEventManager<LB_MouseEvent, LB_IMouseEventHadler>
.
Let's consider an easier example this time since MouseEventManager is a bit complicated. Let's assume we have our HexfallEvent
and IHexfallEventHandler
, and we want to write a HexfallEventManager
. We could write this class as below:
// Singleton event manager for Hexfall related event management
public class HexfallEventManager : BaseEventManager<HexfallEvent, IHexfallEventHandler>
{
// Thread-safe without using locks, not quite lazy
private static readonly HexfallEventManager instance = new HexfallEventManager();
static HexfallEventManager() { }
private HexfallEventManager() { }
public static HexfallEventManager Instance
{
get
{
return instance;
}
}
}
The singleton is a design choice and you do not need to define your event manager as singleton. Also, if you do not need some special requirements, i.e. MouseEventManager, you also do not need to override any methods. BaseEventManager provides you all functionality you need for an event-driven system, such as pushing a new event, notifying listeners, registering to MainEventManager, and so on.
Let's go back to our mouse and input events and their event handlers along their event managers. In this part, we will focus on implementing a game controller that receives mouse and touch events.
There are two steps to achieve this functionality:
- Implementing related event handlers
- Registering to the related event managers
For being able to implement the related event handlers, we need to inherit from the related event handlers. In our case, these are LB_IMouseEventHandler
and LB_ITouchEventHandler
. So, we will define our HexfallGameController
class as public class HexfallGameController : LB_BaseGameObject, LB_IMouseEventHandler, LB_ITouchEventHandler
.
After having the event handlers, implementing them is straightforward and not any different than implementing any interface method. Let's assume, we have the following actions depending on the events we receiver:
- MouseDown or TouchDown: Take input
- MouseUp or TouchUp: Handle hexagon selection
- MouseHoldAndMove or TouchMove: Rotate selected hexagon
so, we would implement the event handlers as below:
public void HandleEvent(LB_MouseEvent e)
{
if (e.EventType == LB_MouseEvent.MOUSE_EVENTS.MOUSE_DOWN)
{
game.TakeInput();
}
else if (e.EventType == LB_MouseEvent.MOUSE_EVENTS.MOUSE_UP)
{
game.HandleHexagonSelection();
}
else if (e.EventType == LB_MouseEvent.MOUSE_EVENTS.MOUSE_HOLD_MOVE)
{
game.HandleHexagonRotation();
}
}
public void HandleEvent(LB_TouchEvent e)
{
if (e.EventType == LB_TouchEvent.LB_TOUCH_EVENTS.TOUCH_DOWN)
{
game.TakeInput();
}
else if (e.EventType == LB_TouchEvent.LB_TOUCH_EVENTS.TOUCH_UP)
{
game.HandleHexagonSelection();
}
else if (e.EventType == LB_TouchEvent.LB_TOUCH_EVENTS.TOUCH_MOVE)
{
game.HandleHexagonRotation();
}
}
Now, we are at the final step to integrate CSEMS fully. It is time to register our class, HexfallGameController
, to the related event managers. This step is inevitable and if you do not register your class to the related event managers, you cannot receive any event calls. It is like expecting phone calls without having a phone number.
It is better to handle this registeration after being sure the core system is ready. Since our event managers are singleton, we can handle this in LB_Awake() method having the same order and logic as Unity's Monobehaviour.Awake(). If the current event managers were not singletons than it would be better to handle the registeration process in LB_Start() the equivalent of Monobehaviour.Star().
Here is the full code of HexfallGameController
:
public class HexfallGameController : LB_BaseGameObject, LB_IMouseEventHandler, LB_ITouchEventHandler
{
HexfallGame game;
public override void LB_Awake()
{
#if (UNITY_EDITOR || UNITY_STANDALONE)
MouseEventManager.Instance.Attach(this.SystemId, this);
#elif (UNITY_IOS || UNITY_ANDROID)
TouchEventManager.Instance.Attach(this.SystemId, this);
#endif
game = new HexfallGame();
game.LB_Awake();
}
public override void LB_Start()
{
game.LB_Start();
}
public override void LB_OnDestroy()
{
game.LB_OnDestroy();
}
public void HandleEvent(LB_MouseEvent e)
{
if (e.EventType == LB_MouseEvent.MOUSE_EVENTS.MOUSE_DOWN)
{
game.TakeInput();
}
else if (e.EventType == LB_MouseEvent.MOUSE_EVENTS.MOUSE_UP)
{
game.HandleHexagonSelection();
}
else if (e.EventType == LB_MouseEvent.MOUSE_EVENTS.MOUSE_HOLD_MOVE)
{
game.HandleHexagonRotation();
}
}
public void HandleEvent(LB_TouchEvent e)
{
if (e.EventType == LB_TouchEvent.LB_TOUCH_EVENTS.TOUCH_DOWN)
{
game.TakeInput();
}
else if (e.EventType == LB_TouchEvent.LB_TOUCH_EVENTS.TOUCH_UP)
{
game.HandleHexagonSelection();
}
else if (e.EventType == LB_TouchEvent.LB_TOUCH_EVENTS.TOUCH_MOVE)
{
game.HandleHexagonRotation();
}
}
}