In the previous blog we created our obstacles, so let’s begin spawning them. If you haven’t read Part 1 yet, it’s highly recommended. Many of the features in this blog will require what we learned in Part 1.
Check out the other parts to this series:
You can download the project files here.
BUILD GAMES FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.
Spawning the Obstacles
First, create an empty GameObject called “GameManager”. This will hold our obstacle spawner as well as other scripts. Then, create a new C# script called “ObstacleSpawner” and attach it to the game manager.
This is how it’s going to work. When we spawn an obstacle, we’re first going to decide if it’s going to come from the left or right hand side of the screen. Then with that side decided, we pick a random Y axis value from a pre-defined min and max. The the object is spawned and thrown across the screen.
The “obstacles” array will hold all of the obstacle prefabs. “minSpawnY” and “maxSpawnY” is the vertical range to spawn objects along. “leftSpawnX” and “rightSpawnX” are the X positions of the left/right side of the screen.
public GameObject[] obstacles; // array of all the different types of obstacles public float minSpawnY; // minimum height objects can spawn at public float maxSpawnY; // maximum height objects can spawn at private float leftSpawnX; // left hand side of the screen private float rightSpawnX; // right hand side of the screen public float spawnRate; // time in seconds between each spawn private float lastSpawn; // Time.time of the last spawn
We’re also going to be pooling our obstacles. This helps with performance as we Instantiate all of them at the start of the game.
// pooling private List<GameObject> pooledObstacles = new List<GameObject>(); // list of objects in the pool private int initialPoolSize = 20; // size of the pool
Now in the “Start” function we need to calculate the left and right spawn X variables. To do this, we first need to get the width of the camera. Unfortunately that’s not something we can just get, so we just need to do a bit of math. Since the middle of the camera is X=0, we can determine that left spawn X will be -camWidth / 2, with right spawn being the same but positive.
Let’s also call the function to create our pool, which we’ll be creating next.
void Start () { // setting left and right spawn borders // do this by getting camera horizontal borders Camera cam = Camera.main; float camWidth = (2.0f * cam.orthographicSize) * cam.aspect; leftSpawnX = -camWidth / 2; rightSpawnX = camWidth / 2; // create our initial pool CreateInitialPool(); }
“CreateInitialPool” instantiates a set number of obstacles, deactivates them and adds them to the “pooledObstacles” list. We’ll then get objects from that list later on to ‘spawn’ them.
// instantiates the initial objects to add to the pool void CreateInitialPool () { for(int index = 0; index < initialPoolSize; index++) { // determine which obstacle type we're going to create GameObject obstacleToSpawn = obstacles[index % 4]; // instantiate it GameObject spawnedObject = Instantiate(obstacleToSpawn); // add it to the pool pooledObstacles.Add(spawnedObject); // deactivate it spawnedObject.SetActive(false); } }
“GetPooledObstacle” returns an inactive object from the “pooledObstacles” list. We’ll be calling this function instead of “Instantiate”.
// returns a new pooled obstacle ready to be used GameObject GetPooledObstacle () { GameObject pooledObj = null; // find a pooled object that is not active foreach(GameObject obj in pooledObstacles) { if (!obj.activeInHierarchy) pooledObj = obj; } // if we couldn't find one, log error if (!pooledObj) Debug.LogError("Pool size is not big enough!"); // activate it pooledObj.SetActive(true); // then send it return pooledObj; }
Now it’s time for the “SpawnObstacle” function, which will spawn a random obstacle.
First, we get an available obstacle from the “GetPooledObstacle” function and set a position for it from the “GetSpawnPosition” function. We’ll get to that soon. Then with the spawned object, we set the move direction to be horizontally across the screen.
// spawns a random obstacle at a random spawn point void SpawnObstacle () { // get the obstacle GameObject obstacle = GetPooledObstacle(); // set its position obstacle.transform.position = GetSpawnPosition(); // set obstacle's direction to move in obstacle.GetComponent<Obstacle>().moveDir = new Vector3(obstacle.transform.position.x > 0 ? -1 : 1, 0, 0); }
The “GetSpawnPosition” returns a Vector3, which is the random position for the object to spawn at. Inside the function, we first determine the X position. This is a 50/50 chance of being on the left or right. Then the Y position is a random value between the min and max Y spawn range.
// returns a random spawn position for an obstacle Vector3 GetSpawnPosition () { float x = Random.Range(0, 2) == 1 ? leftSpawnX : rightSpawnX; float y = Random.Range(minSpawnY, maxSpawnY); return new Vector3(x, y, 0); }
To spawn these overtime, we check each frame in the “Update” function if the time between now and the last time we spawned is more than the spawn rate. If so, we reset the last spawn time and spawn an obstacle.
void Update () { // every 'spawnRate' seconds, spawn a new obstacle if(Time.time - spawnRate >= lastSpawn) { lastSpawn = Time.time; SpawnObstacle(); } }
Now back in the editor we can fill out the script variables. Add all of your obstacle prefabs into the obstacles array. I set the Min Spawn Y to be -3.5 and max to be 2. This spawns them just of the ground to just below the ship. For the spawn rate I set it to 0.75, but you can test and tweak this value.
Here’s how it should look like when you play the game! Make sure that you get stunned when you collide with the obstacles.
Creating the Base Problem Class
The game manager is going to be a script which holds all of our math problems and runs the game’s loop.
So first up, create a new C# script called “Problem”. This is going to be the base class for our math problems.
Under the pre-made class, create a new enumerator called “MathsOperation”. For our game the format is going to be: [number] [operation] [number]. The way the two numbers are going to be calculated together is determined by the operator. Addition, subtraction, multiplication and division are the ones we’re going to be using.
public enum MathsOperation { Addition, Subtraction, Multiplication, Division }
We then need to change the pre-made “Problem” class. First, remove the “MonoBehaviour” text at the end of the class definition and add “[System.Serializable]” just above.
We remove mono behavior because we don’t need any of Unity’s pre-made functions and for the fact that this script isn’t going to be attached to any script. The System.Serializable property makes it so that this class can be displayed in the Inspector with all it’s values laid out to edit.
With our variables, we have our two numbers and our operator. “answers” is a float array which will hold all the possible answers, including the correct one. “correctTube” is the index number of the correct answer in the “answers” array.
[System.Serializable] public class Problem { public float firstNumber; // first number in the problem public float secondNumber; // second number in the problem public MathsOperation operation; // operator between the two numbers public float[] answers; // array of all possible answers including the correct one [Range(0, 3)] public int correctTube; // index of the correct tube }
Scripting the Game Manager
Now that we have our problem class, let’s make the game manager. Create a new C# script called “GameManager” and attach it to the “GameManager” object.
Here’s our variables. “problems” is an array holding all of our math problems. “curProblem” is the index number of the “problems” array, pointing to the current problem the player is on.
public Problem[] problems; // list of all problems public int curProblem; // current problem the player needs to solve public float timePerProblem; // time allowed to answer each problem public float remainingTime; // time remaining for the current problem public PlayerController player; // player object
We also want to create an instance of this script (or singleton). This means we can easily access the script by just going GameManager.instance.[…] without needing to reference it. The only downside, is that you can only have one instance of the script.
// instance public static GameManager instance; void Awake () { // set instance to this script. instance = this; }
Let’s start with the “Win” and “Lose” functions. Right now they’ll just set the time scale to o (pausing the game). Later on we’ll call a UI function to show text saying “You Win!” or “Game Over”.
// called when the player answers all the problems void Win () { Time.timeScale = 0.0f; // set UI text } // called if the remaining time on a problem reaches 0 void Lose () { Time.timeScale = 0.0f; // set UI text }
Now we need a way to present / set a problem. The “SetProblem” function will carry over an index number for the problem array and set that as the current problem.
// sets the current problem void SetProblem (int problem) { curProblem = problem; remainingTime = timePerProblem; // set UI text to show problem and answers }
When the player gets the correct answer, “CorrectAnswer” will be called. “IncorrectAnswer” will be called if it’s the wrong answer. If they get it wrong, we’ll just stun them.
// called when the player enters the correct tube void CorrectAnswer() { // is this the last problem? if(problems.Length - 1 == curProblem) Win(); else SetProblem(curProblem + 1); } // called when the player enters the incorrect tube void IncorrectAnswer () { player.Stun(); }
When the player enters a tube, the “OnPlayerEnterTube” function will be called, carrying over the id of the tube, which correlates back to the “answers” array in the “Problem” class.
// called when the player enters a tube public void OnPlayerEnterTube (int tube) { // did they enter the correct tube? if (tube == problems[curProblem].correctTube) CorrectAnswer(); else IncorrectAnswer(); }
Since we’re having a timer for each problem, we need to check if it’s ran out. If so, then the player will lose.
void Update () { remainingTime -= Time.deltaTime; // has the remaining time ran out? if(remainingTime <= 0.0f) { Lose(); } }
Finally, we need to set the initial problem when the game starts.
void Start () { // set the initial problem SetProblem(0); }
Back in the editor we can begin to create our problems. Here, I have 3 problems, one addition, multiplication and division. Make sure that your “answers” array is as many tubes as you have and set the “correctTube” slider to be the element number with the correct answer.
Creating the UI Elements
It’s going to be pretty hard to play the game at the moment without some UI, so let’s get into that.
What we want to do is have a world space canvas which can hold our text elements. Setting up a world space canvas can be a bit finicky though, so first, let’s create a Canvas.
Then change the “Render Mode” to Screen Space – Camera. Drag the main camera into the “Render Camera” property and you should see that the canvas becomes the same size as the camera.
Now we can change the “Render Mode” to World Space and sorting layer to “UI”. This prevents us from needing to manually scale the canvas down to the camera size.
Create a new Text element as a child of the Canvas (right click Canvas > UI > Text). Then position it in the middle of the large white rectangle and change the boundaries so it fits.
For the text properties, set it to bold, with a size of 30 and center it horizontally and vertically.
Then create another Text element for the answer. This is going to be similar to the problem one, but smaller of course.
Now just duplicate it for each problem tube.
To show the player how much time they have left, we’ll add in a dial that will retract over time. Create a new Image element (right click Canvas > UI > Image). Then set the “Source Image” to be the UIDial and resize the width and height to be 50. Now to make the image actually be able to reduce like a clock, we need to change the “Image Type” to Filled and then set the “Fill Origin” to Top. Now position it to the right of the problem text.
When the game ends, we need text to display if they won or not. Create a new Text and place it in the middle of the screen. I added an Outline component since we’ll be changing the text color later on in script. Also disable it, so it doesn’t appear when we start playing.
Scripting the UI
Create a new C# script called “UI” and attach it to the “GameManager” object.
We first need to reference the UnityEngine.UI library, since of course we’re going to be modifying UI elements.
using UnityEngine.UI;
For our variables, we have the problem text, answers text as an array. The remaining time dial, end text and remaining time dial rate. I’ll explain that shortly.
public Text problemText; // text that displays the maths problem public Text[] answersTexts; // array of the 4 answers texts public Image remainingTimeDial; // remaining time image with radial fill private float remainingTimeDialRate; // 1.0 / time per problem public Text endText; // text displayed a the end of the game (win or game over)
We’re also going to create an instance with the UI script, like the GameManager.
// instance public static UI instance; void Awake () { // set instance to be this script instance = this; }
With our time dial, we’re using an Image that is “filled”. This means we can choose what percentage of the image is visible. In our case, we have it radial so it fills up like how a hand goes around a clock.
The “fill amount” is 0.0 to 1.0. Since our remaining time will be around 15 seconds, we need a way of converting 15 to 1 to use in the fill amount value.
void Start () { // set the remaining time dial rate // used to convert the time per problem (8 secs for example) // and converts that to be used on a 0.0 - 1.0 scale remainingTimeDialRate = 1.0f / GameManager.instance.timePerProblem; }
In the “Update” function, we’re going to be constantly updating the time dial.
void Update () { // update the remaining time dial fill amount remainingTimeDial.fillAmount = remainingTimeDialRate * GameManager.instance.remainingTime; }
Now we need to setup the function “SetProblemText” which gets a problem and displays it on the UI.
// sets the ship UI to display the new problem public void SetProblemText (Problem problem) { string operatorText = ""; // convert the problem operator from an enum to an actual text symbol switch(problem.operation) { case MathsOperation.Addition: operatorText = " + "; break; case MathsOperation.Subtraction: operatorText = " - "; break; case MathsOperation.Multiplication: operatorText = " x "; break; case MathsOperation.Division: operatorText = " ÷ "; break; } // set the problem text to display the problem problemText.text = problem.firstNumber + operatorText + problem.secondNumber; // set the answers texts to display the correct and incorrect answers for(int index = 0; index < answersTexts.Length; ++index) { answersTexts[index].text = problem.answers[index].ToString(); } }
Also when the game ends (either win or lose) we need to set the on screen text. “SetEndText” will send over a bool (win = true, lose = false). If it’s a win, the end text will be “You Win!” and the color will be green. If it’s a loss, the end text will be “Game Over!” and the color will be red.
// sets the end text to display if the player won or lost public void SetEndText (bool win) { // enable the end text object endText.gameObject.SetActive(true); // did the player win? if (win) { endText.text = "You Win!"; endText.color = Color.green; } // did the player lose? else { endText.text = "Game Over!"; endText.color = Color.red; } }
Now we need to go back to the “GameManager” script and connect the two scripts. In the “SetProblem” function add…
UI.instance.SetProblemText(problems[curProblem]);
In the “Win” function add….
UI.instance.SetEndText(true);
In the “Lose” function add…
UI.instance.SetEndText(false);
Now that we’re done with that, go back to the editor and add the objects to the UI script.
Finishing off the Problem Tubes
The last thing we need to do is add functionality to the problem tubes. Create a new C# script called “ProblemTube” and attach it to all of the 4 problem tube objects we made.
This is all we need to code. We check for a trigger enter with the player and if that happens, call the “OnPlayerEnterTube” function in “GameManager” and send over the tube id.
public int tubeId; // identifier number for this tube // called when something enters the tube's collider void OnTriggerEnter2D (Collider2D col) { // was it the player? if(col.CompareTag("Player")) { // tell the game manager that the player entered this tube GameManager.instance.OnPlayerEnterTube(tubeId); } }
Now go back to the editor and enter in the tube id for each tube. 0, 1, 2 and 3.
Conclusion
Congratulations! You’ve now finished your math game! Try it out for yourself, or share it with your friends. Either way, you’ve learned to not only set up timers, enemies, and move a character, but how to provide a logic challenge as well.