Unity C# Design Patterns
I love developing games and other applications using Unity and over the years I have learned some nice design patterns. These patterns can make games you develop in Unity easier to maintain and expand. Most often when you see introductory videos or blogs discussing Unity they don’t show use of these more advanced patterns and tend to prefer use of static variables, singletons or well known scene objects. But as your game gets larger, patterns like this can make it more flexible, easier to expand and much easier to maintain.
The ScriptableObject Pattern
While not exactly a pattern the use of the Scriptable Object capability in the Unity class library can allow you to develop much more complex systems and patterns that take advantage of the building file based assets and designers that allow you to build large parts of your game as assets versus if statements or switch statements.
The ScriptableObject
is a different class that you inherit your scripts from in place of MonoBehaviour
. MonoBehaviours are run as components of the objects that are part of your scene. When you create a object in your scene you can add MonoBehaviours to your objects to provide new behaviors that participate in the object lifecycle and run in the update loop. But when you change the class you inherit from to be a ScriptableObject you are making a definition for an asset that will be tracked on disk like other assets. The best way to think about these classes that inherit from ScriptableObject is that they are like a graphics asset (2D texture for example).
To create the ScriptableObject
- Create a new C# Script from Unity.
- Open the Script and change the class to be inherited from to be ScriptableObject and not MonoBehaviour.
- Remove the Start and Update methods generated.
For example, this is a fictional ItemDefinition ScriptableObject script:
using UnityEngine;
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item Definition")]
public class ItemDefinition : ScriptableObject
{
[SerializeField] private string Name;
[SerializeField] private string Description;
[SerializeField] private Sprite Icon;
[SerializeField] private int Cost;
[SerializeField] private int AttackModifier;
[SerializeField] private int ArmorModifier;
[SerializeField] private int ArmorSlot;
[SerializeField] private int MaxStackSize;
}
A couple of things to note about this class. Each of the fields are private and are using the [SerializeField]
attribute on the field. This is so the designer can access the field but user’s outside of your class cannot. If you want, you can also optionally make these public if you want to be able to access the property externally. The other important thing is the CreateAssetMenu
attribute on the class.
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item Definition")]
This attribute instructs Unity to add a new context menu option when you access the Asset -> Create menu or right click within the Project pane in the assets. The file name is the new default name of the asset created and the menu name is the path to the menu you want to provide to create an instance. Once you right click and select this you will get a new asset and Unity will provide you a basic designer.
So now that we have created the Scriptable object what can we do with it? One thing we can do is to make the scriptable objects data driven data sections of other components. For example, if we had a player component we could add an array of ItemDefinition’s to that component and then assign the inventory that the player would start with. That might look like…
using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField] ItemDefinition [] StartingItems;
}
Then we can drag the definitions from the project definition database to our array in the designer.
One question that immediately comes is how does that make my game easier to extend or maintain? Really there are 3 answers…
- Every object in any scene that references the same scriptable object instance (like my
Iron Sword + 1
) will be given the same instance and values. - When that ScriptableObject’s values are changed, everything referencing the ScriptableObject will see the same changes immediately.
- Finally, we can change the ScriptableObject definitions and not need to change code all over our system, in fact we can dynamically load these assets and provide DLC for our game.
The Scene Reference Pattern
One thing that makes Unity games hard to maintain are the need to maintain direct references between components and remember to change those references. One easy pattern you can develop is the SceneReference pattern. This pattern creates a MonoBehaviour, ScriptableObject and an Enum. The concept is that we are using the ScriptableObject as a global shared reference to all object’s in the scene with a collection it holds. Then we add a MonoBehaviour to the object’s in the scene that we want to broadcast their reference and then the enum is used as the reference catalog. The cool part about this is that it is super expandable and that it provides a very solid way to reference things in the scene but never have a direct reference that scene object.
To start we first create a script and change it into an Enum. This enum list all the possible scene references we want to be able to obtain. Remember we don’t have to just have this be visual objects it could also provide references to our game services like our inventory and more.
public enum SceneReferenceType
{
Player,
Camera,
Rigidbody,
Animator,
NavMeshAgent
}
Next we create a ScriptableObject that is a collection of all the currently tracked references in the scene. Unlike the ScriptableObject we demonstrated earlier this one is intended to have only one reference. Note if you don’t like using the ScriptableObject pattern here you can switch this to a singleton that is basically a static global. The ScriptableObject though provides you a way to have multiple scene reference collections at the same time, if ever needed - one example of that could be in a co-op game where you need to have each player instance know their own references and possibly others.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
[CreateAssetMenu(fileName = "Scene References", menuName = "Global/Scene Reference Collection")]
public class SceneReferenceCollection : ScriptableObject
{
private Dictionary<SceneReferenceType, SceneReference> AllSceneReferences =
new Dictionary<SceneReferenceType, SceneReference>();
public void Add(SceneReference r) => AllSceneReferences.Add(r.ReferenceType, r);
public void Remove(SceneReference r) => AllSceneReferences.Remove(r.ReferenceType);
}
Next we make the SceneReference MonoBehaviour. This class is added to object’s in the scene to mark them as an object that could be referenced.
using UnityEngine;
public class SceneReference : MonoBehaviour
{
[SerializeField] private SceneReferenceCollection Collection;
public SceneReferenceType ReferenceType;
public Transform Owner { get; private set; }
public void OnEnable()
{
Owner = this.transform;
Collection.Add(this);
}
public void OnDestroy()
{
Collection.Remove(this);
}
}
Now we generate a single scriptable object of our Scene Reference Collection in the project assets using the scriptable object menu we added. I will call mine Global Scene References
.
Finally, lets update our SceneReferenceCollection to have helper properties for giving consumer’s easy and safe access to our scene references. After a bit of generic programming and lambda syntactical sugar we have a final SceneReferenceCollection
that looks like this.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
[CreateAssetMenu(fileName = "Scene References", menuName = "Global/Scene Reference Collection")]
public class SceneReferenceCollection : ScriptableObject
{
private Dictionary<SceneReferenceType, SceneReference> AllSceneReferences =
new Dictionary<SceneReferenceType, SceneReference>();
public void Add(SceneReference r) => AllSceneReferences.Add(r.ReferenceType, r);
public void Remove(SceneReference r) => AllSceneReferences.Remove(r.ReferenceType);
public Transform Player => Find(SceneReferenceType.Player, r => r.Owner);
public Animator Animator => Find(SceneReferenceType.Animator, r => r.Owner.GetComponent<Animator>());
public Camera Camera => Find(SceneReferenceType.Camera, r => r.Owner.GetComponent<Camera>());
public Rigidbody Rigidbody => Find(SceneReferenceType.Rigidbody, r => r.Owner.GetComponent<Rigidbody>());
public NavMeshAgent NavMeshAgent => Find(SceneReferenceType.NavMeshAgent, r => r.Owner.GetComponent<NavMeshAgent>());
private T Find<T>(SceneReferenceType type, Func<SceneReference, T> accessor)
{
if (AllSceneReferences.ContainsKey(type))
{
var sr = AllSceneReferences[type];
if (sr != null)
{
return accessor(sr);
}
}
return default(T);
}
}
Breaking down the additions to our collection class the Find
method searches the array of AllSceneReferences that is being added to and removed by the MonoBehaviour SceneReference
assigned to various components in our scene. It takes a lambda expression to translate the generic scene reference to the actual component or reference the user is asking for with that type. We also are using the new shortcut property expressions to make the code more compact. Assuming the Find method finds in our collection a reference of the type being requested it passes it to the lambda expression to use that transform as the basis for getting the actual reference requested.
We then add the SceneReference component one or more times to each object which can give us access to the reference. We then drag the scriptable object instance to the collection and select from the dropdown the Reference Type, in this example it was the player reference.
Remember if you don’t like using ScriptableObject’s for the collection you can make the SceneReferenceCollection a static singleton, but the ScriptableObject pattern helps for future things you can’t expect you might need at the cost of choosing one ScriptableObject instance per use.
Then from anywhere in the rest of your code when you need a reference to some object you can add a SceneReferenceCollection property and assign it in the designer. Here is an example component that needed to have a reference to the player.
using UnityEngine;
public class ExampleReferenceUser : MonoBehaviour
{
public SceneReferenceCollection SceneReferences;
void Update()
{
Transform player = SceneReferences.Player;
if (player != null)
{
Debug.Log($"Player at {player.position.x}, {player.position.y}, {player.position.z}.");
}
}
}
The most important part of this code is the line
Transform player = SceneReferences.Player;
This is giving that script a type safe reference to the player’s transform without ever needing a direct reference to that object in the scene. When the number objects and scripts increase dramatically this pattern is super powerful and useful. Remember low coupling between classes is a good design goal.
The ScriptableObject Based Pub-Sub Pattern
The final design pattern for Unity I am going to talk about today is developing an event type pub/sub model using ScriptableObjects. This really improves game code by decoupling event broadcasting and all the things you want to respond. I find it greatly simplifies input mapping and game logic overall. Without patterns like this eventually you may get spaghetti code hooking things to each other and trying to maintain the calling references.
To start we create a new ScriptableObject named GameEvent
:
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Game Event", menuName = "Global/Game Events")]
public class GameEvent : ScriptableObject
{
#if UNITY_EDITOR
[Multiline]
public string EventDescription = "";
#endif
private readonly List<GameEventListener> eventListeners = new List<GameEventListener>();
public void Raise()
{
for (int i = eventListeners.Count - 1; i >= 0; i--)
eventListeners[i].RaiseEvent();
}
public void Raise(object arg)
{
for (int i = eventListeners.Count - 1; i >= 0; i--)
eventListeners[i].RaiseEvent(arg);
}
public void Register(GameEventListener listener)
{
if (!eventListeners.Contains(listener))
eventListeners.Add(listener);
}
public void Unregister(GameEventListener listener)
{
if (eventListeners.Contains(listener))
eventListeners.Remove(listener);
}
}
This object allows us to define a scriptable object instance for our game events. The instance will hold a collection of GameEventListener
objects which are the object adapters that are listening for the event to be raise. To make events to identify what they are all about we added an editor only event description we can fill out. Once you have lots of events it is important to understand which game event happens to cause the event to be raised.
The Raise()
method is overloaded to be able to pass an argument to the listeners. Next we need to create the GameEventListener
class. This is a helper class to allow a MonoBehaviour to listen to a game event that is shared.
public class GameEventListener
{
private GameEvent Event { get; set; }
private System.Action EventHandlerNoArgs = null;
private System.Action<object> EventHandlerWithObjectArgs = null;
public void Register(GameEvent gameEvent, System.Action handler)
{
Register(gameEvent);
this.EventHandlerNoArgs = handler;
}
public void Register(GameEvent gameEvent, System.Action<object> handler)
{
Register(gameEvent);
this.EventHandlerWithObjectArgs = handler;
}
public void RaiseEvent()
{
EventHandlerNoArgs?.Invoke();
}
public void RaiseEvent(object value)
{
EventHandlerWithObjectArgs?.Invoke(value);
}
public void Unregister()
{
Event?.Unregister(this);
}
protected void Register(GameEvent gameEvent)
{
this.Event = gameEvent;
Event?.Unregister(this);
Event?.Register(this);
}
}
All this class is doing is holding a lambda to an Action delegate that is the method we want called when the game event is raised. We overload the Register()
and Raise()
methods also to be able to pass an argument to the listener. The sequence of events are as follows:
- We create a new game event scriptable object instance for your game event.
- We add the game event as a property of both the broadcasting script and any other script that wants to listen and do something when the event occurs.
- We register a GameEventListener in each listener script and hook it to the Game Event and the method to call when the event is raised. This will in turn call the Register on the game event scriptable adding the listener to the collection of listeners to receive the event.
- We broadcast the event with a Raise() from the broadcasting script and it will iterate through the list of listeners and call the delegate method in each listener script in a loop.
- Finally each listening script calls Unregister() when it goes out of scope or is disable to pull its listener out of the game event’s collection of active listeners.
Our Game Event Instance Created
The broadcasting class is assigned the game event asset as a property and when the event happens it is broadcast.
using UnityEngine;
public class BroadcastExample : MonoBehaviour
{
public GameEvent SpellCastEvent;
void Update()
{
if (Input.GetButtonUp("Fire1"))
{
SpellCastEvent.Raise();
}
}
}
Then every listening script will create another game event property and we assign the same game event instance we created and use the GameEventListener
to connect the game event to a method using a lambda.
using UnityEngine;
public class ListenExample : MonoBehaviour
{
public GameEvent SpellCastEvent;
private GameEventListener SpellCastEventListener = new GameEventListener();
void OnEnable()
{
SpellCastEventListener.Register(this.SpellCastEvent, OnSpellCast);
}
void OnDestroy()
{
SpellCastEventListener?.Unregister();
}
private void OnSpellCast()
{
// Do Something when spell cast
Debug.Log("Spell Cast");
}
}
This pattern of creating Game Event instances and using the Game Event Listener with the Game Events allow us to have lots of things respond to a game event and we don’t have to hook and two things to each other or maintain the events. Also, one side effect of this design is that the broadcasting script can be created after the listening script and the system will still continue to work.
Wrapping Up
These Unity game design patterns are super powerful as building blocks to build more complex games that are easier to extend, refactor, maintain and grow as your game ideas grow. The cornerstone of each of these patterns is use of the ScriptableObject pattern for abstractions that have low coupling. This low coupling is the key to why these patterns are so useful. I hope you find good uses for these patterns in your Unity projects.
-Paul