The Basics Of Event Handling


As I mentioned in my first post about building classes, I try to enforce a software design where classes are arranged in a pyramid-like hierarchy, with methods only trickling downwards and data only trickling upwards. This approach may seem a bit restrictive initially, as you often encounter situations where lower classes need to inform higher classes that something has happened. Since lower classes cannot directly call higher classes under any circumstances, this is often addressed in games by simply polling the lower classes for changes. A classic game loop would look something like this:


while (true)

{

  BeginFrame();

  GetUserInputs();


  GetGameStates();

  CalculateNewGameStates();


  RenderGame();

  EndFrame();

}


This approach often relies heavily on state machines, a great programming paradigm I certainly will cover in a future post. It also makes adhering to the Lego Principle easy. The video editor I showcased in the About post was developed using this method. However, there are many cases where polling for states, replies, or changes simply isn’t effective, and in those instances, you would use event handlers.

Event handling comes with many names and in many variations, such as:

  • Callbacks
  • Interrupts
  • Observers
  • Subscription

But they all boil down to the same concept: setting up the lower class so that when something specific happens, it informs the higher class. I will use the term "event handler" here to keep things consistent. As illustrated in the diagram below, the key takeaway is that by using event handlers, you can keep the RSMouse class unaware of the RSGame class, ensuring that no changes to RSGame could ever break RSMouse.

The event handler setup has three main components:


Handler: The definition of the handler to be called. 

In type-safe languages, these are often referred to as delegates or callbacks. In non-type-safe languages, they are often known as function pointers. They basically define the method you have to implement in RSGame for the event to work.


OnClick: The event itself. 

This holds an instance of the Handler, defined in RSMouseEvent. It is then the responsibility of RSMouse to call this handler when the actual event occurs.


OnClickHandler: The implementation of the method to be called. 

This is the code in RSGame that will be executed when the event occurs.


In practice, it would look something like this:


// RSMouseEvent

// The delegate declaration

public delegate void Handler(data…);


// RSMouse 

// The event property

public event RSMouseEvent.Handler OnClick { get; }


// somewhere in the RSMouse code

if (_buttonWasClicked == true)

{

  // execute the handler

  OnClick(data…);

}


// RSGame

// Set up the event

_mouse.OnClick += OnClickHandler;


// event implementation

private void OnClickHandler(data…)

{

  Debug.WriteLine(“Mouse was clicked”);

}


The interface for the handlers often follows the same set of rules:


public delegate void Handler(object sender, EventArgs argument);


Since it is RSMouse’s responsibility to execute the handler, it sets sender to point to itself and fills argument with whatever relevant information there might be. Sender is often used, so that the receiver of the event (in this case RSGame), knows where the event came from. In theory you can use whatever parameters in the event handler that you find useful, but try to keep it simple and consistent. Microsoft, for some reason, thought that creating a petrillion of EventArgs variations would be a stellar idea, which it really is not. Obviously, I created my own RSEvent wrapper to address that and many other minor quirks that come with using the built-in event handling. 

Also, the code examples above are not crash-safe. Attempting to call OnClick without setting it first will result in the gamer having a very bad day, so my wrapper also addresses that.


The interface looks like this:


public class RSEvent

{

  // Constructors

  public static RSEvent Create() …


  // Properties

  public delegate void Handler(object sender …) …

  public bool Empty …


  // Methods

  public void AddHandler(Handler handler) …

  public void RemoveHandler(Handler handler) …

  public void ClearHandlerList() …

  public bool ExecuteHandler(object sender …) …

}


Note: I am in the process of setting up a Git repository where all the source code will be available, along with examples and other resources. Hopefully, this will be available by the end of February (2024 that is).


Event-driven programming


At the opposite end of the game loop approach lies fully event-driven programming. Building the UI of, for example, a Windows application is a good illustration, where much functionality is based on events generated by the user. Even if the video editor I mentioned is fundamentally based on a game loop, it still incorporates a lot of event-driven programming. This is particularly true in cases where fetching or rendering video takes time. In those instances, blank or low-resolution placeholders are used until an event signals that the final media is ready. Also, any programming related to interwebs functionality should be event-driven to prevent the application from halting and timing out when Google can't be bothered to respond.


Takeaway


While event handling might initially seem a bit cumbersome, it offers some really important advantages. Apart from being able to notify other parts of the application about rare or unpredictably timed events (optionally never), it is also the only way for classes placed lower in the hierarchy to access classes above. This ensures that editing higher-level classes can never break lower-level functionality.


/Lars



Comments