Games Architecture: Event Systems


This week in the Games Architecture Seminar I've been running, we went over the event management pattern. Using events is a key decoupling pattern - it's an important way to re-think how your game operates at a low level, one that makes your code cleaner, clearer, and easier to update and use.


When would I use this pattern? You have a game with a lot of objects, each of which needs to know when things change in your game. Multiple different objects and systems care deeply when a point is scored, or the player goes into stealth mode, or the level changes. Maybe every time a point is scored, your enemies move faster, your achievement system logs the total points scored, the high-score system checks to see if you've gotten on the leaderboard, the audio system plays a sound effect, the background pulses. Maybe every time you go into stealth mode, the UI elements shift, the player gets brand new abilities, the enemies behave differently, the control scheme changes, so on and so forth.


Maybe you're making a soccer game! You have a set of things that happen every time a goal is scored:

So somewhere in the Ball class, you have code that looks something like this:


public void onTriggerEnter(Collider other) {

if (other.tag == “Goal”) {

Achievements.GoalScored();

Achievements.TeamScored(scoringTeam);

ScoreManager.UpdateScore(scoringTeam, 1);

Audio.StartSoundEffect(“cheer1.wav”);

Players.ResetPositions();

… etc.

}

}


Why is this bad? Well, for starters, your Ball class is now connected to every other system in your game. It has to keep a list of every single system that cares about goals scored, and know what happens in each system that cares about goals scored. It doesn't really make sense for the Ball to keep track of all that information, right? In a game of soccer, the ball doesn't enter the goal, then shout "Okay everyone - get back to your starti ng positions!" This is also really difficult to read - nothing in that list of functions makes it clear that a single, simple event has happened. Plus, it's difficult to update and make changes - nine months from now when you're updating this game, will you remember that the ball was the object calling the function that tracks scoring? As a result, your code becomes difficult to collaborate on, as everyone working on the code has to constantly be changing every script you have. And the fact that everything's interconnected and calling functions on every other makes debugging a nightmare, where the function throwing an exception may be five or six layers away from the actual issue.


What is programming with events, then? Imagine if there was a secondary system, that only existed to update other objects in your game when an important event has taken place. Your achievement system, your score controller, your audio system - they all register for events that they care about, and when those events happen, their internal functions get called. That, in a nutshell, is an Event Manager!

Now, our Ball class doesn't need to do anything other than update the Event Manager, telling it went in a goal.


public void onTriggerEnter(Collider other) {

if (other.tag == “Goal”) {

EventManager.GoalScored(other);

}

}


And each of the classes that care about a goal being scored can control what they want to happen on a goal being scored.


// Audio System

public void OnGoalScored(teamName) {

PlaySoundEffect("cheer");

}


// Score System

public void OnGoalScored(teamName) {

UpdateScore(teamName);

}


// Player

public void OnGoalScored(teamName) {

if (teamName == myTeam)

Celebrate();

ResetPosition();

}


Wait, why is this better? Well, this code is extremely modular - everything controls for itself what it wants to happen when an event is called. If different parts of the game cause something to respond differently - like the Level Manager reacts differently to a goal being scored in the normal game than in overtime, you can control that directly in the Level Manager script. Another bonus is that code is decoupled - the ball doesn't have to keep a list of what cares about goals happening, and that makes your code easier to read, and easier to understand.

Is this only better? Are there no downsides? Well, there are a few - debugging can be more difficult, as there's no centralized list of everything that happens in an event. Also, if you aren't careful about making sure you track when things are active and inactive, you can end up with bugs or crashes. The biggest downside is that this pattern is extremely powerful, so it can be easy to overuse this pattern.


The trick to not overusing the pattern, is to make sure that multiple systems care about an event before codifying. Here are a few examples of good events to include in your game, but it will be very specific to the kind of game you're making:

  • Player Died

  • Goal Scored

  • Item picked up

  • Enemy Spawned

  • Game Over

  • Level Over

  • Basically, anything multiple systems would care about.

That's all very well and good, but it seems very abstract. Good point! Here's some example code for Unity that has three examples of Events, but the best is the third one; first, there's an Event Manager class, which defines what events are in your game:


public class EventManager

{

public delegate void GameAction();

public delegate void GameStringAction(string s);

public delegate void GameIntegerAction(int i);

public static event GameStringAction GoalScored, GameOver;

public static event GameIntegerAction Foul;

public static event GameAction Reset;

public static void GoalScoredEvent(string s)

{

if (GoalScored != null)

GoalScored(s);

}

public static void GameOverEvent(string s)

{

if (GameOver != null)

GameOver(s);

}

public static void ResetEvent()

{

if (Reset != null)

Reset();

}

public static void FoulEvent(int i)

{

if (Foul != null)

Foul(i);

}

}


Now, as an example, let's look at the Score Controller - first, you register internal functions with different events:


private void OnEnable()

{

EventManager.Reset += ResetScore;

EventManager.GoalScored += IncreaseScore;

EventManager.GameOver += GameOverCall;

}


private void OnDisable()

{

EventManager.Reset -= ResetScore;

EventManager.GoalScored -= IncreaseScore;

EventManager.GameOver -= GameOverCall;

}


Later on it defines the functions it wants to have happen on different events:


void GameOverCall(string s)

{

// handle game over

}

void ResetScore()

{

// reset the score

}


void IncreaseScore(string team)

{

// increase the score

}


And fires an event in specific situations:


void Update ()

{

if (blueScore > 5)

{

EventManager.GameOverEvent("Blue");

}

else if (yellowScore > 5)

{

EventManager.GameOverEvent("Yellow");

}

}


That's it! Explore the code and project, and reach out if you have any questions!

0 views

© 2019 by Jack Schlesinger