In part 6 of this tutorial series we learned about Unity’s UI system and we created our main menu along with the character select buttons.
In this part we are going to learn how to interact with UI buttons, how to navigate between scenes and we will learn about the singleton pattern which is one of the most important concepts of Unity game development.
Button On Click Event Listener
Now, to execute a function when we press a button we need to create a OnClick listener for that button. To create a OnClick listener, select the Player 1 Button in the Hierarchy, and in the Inspector tab scroll down until you see the Button component and the On Click empty list:
public void PlayGame()
{
Debug.Log("The button is pressed");
}
Now select the Player 1 Button in the Hierarchy and drag the Main Menu Controller game object in the empty field:
Now from the drop down list on the right side where it says No Function, click on it, search for the MainMenuController script and then the PlayGame function of that script and select it:
Now in the On Click list you will see the MainMenuController script and the PlayGame function of that script:
Build Settings
In the Build Settings, click on the Add Open Scenes button and it will add the current scene you have opened, to the build:
One thing that is important when it comes to Build Settings is the order of the scenes in the list. Because the first scene in the list will be the first scene that is called when you export the game on any platform.
You can also see the index numbers on the right side of the scenes indicating the placement of the scene in the list. The scene that has index 0 is the first scene in the list and the first scene that will be called when the game runs on any device:
In our case the Gameplay scene is the one that will be called first, which is not what we want. We can fix this by rearranging the scenes in the build list by dragging them:
We only have two scenes, but you can rearrange any number of scenes the same way you saw in the example above.
Navigating Between Scenes
UnityEngine.SceneManagement.SceneManager.LoadScene("Gameplay");
using UnityEngine.SceneManagement;
When we first introduced programming we said that there are a lot of predefined libraries at our disposal.
This means that a lot of the functionality like Debug.Log that prints to the screen, is already written and defined and we just use it without the need to write the code that will make the Debug.Log function print to the screen.
Same here. We don’t need to create our own code that will load scenes in Unity, the programmers at Unity have already created that for us, and in order for us to use it, we need to import those classes with the using keyboard like we did above.
Now there are two ways how we can load a scene in Unity. The first one is to load the scene by using the scene’s name.
Add the following line of code in the PlayGame function:
public void PlayGame()
{
SceneManager.LoadScene("Gameplay");
}
We can use the index number of the scene to load a specific scene. In our case, if we want to load the Gameplay scene, we need to use index 1.
This is how it looks like in the code:
public void PlayGame()
{
SceneManager.LoadScene(1);
}
public static string GAMEPLAY_SCENE_NAME = "Gameplay";
public static string MAIN_MENU_SCENE_NAME = "MainMenu";
public void PlayGame()
{
SceneManager.LoadScene(TagManager.GAMEPLAY_SCENE_NAME);
}
The Singleton Pattern
When the button for the appropriate player character is pressed, we are going to load the Gameplay scene and play the game with the selected character.
To do this, we first need to create a new script. Inside the Assets -> Scripts -> Controller Scripts, Right Click -> Create -> C# Script and name it CharacterSelectController.
In the Hierarchy tab, Right Click -> Create Empty and name the new game object Character Select Controller and attach the CharacterSelectController script on it:
public static CharacterSelectController instance;
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
To explain how destroying the duplicate singleton object works, I am going to comment out the else statement in the CharacterSelectController script:
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
//else
//{
// Destroy(gameObject);
//}
}
void Start()
{
playerTarget = GameObject.FindWithTag(TagManager.PLAYER_TAG).transform;
Invoke("LOAD", 2f);
}
void LOAD()
{
UnityEngine.SceneManagement.SceneManager.LoadScene("MainMenu");
}
When we don’t use the Destroy function then we have duplicate game objects that have the same script, and that script is not a singleton because there are two copies of game objects who carry that same script.
Now remove the comments from the else statement in the CharacterSelectController:
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
Now when we move from scene to scene, there is only one copy of the CharacterSelectController script in the whole game. And this is the point of the singleton pattern.
This allows us to create unique game objects in the game that can carry information throughout the whole game.
That is how we are going to select the player character in the MainMenu scene, and then load the selected player character in the Gameplay scene.
Before we proceed, remember to remove the LOAD function we created in the CameraFollow script:
public class CameraFollow : MonoBehaviour
{
private Transform playerTarget;
private Vector3 tempPos;
[SerializeField]
private float minX, maxX;
// Start is called before the first frame update
void Start()
{
playerTarget = GameObject.FindWithTag(TagManager.PLAYER_TAG).transform;
}
// Update is called once per frame
void LateUpdate()
{
if (!playerTarget)
return;
tempPos = transform.position;
tempPos.x = playerTarget.position.x;
if (tempPos.x < minX)
tempPos.x = minX;
if (tempPos.x > maxX)
tempPos.x = maxX;
transform.position = tempPos;
}
} // class
Loading The Selected Player Character
[SerializeField]
private GameObject[] characters;
private int _charIndex;
public int CharIndex
{
get { return _charIndex; }
set { _charIndex = value; }
}
The order of player prefabs in the Characters array is very important here, because we are going to use indexes to select a player character, then we need to make sure that the indexes match.
When we press the Player 1 Button we want to select the player with the yellow shirt, so that player needs to be the first element in the Characters array e.g. he needs to be at element index.
So again, make sure that Player 1 prefab is at element 0, and Player 2 prefab is at element 1:
public void SelectPlayer1()
{
// first character is selected
CharacterSelectController.instance.CharIndex = 0;
Debug.Log("The index of the selected character is: " + CharacterSelectController.instance.CharIndex);
// load the gameplay level
}
public void SelectPlayer2()
{
// second character is selected
CharacterSelectController.instance.CharIndex = 1;
Debug.Log("The index of the selected character is: " + CharacterSelectController.instance.CharIndex);
// load the gameplay level
}
When we press the appropriate button, we see that the appropriate index is selected.
Another way how we can select the character is to create a function with a parameter. In the MainMenuController script add the following lines:
public void SelectPlayer(int index)
{
CharacterSelectController.instance.CharIndex = index;
Debug.Log("The selected character is: " + CharacterSelectController.instance.CharIndex);
// load gameplay level
}
We use the index parameter of the function and assign it as the CharIndex. To make this work, attach the SelectPlayer function to both buttons as the OnClick listener.
When you do that, you will notice a field bellow the list where we selected the SelectPlayer function:
As you can see we print the same text in the console, but the code is different. Now let’s take a look at our third way how we can achieve the same effect.
When we press a button, we can get information about the button that is pressed from Unity’s event system. With that information we can access things such as the name of the button.
Remove the current code from the SelectPlayer function and add the following lines:
public void SelectPlayer()
{
string btnName = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject.name;
Debug.Log("The name of the button that is pressed is: " + btnName);
}
When we press the appropriate button we see the name of that button printed in the console. But how is this going to help us with selecting the player character?
Well, we can change the names of the buttons, instead of Player 1 Button and Player 2 Button we can name them 0 and 1:
public void SelectPlayer()
{
string btnName = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject.name;
CharacterSelectController.instance.CharIndex = int.Parse(btnName);
Debug.Log("The index of the selected player is " + CharacterSelectController.instance.CharIndex);
}
public void SelectPlayer()
{
CharacterSelectController.instance.CharIndex = int.Parse(UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject.name);
Debug.Log("The index of the selected player is " + CharacterSelectController.instance.CharIndex);
}
What we demonstrated now is a really good example of the fact that in programming there are always multiple ways how to fix a certain problem.
You can use whichever solution you want out of the ones we presented, but aim to use the easiest solution and the most efficient.
For example, I would never use two functions approach because that is wasting lines of code if there we already have a solution where we can use one function.
This is how you should think of programming in general, always chose the easiest and the most efficient solution.
You are probably wondering how will you know what is the easiest and most efficient solution, right?
That comes with experience, the more code you write, the more projects you create and the more you learn, the better you will be at fixing problems and getting to that “thinking as a programmer” mentality.
I will leave all the solutions down below and you can chose which want you want to use, just don’t forget to load the Gameplay scene after you get the index of the selected character, also, if you are going to use parsing solution, make sure that the names of your buttons are 0 and 1:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MainMenuController : MonoBehaviour
{
public void SelectPlayer1()
{
CharacterSelectController.instance.CharIndex = 0;
SceneManager.LoadScene(TagManager.GAMEPLAY_SCENE_NAME);
}
public void SelectPlayer2()
{
CharacterSelectController.instance.CharIndex = 1;
SceneManager.LoadScene(TagManager.GAMEPLAY_SCENE_NAME);
}
public void SelectPlayer(int index)
{
CharacterSelectController.instance.CharIndex = index;
SceneManager.LoadScene(TagManager.GAMEPLAY_SCENE_NAME);
}
public void SelectPlayer()
{
CharacterSelectController.instance.CharIndex = int.Parse(UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject.name);
SceneManager.LoadScene(TagManager.GAMEPLAY_SCENE_NAME);
}
}
Instantiating The Selected Character When The Game Starts
Now that we are able to select the player character to play the game, we need to instantiate that player character when the Gameplay scene is loaded.
We are going to do by testing if the loaded scene’s name is Gameplay, and if it is, then we will instantiate the selected character.
This can be done with the help of delegates and events. If you don’t know what are delegates and events, make sure that you learn about that concept by clicking here before you proceed with this part of the tutorial.
Following the concept of events, we need to create a function that has the same signature as the function declared by Unity that will inform us that the scene has been loaded.
We can find the signature of that function in Unity’s documentation about SceneManager.sceneLoaded.
From the documentation we see that we need to create a function that takes a Scene and LoadSceneMode as a parameter.
Inside the CharacterSelectController declare the following function:
void OnLevelFinishedLoading(Scene scene, LoadSceneMode mode)
{
}
using UnityEngine.SceneManagement;
private void OnEnable()
{
SceneManager.sceneLoaded += OnLevelFinishedLoading;
}
private void OnDisable()
{
SceneManager.sceneLoaded -= OnLevelFinishedLoading;
}
In the OnLevelFinishedLoading function add the following lines:
void OnLevelFinishedLoading(Scene scene, LoadSceneMode mode)
{
if (scene.name == TagManager.GAMEPLAY_SCENE_NAME)
{
Instantiate(characters[CharIndex]);
}
}