Now that you've got used to the Unity editor, created some basic 3D objects and added some input controls, it's time to turn the scene into a game.
The game you are going to create is very simple. It will feature an enclosed arena containing four zones (left, right, front and back), which your ball must enter as quickly as possible.
To model the walls and roof of the game arena, you are going to make copies of the existing ground and then transform those copies so they are in the correct position.
So, in the Hierarchy, right-click on "Ground" and select copy. Then, right-click again in the Hierarchy, and select paste - you will have a new GameObject, named "Ground (1)". Right-click on that, select Rename, and call it "Left Wall". Now, you need to position it so it does indeed become the left wall. The easiest way to do that is to type in values directly into the GameObject's Transform in the inspector; the x and y positions should be 5, and the z rotation should be 90 degrees, just like Figure 1.
Figure 1: Left wall transform
Next, repeat the process for the right wall. However, this time, the x position should be -5 and the y position should be 5. The z rotation should be -90 degrees, just like Figure 2.
Figure 2: Right wall transform
Then, repeat the process for the back wall. However, this time, the y position should be 5 and the z position should be -5. The x rotation should be 90 degrees, just like Figure 3.
Figure 3: Back wall transform
Next, the front wall. Here, you are going to take advantage of back face culling, whereby, to preserve processing cycles, Unity will only render one side of a GameObject. Hence, if the wall is positioned so that the rendered side looks inward, the player will be able to see into the arena, but the ball will still rebound off the wall. To take advantage of this, the y and z positions should be 5 and the x rotation should be -90 degrees, just like Figure 4.
Figure 4: Front wall transform
All that remains is to repeat the process for the roof of the arena. this time, the y position should be 10 and the x rotation should be 180 degrees, just like Figure 5.
Figure 5: Roof transform
Now position everything so that the player is looking directly into the arena, and align the main camera with that view.
At this point, the Hierarchy is beginning to look a little unorganised, so, just as you have done for the Project, you should tidy up. To do so, click on your first wall in the hierarchy, then shift-click on the last wall to highlight all four walls. Then right-click > Create Empty parent and call it "Walls". Next, highlights "Ground", "Roof" and "Walls", right-click > Create Empty Parent and call it "Arena".
All being well, you will have a Hierarchy and scene that looks something like Figure 6, below:
Figure 6: Arena walls
Now, if you press play in the Toolbar, you should be able to move the ball using the spacebar and arrow keys and it should be impossible for the ball to escape the arena.
To turn the arena into a game, you're going to create some trigger zones for the ball to move into, then use some more of Unity's physics engine, via Triggers, to keep a track of which zones the ball enters.
First, create the front trigger zone using a plane GameObject. Name the object "Front Trigger", and make its transform look like that of Figure 7.
Figure 7: Front trigger transform
The idea here is to make the trigger zone a thin strip (its z scale is 0.1), position it at the front of the arena (its z position is 4.5) and move it so it is just off the floor (its y position is 0.001 - if you left this with a y position of zero, you might experience a phenomenon called Z-fighting, a flickering effect that happens when two or more objects are fighting to be rendered at the same position).
Also, select the Convex and Is Trigger options of the "Front Triggger" Mesh Collider component in the Inspector, as per Figure 8.
Figure 8: Convex trigger
A collider that's marked as a trigger detects other colliders (but will not trigger collisions themselves). To handle the detection, you use an OnTrigger method in a script. So, in the scripts folder in the Projects window, right click > create > C# Script and call it "Zones". Then drag the script onto the "Front Trigger" in the hierarchy. Finally, double click on the script to open it in Visual Studio Code. Now, add an OnTrigger method, and use the Debug.Log trick to check that the trigger is firing whenever the ball collides with it:
void OnTriggerEnter(Collider other)
{
Debug.Log("In Trigger");
}Save the script and return to Unity - if you've written the script correctly, when you play and the ball collides with "Front Trigger", you should see "In Trigger" in the console window. If you do not see that, first check that you did not have any compilation errors. If not, then check that the script is attached as a component via the "Front Trigger" inspector.
Next, create the left trigger zone similar to how you created the walls of the arena; copy the front trigger zone in the Hierarchy, rename it "Left Trigger", then make its transform look like Figure 9.
Figure 9: Left trigger transform
For the right trigger zone, copy the front or left trigger zones in the Hierarchy, rename it "Right Trigger", then make its transform look like Figure 10.
Figure 10: Right trigger transform
The back trigger is going to go on a stand (so the player has to use the jump interaction to activate the trigger). So, in the hierarchy, right-click > 3D Object > Cube. Name it "Back Stand". Drag the green material onto it (to differentiate it from the trigger zones). Then make its transform look like that of Figure 11.
Figure 11: Back stand transform
Now, For the back trigger zone, copy the front, left or right trigger zones in the Hierarchy, rename it "Back Trigger", then make its transform look like Figure 12. The idea here is to place the trigger on the stand you just created above.
Figure 12: Back trigger transform
You should organise the hierarchy to remove the clutter of the trigger zones. Something like Figure 13 would do that.
Figure 13: Arena hierarchy
Finaly, your scene should look something similar to that in Figure 14, below.
Figure 14: Arena scene
Now, if you press play in the Toolbar, you should see "In Trigger" in the console window whenever the ball collides with the front, back, left or right trigger zones.
Instead of just logging "In Trigger" in the Console, the "Zones" script should record that the ball has collided with its trigger. To do so, make the script look like this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Zones : MonoBehaviour
{
private bool hasTriggered = false;
public bool HasTriggered
{
get { return hasTriggered; }
}
void OnTriggerEnter(Collider other)
{
hasTriggered = true;
}
}The script above uses a boolean variable, hasTriggered, to record that its OnTriggerEnter method has been called. It also declares a C# Property (the HasTriggered method) to return the value of that variable. Additionally, the Start and Update methods have been removed, as they are not required.
The final script you will write manages the game. It will know about the trigger zones and will keep track of time, so that, when all four zones have been visited, it can output the total time the player took to complete that.
To create the script, in the scripts folder in the Projects window, right-click > create > C# Script and call it "GameManager". Double click on the script to open it in Visual Studio Code. Below is a first draft of "GameManager", where it knows about the trigger zones and knows when the ball has visited all four, at which point, it outputs that fact to the Console:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
[SerializeField] private GameObject leftTrigger;
[SerializeField] private GameObject rightTrigger;
[SerializeField] private GameObject frontTrigger;
[SerializeField] private GameObject backTrigger;
private Zones leftTriggerScript;
private Zones rightTriggerScript;
private Zones frontTriggerScript;
private Zones backTriggerScript;
// Start is called before the first frame update
void Start()
{
leftTriggerScript = leftTrigger.GetComponent<Zones>();
rightTriggerScript = rightTrigger.GetComponent<Zones>();
frontTriggerScript = frontTrigger.GetComponent<Zones>();
backTriggerScript = backTrigger.GetComponent<Zones>();
}
// Update is called once per frame
void Update()
{
if (leftTriggerScript.HasTriggered &&
rightTriggerScript.HasTriggered &&
frontTriggerScript.HasTriggered &&
backTriggerScript.HasTriggered)
{
Debug.Log("Game complete!");
}
}
}To make the script work, you should create an empty GameObject called "GameManager" in the Hierarchy. Drag the script onto that, then, in the Inspector for "GameManager", drag the "Left Trigger" GameObject into the serialised field "Left Trigger". Do the same for "Right Trigger", "Front Trigger" and "Back Trigger". Your scene should look solmething like Figure 15:
Figure 15: The GameManager with the Triggers Assigned
Now, if you press play in the Toolbar, after you visit all four trigger zones with the ball, you should see "Game Complete!" in the Console. However, if you did not assign the triggers properly, your game might have crashed; at the very least, it will have output lots of error messages to the Console. That's because the "GameManager" script does not check for null operators. There are a number of ways to fix that, but those are left as an exercise (you may wish to research C#'s Null-Conditional Operator and Null reference types).
Additionally, imagine you had thousands of trigger zones - in that instance, it would not be a good idea to individually reference each of them. Better would be to use lists and one of Unity's GameObject.Find methods (such as FindGameObjectsWithTag). However, again, that is left as an exercise. Besides, in the initial stages of coding a game or application, the belt and braces approach, such as that used above, is often a great starting point - the code can always be optimised later.
Now, you should introduce a timer into "GameManager". For that, you can use the Update method (which runs once per frame) to accumulate Time.deltaTime, which is the interval in seconds from the last frame. Additionally, make use of a boolean variable to check whether the game is complete. Below are the updates to the script.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
[SerializeField] private GameObject leftTrigger;
[SerializeField] private GameObject rightTrigger;
[SerializeField] private GameObject frontTrigger;
[SerializeField] private GameObject backTrigger;
private Zones leftTriggerScript;
private Zones rightTriggerScript;
private Zones frontTriggerScript;
private Zones backTriggerScript;
private float runTime = 0;
private bool isComplete = false;
// Start is called before the first frame update
void Start()
{
leftTriggerScript = leftTrigger.GetComponent<Zones>();
rightTriggerScript = rightTrigger.GetComponent<Zones>();
frontTriggerScript = frontTrigger.GetComponent<Zones>();
backTriggerScript = backTrigger.GetComponent<Zones>();
}
// Update is called once per frame
void Update()
{
if (!isComplete) {
runTime += Time.deltaTime;
if (leftTriggerScript.HasTriggered &&
rightTriggerScript.HasTriggered &&
frontTriggerScript.HasTriggered &&
backTriggerScript.HasTriggered)
{
isComplete = true;
Debug.Log("Game Complete at " + runTime.ToString("##.##"));
}
}
}
}Now, if you press play in the Toolbar, after you visit all four trigger zones with the ball, you should see "Game Complete at (number of seconds)" in the Console.
The final element for the game is to output some text to the screen so that the player knows what's going on. To do that, you should introduce some text elements onto the screen. So, in the Hierarchy right-click > UI > Text - TextMeshPro; a TMP Importer dialogue will appear and you should click on the Import TMP Essentials button, as well as the Import TMP Examples & Extras button. Then close the dialogue box. Rename the Text (TMP) GameObject to "Game Time", and in its Inspector, change the default text from "New Text" to "Time: 0". Create another Text - TextMeshPro GameObject under the Canvas element, and call it "Game Complete". Change its default text to "Finished!".
If you zoom out of the scene, you can see the whole canvas and position the two TextMeshPro GameObjects on it. Figure 15 shows them positioned in the centre of the screen, above the arena.
Figure 15: Postioned UI Elements
Finally, you need to reference and update those TextMeshPro GameObjects in the "GameManager" script, and in its Inspector , drag the "Game Time" GameObject into the serialised field "Time", and the "Game Complete" GameObject into the serialised field "Complete". The finished script is below.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using System;
public class GameManager : MonoBehaviour
{
[SerializeField] private GameObject leftTrigger;
[SerializeField] private GameObject rightTrigger;
[SerializeField] private GameObject frontTrigger;
[SerializeField] private GameObject backTrigger;
[SerializeField] private TMPro.TextMeshProUGUI time;
[SerializeField] private TMPro.TextMeshProUGUI complete;
private Zones leftTriggerScript;
private Zones rightTriggerScript;
private Zones frontTriggerScript;
private Zones backTriggerScript;
private float runTime = 0.0f;
private bool isComplete = false;
private String timePreText = "Time: ";
// Start is called before the first frame update
void Start()
{
leftTriggerScript = leftTrigger.GetComponent<Zones>();
rightTriggerScript = rightTrigger.GetComponent<Zones>();
frontTriggerScript = frontTrigger.GetComponent<Zones>();
backTriggerScript = backTrigger.GetComponent<Zones>();
time.text = timePreText + "00.00";
complete.text = "";
}
// Update is called once per frame
void Update()
{
if (!isComplete) {
runTime += Time.deltaTime;
time.text = timePreText + runTime.ToString("##.##");
if (leftTriggerScript.HasTriggered &&
rightTriggerScript.HasTriggered &&
frontTriggerScript.HasTriggered &&
backTriggerScript.HasTriggered)
{
isComplete = true;
complete.text = "Finished!";
}
}
}
}If you've done everything correctly, you should have a game that runs something like that shown in Figure 16, below.
Figure 16: The finished game
You've made substantial changes, so save the project.
The finished game may not win any design awards; furthermore, the "Game Manager" script is responsible for too much - in a real game, you would want to consider seperating game logic from UI, perhaps using an Observer Pattern (which you can read about in the document on scripting). However, the game above serves as a good introduction to building games in Unity, and it's a fine basis upon which to build. So, now you know how, go fulfill your creative urge and start building more games!
- Back Face Culling
- Z-fighting
- Time.deltaTime
- The Unity Roll-a-ball tutorial is another nice introduction in how to move rigidbodies in a simple game. If you want to do that, create a 3D Core project and call it "Rollaball" and then follow the Roll-a-Ball tutorial.
















