Next up in programming patterns for games is the Observer pattern.
The Problem
In games, objects need to "talk" to other objects all the time. Most of the time, when a player character takes damage, multiple objects must react; the health bar must register the damage taken and decrease its value accordingly, which must cause the visual component to change; Enemies may need to know the player took damage- maybe your enemy's strength increases when it lands an attack, or there is a cooldown period where the enemy cannot attack you again. If you've ever tried to create a game, you've probably come across this type of problem before- it's an essential part of creating a game if you want your objects to interact in some way.
The problem is, what is the best way to create that communication between scripts? You could have a variable in your players' script that references an enemy script, and directly call a method on the enemy script when the player is hit by or hits the enemy, like this:
class Player
{
//reference the enemy script
Enemy enemyInstance = GameObject.FindObjectOfType<Enemy>();
//player attacks enemy- you can imagine the enemy script
//has a method called TakeDamage() that takes in an Int
//argument and subtracts that value from its health value
void Attack()
{
enemyInstance.TakeDamage(5);
}
}
If your game only ever has one instance of an enemy that stays the same the whole game, that solution could work (although it still tightly couples our code and is not ideal). However, most of the time, we have many instances of an enemy. It would be a bit ridiculous to create a reference in our player script to every single enemy instance (there might be thousands!) And how would we account for those instances that are instantiated at runtime?
Not only that, but as I mentioned, this kind of solution tightly couples our code- the player script has a direct reference to an enemy object. This is bad for maintainability, testing, code reusability, and scalability. If you're not sure why coupling would be bad in these areas, check out this source that goes into more detail on code coupling.
It also means your game would be consuming more memory than necessary- if an enemy game object is destroyed, normally the garbage collector would dispose of it, but since the player script references the enemy script, the garbage collector cannot destroy it when it should otherwise be able to.
Imagine if, instead of traffic lights or stop signs, each car was responsible for coordinating directly with the cars around it. It would be chaos! Code coupling and direct references to communicate between objects has a similar effect. As you can see, this isn't really a good solution for many reasons.
The observer pattern allows for our scripts to communicate, but decouples our code so our game objects don't have to be directly tied to the objects they are interacting with.
The Observer Pattern
With the observer pattern, an object, known as the subject, keeps a list of dependents, known as the observers. When something happens in the game that you need other objects to know about, your subject object can invoke a function that the observers all subscribe to. You may be familiar with events systems, which are themselves using the observer pattern. In Unity, there are a few ways to create an event and its listeners. Let's go over one of them by looking at a ScoreManager script with an event to update other scripts when there is a change in score.
public class ScoreManager : MonoBehaviour
{
private int currentScore = 0;
// Declare an event to notify when the score is updated.
public delegate void ScoreUpdatedEventHandler(int newScore);
public event ScoreUpdatedEventHandler ScoreUpdated;
private void Start()
{
// Initialize the score (for demonstration purposes).
currentScore = 0;
}
// Function to update the player's score.
public void UpdateScore(int scoreChange)
{
currentScore += scoreChange;
// Notify subscribers that the score has been updated.
ScoreUpdated?.Invoke(currentScore);
}
}
In this script, we create a delegate function that returns nothing and takes in an argument of the new score value. Delegate functions allow us to define a functions basic signature, or the "shape" or kind of function it is, to allow us to pass a function as an argument without actually calling the function. We then create an event of the type of our delegate and call it ScoreUpdated
. Now, in any scripts that need to be updated when the score changes, they simply have to subscribe to that event, and the line ScoreUpdate?.Invoke(currentScore)
will invoke the event and all functions/methods that are subscribed to it, regardless of the script they're in.
This is how the observer scripts would subscribe to the event.
public class ScoreDisplay : MonoBehaviour
{
public Text scoreText;
private void Start()
{
// Find the ScoreManager in the scene.
ScoreManager scoreManager = FindObjectOfType<ScoreManager>();
// Subscribe to the ScoreUpdated event.
scoreManager.ScoreUpdated += UpdateScoreText;
}
// This method updates the UI text with the current score.
private void UpdateScoreText(int newScore)
{
scoreText.text = "Score: " + newScore.ToString();
}
}
As you can see, in the Start
method, we grab a reference to the script containing the event definition (another way to reference the script is to use a Singleton, which you can read more about here). Next, we create a method in the observer class to handle the logic of updating score for that particular object, and subscribe that method to the event in the ScoreManager script.
If you're coming from a web development background, imagine you're creating a <button>
element. In order to have that button respond to a click, you have to create a function that handles what happens when the button is clicked, and you have to subscribe that function to the onClick
event. Then, when the button is clicked, the onClick
event is invoked, and any functions that are subscribed to the event are called. All these examples are using the Observer pattern!
In Unity, while the above example works, what you will most often see is an observer subscribe to an event in Unity's OnEnable
method rather than Start
, and then unsubscribe from the same event in the OnDisable
method. This way, the script is only subscribed to any events while the game object it's attached to is active, therefore it does not retain any connection with the events when the object is disabled or destroyed, again helping the garbage collector do it's job.
To summarize, the Observer pattern offers an elegant solution to a common challenge in game development— efficient communication between objects. We've explored how tightly coupling code can lead to issues in maintainability and scalability, and we've introduced the Observer pattern as a decoupled alternative.
By using events and subscribers, as demonstrated in our Unity examples, developers can create flexible and extensible systems that allow game objects to communicate without the need for direct references. This not only enhances code organization but also improves memory management.
As you delve deeper into game development, or even in different domains of software development, mastering common programming patterns like the Observer pattern becomes increasingly valuable.
For more information, this site has a great overview of the Observer pattern if you'd like to learn more.
Top comments (0)