We’ve come a long ways, but there is still one major thing we’re missing. We don’t have a real game because there is no way to win or lose! In this lesson, we will learn about Unity events, and how to compose them together so that we can have victory and loss conditions.
Continue following along with your existing “Breakout” project. Otherwise, you can start from this sample project here.
Create A Hole Script
Create a new C# Script named “Hole”, which we will use to trigger a lose condition where the ball reaches the bottom of the screen. Copy the code below and save the script.
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; public class Hole : MonoBehaviour { public UnityEvent fallEvent; void OnCollisionEnter2D(Collision2D collision) { fallEvent.Invoke(); } }
About The Hole Script
As far as scripts go, this one is quite simple, still, there are several things to point out.
using UnityEngine.Events;
This is the first time we have needed to add a new “using” statement. Some of the “vocabulary” we will be using is located in a “namespace” that wasn’t included by default.
public UnityEvent fallEvent;
Here we have added a new field named “fallEvent”. It has a data type of “UnityEvent” which is what required us to add the new namespace above. This special type of data will allow us to connect an “event” to a “handler”, where we can create the connection via the Unity Editor rather than in code. This makes code very composable and flexible.
void OnCollisionEnter2D(Collision2D collision) { fallEvent.Invoke(); }
We’ve seen “OnCollisionEnter2D” before. Here, we use it to “Invoke” our event. You can think of a Unity Event as a reference to another script’s method, and invoking the event as actually running that method.
Update the Block Script
To trigger a win, we need to clear all of the Blocks from the screen. To know when this has happened, we will use more events. Add the appropriate “using” statement.
using UnityEngine.Events;
Next, add an event field named “destroyedEvent” inside the class after the other fields.
public UnityEvent destroyedEvent;
We will invoke the event just before destroying the game object, inside of the “else” condition of the “OnCollisionEnter2D” method.
destroyedEvent.Invoke();
Update the Board Script
Since our Board knows how many blocks are created, it is a great place to go to know when ALL of the blocks are removed. When all blocks are removed, we will then post yet another event. Like before, start by adding the using statement.
using UnityEngine.Events;
Add two new fields, one to keep track of the number of blocks, and another for our event:
int blocksRemaining; public UnityEvent boardClearedEvent;
After the outer “for” loop’s closing bracket, and just before the “Start” methods closing bracket, we will assign the initial value for “blocksRemaining” which is calculated by the number of rows and columns.
blocksRemaining = rows * columns;
Next, we will add a new custom method called “BlockDestroyed” that should be called from the event of a block getting destroyed.
void BlockDestroyed() { blocksRemaining--; if (blocksRemaining == 0) { boardClearedEvent.Invoke(); } }
Each time the above method is triggered, we decrement the number of “blocksRemaining”. Then we use an “if” condition to check and see whether or not the total number of blocks has reached zero. Note that in C# a double equal sign means “is equal to” whereas a single equal sign actually performs an assignment. When we are out of blocks, we trigger our own new event.
I mentioned earlier that we could connect events through a Unity Inspector, but it is also worth pointing out that they can be connected in code as well. Inside our inner “for” loop, just after we set a block’s health based on the row, add the following:
block.destroyedEvent.AddListener(BlockDestroyed);
Now, anytime a Block invokes its own “destroyedEvent”, our Board’s “BlockDestroyed” method will be run.
Create A Game Script
Create a new C# Script named “Game”. We will use this script as a place to manage when the game is actually won or lost, and what to do as a result. Copy the following code and save the script.
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class Game : MonoBehaviour { [SerializeField] GameObject ball; public void Win() { StartCoroutine("Restart"); } public void Lose() { StartCoroutine("Restart"); } IEnumerator Restart() { Destroy(ball); yield return new WaitForSeconds(1); SceneManager.LoadScene("SampleScene"); } }
About the Game Script
What new things do you notice this time?
using UnityEngine.SceneManagement;
We needed to add another new “using” statement. This one is necessary for us to load new levels. That could be a special scene, like a Game Over screen, or a Main Menu, but in this case, we will simply reload the same scene, so the user can play again.
[SerializeField] GameObject ball;
This field will hold a reference to the Ball GameObject. When a game-ending scenario occurs, we will destroy the Ball, so that no other scenario will also occur. For example, if we have just cleared all blocks, we don’t want to risk the Ball then dropping to the bottom and triggering a loss.
public void Win() { StartCoroutine("Restart"); } public void Lose() { StartCoroutine("Restart"); }
At the moment, the “Win” and “Lose” methods are redundant. I decided to leave them as placeholders so that it would be more obvious where to add additional functionality. Perhaps you will want to show a message on screen, or route to a particular scene based on which scenario is triggered. For now, either scenario will simply start a coroutine. You can think of this as running a method over time, instead of all at once.
IEnumerator Restart() { Destroy(ball); yield return new WaitForSeconds(1); SceneManager.LoadScene("SampleScene"); }
This is the code that will be run over time. When “Restart” is triggered, it will immediately start executing its statements from top to bottom until it reaches a “yield”. Then it will wait as-needed, then resume from where it left off when the yield is satisfied.
In this case, the Ball is destroyed right away, then we wait a second before reloading the scene, so that the change is not quite as jarring.
Create A Game Object
Head back over to Unity and create a new Empty GameObject named “Game”.
Attach the Game Script as a component.
In the Inspector, click the target icon next to Ball, and select the Ball from within the Scene tab.
For the sake of additional organization, I like to make the Game the parent of all GameObjects which I consider part of the Game. That includes the Ball, Background, Walls, Paddle, and Board. Note that this step makes no difference in functionality.
Attach The Hole
Select the Bottom Wall from the Hierarchy window.
Add the Hole script.
The “Fall Event” will appear in the Inspector with an empty list of observers. Click the “+” button to add a new observer entry.
A new entry appears in the list. We can click the target icon next to the object picker and then choose our Game object from the Scene tab.
Next click the pull-down that says “No Function”. Then choose “Game -> Lose ()”.
We have now successfully connected the Hole’s fall event to the Game’s Lose method, without code.
Connect the Board Cleared Event
Now we just need to finish by attaching an event that leads to a victory. Select the Board, add a new event entry to the list, choose the Game Object as the target, and then choose “Game -> Win ()” as the function. Review the above section if you get stuck at any of those steps.
Try It Out
It has been a long journey, and now it’s time to enjoy the fruits of our labor. Play the game! While you are at it, verify that both a win and loss are triggered at the appropriate time.
Feel free to experiment on your own from here. Should the Ball move faster? Maybe a speed of 5 is more fun than a speed of 3?
Are there other features you would like to add? Maybe new block types that spawn power-ups? Perhaps you could use more than one ball?
Are there any issues that need polish? Sometimes when I play, I encounter scenarios where physics interactions cause the ball to move sideways, making it feel as if the game is stuck. How would you prevent that from happening?
Should you be able to “aim” somehow? Perhaps you can control the angle of bounce for the ball from the paddle based on where on the paddle it hits.
I hope I have helped to both enable and inspire. Good luck!
Summary
In this lesson we wrapped up the rest of the project by making sure that the game had conditions to lead to a win or lose scenario. We learned how to load scenes, run code over time, and how to create and connect with Unity events, both in code and in the editor.
If you’d like you can also download the completed project for this lesson here.
If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!
Hey! So glad you’re back.Will you do more unity tutorial about Tactics RPG?
Thanks :). In my spare time I have been making a new project that is based on a D20 style rpg. I bet it could be helpful to you.
Of course!I’m really glad to hear it!I hope I can see it in the near future.Look forward to your good news!!!