Your First Game in Unity: Ghost Evader: Part 08: Game Manager
Hey everyone, welcome to the next step in the setup for our game, Ghost Evader. This is a simple 2D top-down game where the player must collect coins and stay alive as long as possible. In this series we are going to go step by step and explain everything as clearly as we can.
In this post we are going to set up the game manager.
Game Manager Design
Let’s start by thinking about what the game manager needs to do.
- Spawn the player character
- Respawn the Player on death (Player has three lives)
- Keep the game score
- Handle pause and resume functionality
- Handle activation of the spawn managers (Enemy Spawner, Pickup Spawner)
- Handle game over conditions, such as displaying the final score and allowing the player to restart the game
In a more complex game, I would probably create a separate level manager and a scoring manager.
Given the simplicity of this game, we will keep the game manager responsible for all of these tasks. However, in a more complex game, it would be better to separate these responsibilities into different managers to keep the code organized and maintainable.
We will be covering the Game Manager and the UI Manager we intend to create over several posts.
Last, we will also create an Audio Manager to handle sound effects and music playback.
Starting the Game
I like to think of the game manager as the central hub of the game. It is responsible for coordinating all the different parts of the game and ensuring that everything runs smoothly.
This is why we are going to spawn the player, rather than the player already existing in the scene.
So I am going to make the player a prefab:
[Header("Player")]
[SerializeField] private Player playerPrefab;
We already covered prefabs in the enemy spawner setup. The new attribute is the [Header] attribute, which allows us to add a header to the inspector for the player prefab. This makes it easier to identify and configure the player prefab in the Unity editor.
I am explicitly getting a reference to the Player Script in case I want to call methods on it later. In addition, this prevents me from attaching a random game object to the Game Manager.
Player - Game Manager Communication
Since the GameManger is responsible for spawning the player and spawning a new player when the player dies, we need a way to know when the player dies.
There are a number of ways to do this. Since this is a beginner series, we are going to keep this as easy to understand as possible.
By design, we will have the player call a function on the GameManager when the player dies. This removes the need for the GameManager to micromanage the player.
This means the player will need a reference to the GameManager so it can call the function.
You might think, based on the series so far, to add this to the Player class:
[Header("GameManager")]
[SerializeField] private GameManager gameManager;
The problem is, the Player class will be a prefab (or saved game object). A saved prefab can only reference objects on its own game object or its children. It can’t reference other prefabs or objects in the scene.
Therefore, we will have the Player class find the GameManager in the scene when it is spawned. This way, the Player script can call the function on the GameManager when the player dies.
Advanced alternatives
This is not the only way to do this (finding the GameManager through code). We could have the GameManager spawn the player and pass a reference to itself to the player. This would allow the Player to call the function on the GameManager when the player dies.
However, this could add more complexity than needed, which is not the goal of this series.
Alternatively, we could use a singleton pattern to have the GameManager be a singleton.
The singleton pattern is a technique for ensuring a class has only one instance and provides a global point of access to it. We won’t use this approach in this series.
This would allow the Player to call the function on the GameManager when the player dies. However, this would require the GameManager to be a singleton, a bit more complex than needed, which is not the goal of this series.
Yet another way would be to use C# events. Since the GameManager spawns the player, we could add an event on the Player class the GameManager could listen to. This would be my preferred methodology most of the time.
We’ll explore alternatives at the end of the series. Let’s keep the tutorial as simple as possible for now.
Score Setup
Since the GameManager is responsible for the score and the number of lives, we’ll set those up as C# properties.
public int CurrentScore { get; private set; }
public int NumberOfLives { get; private set; }
This is very close to the setup we did for the MoveInput variable in the GameInput class.
This allows other systems in the game to access the score and the number of lives.
Game Loop
We’re making the GameManager the core component of the game. In our design we give the player 3 lives to start.
So let’s consider each life a round. This is how the loop works
Start Unity
Start the Game
Start Round One
Player Dies
End Round One
Inter Round
Start Next Round
We are going to setup functions to replicate this paradigm. Let’s start with the player spawn.
// Unity Events
private void Start()
{
StartGame();
}
// Internal Functions
private void StartGame()
{
// Add any startup code
StartRound();
}
We need to instantiate the player. We will call the same function we did to spawn the Enemy but this time we will save the reference.
private void StartRound()
{
_playerScript = Instantiate(playerPrefab, Vector2.zero, Quaternion.identity);
}
Instead of picking one of the random spawn points, we will intentionally spawn the player at the center of the screen.
In Unity the center of the screen is (0, 0). Vector2.zero is a shortcut for (0, 0). This will place the player in the center of the Unity world.
Note, there is nothing forcing you to spawn the player at the center of the screen. You can spawn the player anywhere you want.
Refactoring the MoveInput
As you may recall, we set the MoveInput on a separate game object. When making the player a prefab, however, we can’t use the [SerializeField] attribute to link to an object in the scene.
So, when saving the Prefab, the reference to the MoveInput will be lost.
If you try running the game now, you will get an error. This is because we are trying to access a component that does not exist.
To address this, we will need to refactor the gameInputScript variable. The term ‘refactor’ means to make changes to the code without changing its behavior.
So I am going to remove the [SerializeField] attribute but keep the gameInputScript variable. Additionally, I am going to rename the variable to prefix the variable with an underscore to _gameInputScript;.
Renaming it to have the underscore prefix follows my convention of using underscores for private variables that do not have the [SerializeField] attribute. You need to be careful to change every instance of the variable name. Most IDEs provide a refactor tool that can help with this.
Here are the changes to the Player class
private GameManager _gameManagerScript;
private GameInput _gameInputScript;
private void Start()
{
_gameInputScript = FindAnyObjectByType<GameInput>();
_gameManagerScript = FindAnyObjectByType<GameManager>();
if (_gameInputScript == null)
{
Debug.LogError("Game Input is null!");
}
if (_gameManagerScript == null)
{
Debug.LogError("Game Manager is null!");
}
}
We are also adding the GameManager class reference that we will need when the player dies.
Initial Script
This is our initial script:
using UnityEngine;
public class GameManager : MonoBehaviour
{
// Public Properties
public int CurrentScore { get; private set; }
public int NumberOfLives { get; private set; }
// Editor Fields
[Header("Player")]
[SerializeField] private Player playerPrefab;
// Internal Fields
private Player _playerScript;
// Unity Events
private void Start()
{
StartGame();
}
// Internal Functions
private void StartGame()
{
// Add any startup code
StartRound();
}
private void StartRound()
{
_playerScript = Instantiate(playerPrefab, Vector2.zero, Quaternion.identity);
}
}
Conclusion
We have created the GameManager, we refactored the Player to be a Prefab, and we are now spawning the Player from the GameManager class.
Our game now runs exactly the same as before, except we are now using the GameManager class, and we can control the score and the number of lives.
We also have a game loop that can support multiple rounds.
Next, we will add collision detection, so our primary gameplay mechanic is set up.