In this chapter, we are going to continue structuring our game by applying a Singleton design pattern to our GameManager script. This will allow our game to move on to another scene while keeping the script managers functioning and preventing them from being wiped (thereby preserving our data). We will then make a start on other details of our script and observe how information (such as the player's lives) travels through the game's frameworks. If and when the player dies, a life is deducted. If and when the player loses all of their lives, the game over scene will be triggered.
We will be extending our original code and introducing enemy points so that when we hit our enemies with bullets, the enemy will disappear as usual, but will also generate points. This scoring mechanism will be handled by a new score manager that we will be creating.
We'll also be adding sound to the player's bullets, which is a straightforward task. This will introduce us to extending and tweaking our audio sources, which we'll proceed with in a later chapter.
Finally, we will be quizzing ourselves with a couple of questions that suit the theme of this book, preparing you for the exam. The questions will cover what we have already learned, and if you have been following along with this book, you'll have a strong chance of passing.
By the end of this chapter, we will have extended our game's framework, added more features to our game, and tested our knowledge with some Unity exam questions.
In this chapter, we will be covering the following topics:
The next section will introduce the core exam skills that are covered in this chapter.
Programming core interactions:
Programming for scene and environment design:
Working in professional software development teams:
The project content for this chapter can be found at https://github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition/tree/main/Chapter_03.
You can download the entirety of each chapter's project files at https://github.com/PacktPublishing/Unity-Certified-Programmer-Exam-Guide-Second-Edition.
All content for this chapter is held in the chapter's unitypackage file, including a Complete folder that holds all of the work we'll be carrying out in the chapter.
Check out the following video to see the Code in Action: https://bit.ly/3xW4Zte.
As you will recall, back in Chapter 1, Setting Up and Structuring Our Project, we spoke about design patterns and how useful they are for maintaining our code. One of the design patterns we briefly covered was the Singleton pattern. Without repeating ourselves, the Singleton pattern gives us global access to code that can then be obtained at a point in our game. So, where can we see the benefits of using the Singleton design pattern? Well, we could use it so that Unity always keeps certain scripts accessible, no matter what scene we are in. We have already added a lot of structuring to our game framework and we still have a couple of manager scripts to add, such as ScoreManager and ScenesManager.
Now is a good time to give all of the manager scripts global access to all other scripts in the game. Managers give a general overview of what is going on and steer which way the game needs to go without getting caught up in the details of the other scripts that are running during gameplay.
In our current setup, when we run the testLevel scene, our GameManager object is in the Hierarchy window. We also have—and will be adding—more manager scripts to this game object. Currently, when we change scenes, our GameManager script, which sets up our scene's camera and lights, is no longer present.
To stop our GameManager game object and script from being wiped, we are going to add a Singleton design pattern so that our GameManager script will always be in the scene. This design pattern will also make it so that there is only one GameManager script (which is where this design pattern gets its name from).
In the following instructions, we will extend our original GameManager code to work as a Singleton script. Double-click on the GameManager script and let's make a start:
static GameManager instance;
public static GameManager Instance
{
get { return instance; }
}
The reason we do this is that static means there is only one type of game manager. This is what we want; we don't want to have multiple instances of the same manager.
The Awake function ends with a Unity function called DontDestroyOnLoad. This will make sure the game object holding our GameManager class will not be destroyed if the scene changes.
Tip
If the player dies and loses all their lives, we can move from the level scene we are on to the gameOver scene, but we won't wipe the GameManager game object from the scene as this holds the main core methods to run the game.
void Awake()
{
if(instance == null)
{
instance = this;
DontDestroyOnLoad(this);
}
else
{
Destroy(this.gameObject);
}
}
Tip
A similar method to DontDestroyOnLoad is MoveGameObjectToScene, which can be used to carry a single game object over to another scene. This could be useful for moving a player from one scene to another: https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.MoveGameObjectToScene.html.
That's it, our Singleton design pattern is done! The following screenshot shows a snippet of what our GameManager script should look like:
We have created a Singleton design pattern that will not be wiped away when we alternate through the scenes in our game, giving us global control of our game no matter which scene we are in.
Now, we can jump into adding the ScenesManager script and attaching it to the same game object as GameManager (in its Inspector window).
Setting up our ScenesManager script
We will take some responsibility away from the GameManager script by making another manager script to be more consistent with the data and methods it holds. ScenesManager will take and send information to and from GameManager. The following diagram shows how close to GameManager our ScenesManager script is within the framework when only communicating with GameManager:
The purpose of ScenesManager, apart from taking the workload off GameManager, is to deal with anything related to creating or changing a scene. This doesn't mean we only focus on adding and removing game levels; a scene can also consist of a start up logo, a title screen, a menu, and a game over screen, all of which are part of the ScenesManager script's responsibility.
In this section, we will be setting up a scene template and two methods. The first method will be responsible for resetting the level if the player dies (ResetScene()); the second will be the game over screen (GameOver()).
Let's make a start by creating a new script in the same way that we did in Chapter 2, Adding and Manipulating Objects. Follow these steps:
Let's open the ScenesManager script and start coding:
using UnityEngine.SceneManagement;
using UnityEngine;
public class ScenesManager : MonoBehaviour
{
Now, we need to create a list of references for our scenes, as mentioned earlier. I currently have the following scenes labeled:
We will be labeling these scenes as enumerations (which are denoted as enum in the C# language). These values stay consistent.
Tip
If you would like to know more about enumeration, check out https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum.
Scenes scenes;
public enum Scenes
{
bootUp,
title,
shop,
level1,
level2,
level3,
gameOver
}
We will be making and adding these scenes in their respective order in the Unity Editor later on in the book. Before we do so, let's add two methods, starting with the ResetScene() method, which is typically used when the player dies and the current level is reloaded. The other method, GameOver(), is typically called when the player loses all of their lives or when the game is complete.
The ResetScene() method will be called when the player loses a life but still has another remaining. In this short method, we will set its accessibility to public and it returns nothing (void).
Within this method, we will refer to Unity's SceneManager script (not to be confused with our ScenesManager class), followed by Unity's LoadScene method. We now need to provide a parameter to tell LoadScene which scene we are going to load.
We use Unity's SceneManager script again, but this time we use GetActiveScene().buildIndex, which basically means getting the value number of the scene. We send this scene number to SceneManager to load the scene again (LoadScene):
public void ResetScene()
{
SceneManager.LoadScene(SceneManager.GetActiveScene(). buildIndex);
}
A small but effective method, this can be called whenever we need the scene to reset. Let's now move on to the GameOver() method.
This method, as you may expect, is called when the player has lost all of their lives and the game ends, which means we need to move the player on to another scene.
In this method, we continue adding to the ScenesManager script:
public void GameOver()
{
SceneManager.LoadScene("gameOver");
}
}
Similar to the previous method, we refer to this method as public with void return. Within the method, we call the same Unity function, SceneManager.LoadScene, but this time, we call the SceneManager Unity function, followed by the name of the scene we want to load by name (in this case, gameOver).
More Information
SceneManager.LoadScene also offers a LoadSceneMode function, which gives us the option of using one of two properties. By default, the first property is Single, which closes all the scenes and loads the scene we want. The second property is Additive, which adds the next scene alongside the current one. This could be useful when swapping out scenes, such as a loading screen, or keeping the previous scene's settings. For more information about LoadScene, check out https://docs.unity3d.com/ScriptReference/SceneManagement.LoadSceneMode.html.
That's our GameOver() method made, and when used in the same way as our ResetScene() method, it can be called globally. GameOver() can be called not only when the player loses all their lives but also when the user completes the game. It can also be used if, somehow, the game crashes, and as a default reset, we proceed to the gameOver scene.
The next method to bring into our ScenesManager script is BeginGame(). This method is called when we need to start playing our game.
In this short section, we will add the BeginGame() method to our ScenesManager script as this will be called to start playing our game after visiting the shop scene, which we will cover in Chapter 5, Creating a Shop Scene for Our Game.
With the ScenesManager script still open from the previous section, add the following method:
public void BeginGame()
{
SceneManager.LoadScene("testLevel");
}
The code that we have just entered makes a direct call to run the testLevel scene, which we play our game in already. However, as our game begins to grow, we will use more than one scene.
The next thing to do is to create our scenes and add them to the Unity build menu, so let's do that next. Remember to save the ScenesManager script before returning to the Unity Editor.
Adding scenes to our Build Settings window
Our game will consist of multiple scenes through which the player will need to navigate before they can fly their spaceship through the levels. This will result in them either dying or completing each level and the game, and then being taken back to the title scene. This is also known as a game loop. Let's start by going back to Unity and, in the Project window, creating and adding our new scenes. Follow these steps:
Once we have made all of our scenes, we need to let Unity know that we want these scenes to be recognized and applied to the project build order. This is a similar process to what we did in the last chapter when adding testLevel to the Build Settings window. To apply the other scenes to the list, do the following:
Once we have added all the scenes, order them as follows:
Tip
Note that each scene automatically has a camera and a light by default in its Hierarchy window. This is fine and we will customize them later on in this book.
The Build Settings window should now look as follows:
The reason why we are putting our scenes in this order is so that there is a logical progression in the levels. As you can see at the far right of each scene in the previous screenshot, the scenes are counted in increments. So, the first level to load will be the bootUp scene.
Now that we have added multiple scenes to our game, we can consider the fact that we may not want our camera and light setup methods in our GameManager method to run in every scene of our game. Let's briefly return to our GameManager script and update our LightSetup and CameraSetup methods, as well as a few other things.
In this section, we are going to return to the GameManager script and make it so that the CameraSetup and LightSetup methods are called when we are controlling our spaceship only.
To update our GameManager script to support various scenes for our lights and camera, we need to do the following:
public static int currentScene = 0;
public static int gameLevelScene = 3;
bool died = false;
public bool Died
{
get {return died;}
set {died = value;}
}
currentScene is an integer that will keep the number of the current scene we are on, which we will use in the following method. The second variable, gameLevelScene, will hold the first level we play, which we will use later on in this chapter.
void Awake()
{
CheckGameManagerIsInTheScene();
currentScene = UnityEngine.SceneManagement.SceneManager.
GetActiveScene().buildIndex;
LightAndCameraSetup(currentScene);
}
In the code we just entered, we store the buildIndex number (the numbers we have to the right of each scene in our Build Settings window from the previous section) in the currentScene variable. We then send the currentScene value to our new LightandCameraSetup method.
void LightAndCameraSetup(int sceneNumber)
{
switch (sceneNumber)
{
//testLevel, Level1, Level2, Level3
case 3 : case 4 :case 5: case 6:
{
LightSetup();
CameraSetup();
break;
}
}
}
In the code we just wrote, we ran a switch statement to check the value of the sceneNumber variable, and if it falls into the 3, 4, 5, or 6 values, we run LightSetup and CameraSetup.
To reflect on this section, we have created a structure of empty scenes that will each serve a purpose in our game. We have also created a ScenesManager script that will either reset a scene when the player wins or dies and/or move to the game over scene.
Now that we have our scenes in place and the start of the ScenesManager script has been built, we can focus on the player's life system.
In this section, we are going to make it so that the player has a set number of lives. If and when the player collides with an enemy, the player will die, the scene will reset, and a life will be deducted from the player. When all the lives are gone, we will introduce the game over scene.
We will be working with the following scripts in this section:
Let's start by revisiting the GameManager script and setting up the ability to give and take the player's lives:
public static int playerLives = 3;
At the top of the script, just after entering the class and inheritance, enter a static (meaning only one) integer type labeled playerLives, along with the value 3.
Next, we need to create a new method for our GameManager script that will ensure the player loses a life. After we make this new method, the Player script will call it when it makes contact with an enemy.
Let's continue with our GameManager script.
public void LifeLost()
{
We need this to be a public method so that it can be accessed from outside of the script. It's set to void, meaning nothing is returned from the method, and it's followed by the name of the method with empty brackets as it isn't taking any arguments.
//lose life
if (playerLives >= 1)
{
playerLives--;
Debug.Log("Lives left: "+playerLives);
GetComponent<ScenesManager>().ResetScene();
}
After reviewing the if statement code we have entered, we will make a start by adding a comment to let ourselves or other developers know what this condition is doing (//lose life). We will then add the if statement condition, checking whether the player has more than or equal to one life left. If the player does have one or more lives left, we will deduct the player's lives by 1 with the -- operator, which is just a quicker way of saying playerLives = playerLives - 1;.
The line of code following on from the deduction of the player's lives isn't required, but it will notify us, in the Unity Editor Console window, with an information box telling us how many lives the player has left (for debugging purposes), as shown in the following screenshot:
Following on from displaying how many lives the player has left in the Console window, we will refer to the ScenesManager script, which is attached to the GameManager game object. We can use GetComponent to access the ScenesManager script's ResetScene method, which will reset our scene.
else
{
playerLives = 3;
GetComponent<ScenesManager>().GameOver();
}
}
If our player doesn't have any more lives left, that means the if statement condition isn't met, so we can then offer an else condition. Within the scope of our else statement, we reset our player's lives back to 3.
We then access the GameOver() method from the ScenesManager class, which will take us from the scene we are on over to the gameOver scene.
Lastly, all that we need to do now is to make our Player script call the LifeLost method when the player has collided with the enemy or the enemy's bullets:
GameManager.Instance.LifeLost();
Note that we can call the GameManager script directly without finding the game object in the scene by using code such as GetComponent to acquire a script. This is the power of using the Singleton design pattern, calling directly to the LifeLost method.
The level should reset with a message in the Console window showing that we have a particular number of lives left. Repeat this three more times. When the third life is lost, our scene should change from testLevel to gameOver.
The following screenshot shows the Console window tab selected and logging the lives that are lost; also, above the Console section is the Hierarchy window, showing that our game has gone from testLevel to the gameOver scene:
With minimal code, we have now made it so that our player has a number of lives. We have introduced a ScenesManager script into our game framework that talks directly to GameManager, regardless of restarting and changing scenes.
As a side note, you might have noticed that when we changed to the gameOver scene, our GameManager game object was carried over into the gameOver scene. If you recall the Adding a Singleton design pattern section, we set up the CheckGameManagerIsInTheScene method, which is called in the Awake function. This means that just because we are in a different scene, it doesn't mean the Awake function is called again.
Information
Remember, the Awake function will only run when the script is active and will only run once, even if the script is attached to a game object and is carried through scenes.
This is because our gameOver scene only carried the GameManager game object over to the gameOver scene. It wasn't activated, which means the Awake function wasn't called.
We have our basic lives and scene structure, and we have also used the Console window to help us acknowledge the changes.
Before we move on, you may notice that when the player dies, the lights get darker in the scene. The following screenshot shows what I mean:
As you can see in the previous screenshot, on the left is the scene we start with, and on the right is the scene when the player has died. To fix this, we just need to make it so that we generate our lighting manually instead of it being autogenerated by Unity.
To prevent our lighting from going dark between scenes, we need to do the following:
Note that we will likely need to set the lighting manually for other scenes, such as the other levels and the shop scene, later on in this book.
Let's now turn our focus to the enemy and add some functionality so that when it is destroyed by the player, we can add a score to ScoreManager, which is a new script that we will be making next.
As with most games, we need a scoring system to show how well the player has done at the end of the game. Typically, with side-scrolling shooter games, the player is rewarded for each kill they make. If we turn to our game framework diagram, we can see that ScoreManager is hooked up to GameManager like ScenesManager was:
Our code for adding a scoring system will once again be minimal. We also want flexibility so that different enemies are worth different points. We also want it so that when we add another enemy to our game with a different scoring point, we can avoid altering our code each time.
We will be working with the following scripts in this section:
Since the scoring system is an integral factor in our game, it would make sense to add a simple integer to SOActorModel that injects common values into our game objects. This trend will then follow on to other scripts. Let's start adding some code to our already-made scripts before we introduce ScoreManager.
If you recall Chapter 1, Setting Up and Structuring Our Project, we spoke about the SOLID principles and how important it is to add to our code rather than change it, or we risk errors and our code may start mutating and eventually become unfit for purpose. In order to prepare, we will add code to the scripts that we have already made to fit our ScoreManager script into place. Let's start with SOActorModel first. Follow these steps:
public int score;
Before we add more code to the other scripts to fit ScoreManager into our game, we need to acknowledge that we have made a change to our ScriptableObject template.
Let's check our BasicWave Enemy scriptable object in the Unity Editor. Follow these steps:
We have updated the BasicWave Enemy scriptable object. We now need to focus on the EnemyWave script to create and receive this new variable.
int score;
We now need to update the score variable from the ScriptableObject value.
score = actorModel.score;
The EnemyWave script now has a score variable that is set from the value given to it by SOActorModel. The last thing we need to do is send the score value to ScoreManager when the enemy dies due to the actions of the player. Before we do that, let's create and code our ScoreManager script.
The purpose of the ScoreManager script is to total up the score of the player during their game, concluding when they arrive at the gameOver scene. We could also give the ScoreManager script other score-related functionality, such as the ability to store our score data on the device that we are playing the game on or to send the score data to a server for an online scoreboard. For now, we will keep things simple and just collect the player's score.
We can create and add our ScoreManager script to the game framework, as follows:
If you can't remember how to do this, then check out the Setting up our ScenesManager script section of this chapter. The following screenshot shows ScoreManager attached to the GameManager game object in the Inspector window:
Next, we are going to open the ScoreManager script and add code that will hold and send score data. Open the ScoreManager script and enter the following code:
using UnityEngine;
By default, we require the UnityEngine library, as previously mentioned.
This is a public class, with ScoreManager inheriting MonoBehaviour to increase the functionality of the script.
Following on from this is our public property, which gives outside classes access to the playerScore variable. As you'll notice, the PlayerScore property returns an integer. Within this property, we use the get accessor to return our private playerScore integer. It is a good habit to keep our variables private, or you risk exposing your code to other classes, which can result in errors. The following code shows you how to complete this step:
static int playerScore;
public int PlayersScore
{
get
{
return playerScore;
}
}
Accessors
To find out more about accessors, check out https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/get.
public void SetScore(int incomingScore)
{
playerScore += incomingScore;
}
public void ResetScore()
{
playerScore = 00000000;
}
}
We can call this method at the beginning or end of a game to stop the score from carrying on into the next game.
As mentioned earlier, we can now return to the EnemyWave script to send the value of the enemy's score points to the ScoreManagers method, SetScore, thereby adding them to the player's total score:
GameManager.Instance.GetComponent<ScoreManager>().SetScore(score);
When this particular enemy dies as a result of the player, this line of code will send the enemy's score value directly to the playerScore variable and increment it toward its total until the player loses all of their lives.
Debug.Log("ENDSCORE: " +
GameManager.Instance.GetComponent<ScoreManager>
().PlayersScore);
This code will tell us how much the player has scored because it directly accesses ScoreManager and grabs the PlayerScore property when the game is over. The following screenshot shows an example of a totaled score:
In this section, we introduced the ScoreManager script with its basic working structure of totaling up our end score and displaying the final count in the Console window. We have also added more code to a selection of scripts without deleting and changing any of their content. Next, we will be doing something different that doesn't involve any coding but gets us more familiar with Unity's sound components.
Up until now, our game has been silent, but sound is an important factor in any game. In this section, we will be introducing our first sound component. We will make a start by creating sound effects for when our player fires a bullet.
Feel free to add your own type of bullet sound if you wish. You can add sound to your player's standard bullets as follows:
Information
As well as the Volume option in the Audio Source component, there is Pitch to change the sound of our bullet and Stereo Pan to make the sound more dominant in the left or right speaker. Finally, because this is a two-dimensional game, we don't want the sound to be affected by how close our camera is to the bullet. So, we slide the Spatial Blend toggle all the way to the left to make sure it is not affected by its distance.
That brings us to the end of this short section on audio, but we will cover more on audio throughout this book. Don't forget that if you get stuck at any point, check the Complete folder for this chapter and compare the scenes and code to make sure nothing is missing.
In this chapter, we have extended our game framework structure by implementing and reinforcing the GameManager script by extending its code. This means that it will never be deleted, regardless of scene changes. We have also introduced the score and scenes managers, which were originally planned in our game framework. These two additional managers take responsibility away from the game manager and add additional features to your game. We ensured these scripts don't mutilate our original code (removing, overflowing, or compensating for our game manager). Your game now has a working scoring system, as well as multiple scenes that can be restarted and changed with very little code. We also introduced sound, which we'll implement in more detail in later chapters.
In the next chapter, we'll focus less on code-heavy content and instead concern ourselves with the art of the game. Even though we are programmers, we need to understand how to manipulate assets and how to animate with Unity's API. With just a little bit of coding, this will allow us to understand the connection between the Editor and our script. We'll also touch on some particle effects.
Well done—you've done and covered a lot. Before we move on, have a go at the following questions. They resemble what you will encounter in your programmer exam.
This is your first mini mock test. These tests represent sections of your final Unity exam. This first mini mock test consists of just five questions. Later on in this book, we'll introduce more mini mock tests with more questions.
Fortunately, you will only be tested on what we have covered so far:
void Start()
{
Light playersTorch = GetComponent<Light>();
playersTorch.lightMapBakeType = LightMapBakeType. Mixed;
playersTorch.type = LightType.Area;
playersTorch.shadows = LightShadows.Soft;
playersTorch.range = 5f;
}
You notice, however, that the player's torch isn't casting any light or shadows. What should you change for this code to work as desired?
You've checked your code and the joystick and both seem to be working fine, suggesting the issue is with the input manager.
What change should you make within the input manager?
Which design pattern suits having a GameManager script in a persistent instance role?
Your lead developer says that your method is costing too much in-memory performance and wants you to store a maximum of 10 rocks from within an array of rocks using a design pattern. Once a rock is used, instead of being destroyed, it should return to the array.
What design pattern is the developer referring to?
That's the end of your first mini mock test. To check your answers, refer to the Appendix section at the back of this book. How did you do? To review any incorrect answers, I suggest flicking back through the last couple of chapters to the relevant section and refreshing your memory where needed. Sadly, exams can be a bit of a memory game. Everyone's memory is different, and the majority of people that pass these exams have failed on certain sections before passing.
Either way, the more you complete these tests, the stronger you will become at them. Just stay focused and you'll get through it!