Singletons, PubSub, and Events — Data Flow Paradigms for Game Dev

Zachary Buffone
6 min readMar 9, 2022

--

A concise guide on three popular models for managing data in your games.

An ongoing and divisive discussion in OOP is how to manage the flow of data through a program. How you structure the flow of data through your game or program is important as, if not managed properly, the diagram of which objects talk to which and in what way will grow to be unmanageable (I.E. like a popular pasta dinner.) This little reference will go into three models that can help you control data traffic: singletons, PubSub, and events/callbacks.

What we are trying to avoid (Credit)

Singletons

In your days of Unity, you might have saw or written code that looks something like this:

GameObject.Find("GameManager").GetComponent<GameManager>().StartGame();var score = GameObject.Find("GameManager").GetComponent<GameManager>().Score

The problem with the Find() method is — it is slooow. Find() must look through every GameObject in your scene and compare its name to the string you provided. In worst-case scenario, it will perform at O(n), where n is the number of GameObjects in your scene. As your scene becomes more complex, it will take longer to execute.

Also, and this may be a personal opinion, you should avoid using names that you can be changed in the editor in your code. The name can change in the editor and that will break your code. You may also forget the name you chose for a GameObject and have to go back to the editor to find it (which is annoying).

To combat this, we can use the singleton model for MonoBehaviours that will only ever be in a scene once.

A singleton is a class that can only be instanced once. In a MonoBehaviour context, it is a component that will only ever be instanced once in a scene. The blueprint for creating a MonoBehaviour singleton is shown below;

public static GameManager Instance { private set; get; } = null;...private void Awake() {
if(Instance != null)
throw new System.Exception("More than one instance");
Instance = this;
}
private void OnDestroy() {
Instance = null;
}

Not only does this set up a public static variable so other objects can reference it through the classes namespace, but it also checks if there has been a singleton object instanced already, and error if so. Two instances of the same singleton can break your game heavily, so be sure to check for that programmatically.

To access the singleton, you can run this anywhere.

GameManager.Instance.StartGame();
var score = GameManager.Instance.Score;

Nice and simple, right? Some things to consider.

  • It is important to set the Instance property to null when the GameObject or MonoBehaviour is destroyed, as static members persist through scene loads. The MonoBehaviour hook OnDestroy() is suited will for this.
  • You might be tempted to write this in OnEnable() and OnDisable() and, while you can do this, I would suggest against it as there are situations that you might want to access a disabled component.
  • Like all scripting for Unity, you should only access Instance from the main thread, as accessing it from another thread can cause collisions. For example, don't access GameObject.Instance from a Timer callback. You can check for this, but I have found it's not necessary.

PubSub

PubSub acts as a central hub for the flow of data. Objects can publish messages to a specific key, and it will send the message to all subscribed to that specific key. I find this especially useful for messages that have a large scope to them and when there are many objects needing to receive it, such as the death of a player.

The PubSub class can be written as static so it can be easily accessed, or written as a singleton MonoBehaviour. I prefer the static class approach as it doesn’t clutter your scene, and doesn’t require editor setup.

The three behaviours needed for PubSub are.

  • Allow objects to subscribe to keys.
  • Allow objects to unsubscribe from keys.
  • Allow objects to publish to keys.

If you are using a static class to implement it, you will also want the ability to clear and reset when a new scene is loaded, as static members persist through scene loads. SceneManager.sceneUnloaded is helpful for this.

An implementation meant for Unity can be found here. Feel free to use this in your code.

https://gist.github.com/ZacharyBuffone/df9d2fb0ab1a2032c9b205aa31eda10c

Note that the PubSubID class might be overkill, but I like how it doesn’t allow the subscribing objects to modify the value of the ID (as opposed to a uint).

Events and Callbacks

Callbacks are a popular way of messaging in web development. A callback is a method that is given to another object, which then can be invoked by that object when an event occurs. This can be especially useful in UI development, where you want code to execute on a button press. A popular way to achieve this in C# is using delegates and events.

A delegate is a custom data type that defines a method signature (which is what a method accepts as arguments and returns, i.e. void SomeMethod(int i) ). Using a delegate, you can declare an event , which stores methods to be later invoked. A simple example of delegate/event is shown below:

public class ClassA {
public delegate OnEventHandler(int amount);
public event OnEventHandler OnEvent;
public void Update() {
OnEvent?.Invoke(1);
}
}

To subscribe to an event, you can pass a method to the event using the += operator. You may pass lambda functions and methods (and System.Action<>/System.Func<> if you wanted to).

objectA.OnEvent += (amount) => {
//handle event
}
ORobjectA.OnEvent += MethodWithSameSignatureAsDelegate;

To unsubscribe from an event, use the -= operator. Note that you cant unsubscribe a lambda function, so be sure you are ok with it sticking around until the object deletes. If you want to be able to unsubscribe the function, create a method and pass it instead.

object.OnEvent -= MethodWithSameSignatureAsDelegate;

Unity also has a great class called UnityEvent which are like c# events, but are serializable and can be hooked into the editor. They are great for level events such as a button being pressed or a trigger being triggered. In fact, any game would probably get a lot of use out of a customTriggerEvent component that pipes trigger data to a serialized object defined in the editor.

Another option for callbacks is to use the System.Action<> class. System.Action<T> will store a method with a parameter of type T . This is useful if you only need one method to callback to. However, you may only store one method.

public class ClassA {
private System.Action<int> callback = default;
public void Update() {
callback?.Invoke(1);
}
public void SetCallback(System.Action<int> cb) {
callback = cb;
}
}

In this scenario, you can pass a method or lambda function to objectA.SetCallback() to subscribe, and pass null to unsubscribe.

Things to Avoid: Bidirectional references

Say you have two classes, House and Road. It might seem reasonable that House objects would want to reference the Road object they are on, and the road Object might want to reference the House objects that exist on it. So you might have something like this…

public class House {
private Road road;
...
}
public class Road {
private House[] houses;
...
}

…and the dependency chart would look like…

House -> Road
Road <- House

This is what is called circular or bidirectional referencing. The problem with this is a deletion of either object will require an update to the reference in the other. Without updating for deletion, you will get a MissingReferenceException and, most likely, undefined behaviour. While it’s possible to do the deletion and updating without errors, doing so will create unnecessary complexity to your code.

To solve bidirectional referencing, see if you really need both objects to carry a reference to the other. A good way to think of referencing is to carry references from largest scope to smallest scope. Your dependency chart for a city building game might look like…

City -> Neighborhood -> Road -> House -> Resident

…which is unidirectional and can update linearly. You can also add managers for popular objects like Road and House that can manage operations like FindRoadForHouse(House h), if you need to also go backwards (these can be singletons also 👍).

Conclusion

Managing the flow of data in OOP is very hard. If not done correctly, your codebase can balloon to an unmanageable mess of multidirectional dependencies, which makes bugs likely and implementing new features unfeasible. By using some of these models, you can hope to avoid this.

--

--

Zachary Buffone

Indie game dev from Boston, MA. Email me at zacharybuffone@gmail.com. Follow me @ZacharyBuffone.