Today we look at how to extend our functions beyond the Update loop by allowing them to pause and resume execution using “Coroutines”. We also take our first look at Unity’s UI system, and use these together to create a “Get Ready” state to prepare our players for gameplay. Get Ready!!!
Part 1: Coroutines
In past discussions, I have mentioned that Unity runs through it’s objects and their scripts sequentially. It is a very linear process. When the “awake” message is sent out, Unity goes down the list of objects that it maintains (and that you will not control) and one by one passes it the “awake” message. That message is passed to each one of that object’s components, and if that object has an Awake( ) function it is executed, line by line. Once every object has received the “awake” message, the “start” message is called prior to rendering the next frame, and Unity again goes down the list, object by object, component by component, and any corresponding function will execute.
When a function is called, it must run until it is completes every line (or until it encounters a “return”, which is a type of completion). If another function is called during that original function, then THAT new function must run to completion before moving to the next line in the original, and so forth and so on.
Using this method, it can be difficult or cumbersome to create functions that execute periodically (such as a timed event), or to occur across a span of time and across many frames (such as a fading transition). Creating and maintaining internal timers can be tricky, and it requires a lot of extra calls.
A coroutine is a function that lets us break out of this pattern by allowing us to pause it’s execution, hand control back to Unity, and then resume later and pick back up where it left off after some measure.
In our game, we would like to create a new state to let the player “get ready” to begin a round. A moment between when the ball is reset, and the action begins. For our first co-routine, we are going to make this moment last for 3 seconds.
We create our function by declaring an IEnumerator. This function will wait for three seconds, and then call the StartBall( ) function.
public IEnumerator GetReady() { // tell the engine to come back in three seconds yield return new WaitForSeconds(3.0f); // resumes after the pause StartBall(); }
Next we call this by replacing the StartBall( ) function call in InitRound( ) with the StartCoroutine( ) function, passing our coroutine’s call as the parameter.
private void InitRound() { // Reset Ball ResetBall(); // start the coroutine StartCoroutine(GetReady()); }
Now when we run this, our ball sits there for three seconds and then begins to move.
Part 2: Basic UI
Three seconds can be a long time, and we don’t want our player to get confused, so first we should build a message to the player letting them know that now is the time to Get Ready!!!
To do this, we create a new text object by going to Game Object > UI > Legacy > Text or Create > UI > Legacy > Text.
Make sure to use the “Legacy” Text objects for this assignment. In a future assignment we will look at TextMeshPro, which is a more advanced (and thus more complicated) system.
When you do this, you will see a few new objects appear in your scene, including a Canvas object, and an EventSystem object. We will cover the UI system more in a future lesson, for now just know that the Canvas is the two-dimensional space where UI elements live, and renders as an overlay for the active camera.
One change you will definitely want to make now is in the Canvas object. Look for the Canvas Scaler component and change the UI Scale Mode to Scale with Screen Size. This ensures that your UI elements will fill the same proportions of the screen whether it is in the Game window or full screen.
Select the text object that you have created and change the name. We used myOverlayTextObject (because we will use this for more than one message). In the Text component, change the Text field (a longform text area, and yes, text inside text is redundant) to read something besides “New Text”. I chose “Hello World”. Set your Alignments to “Center” and “Middle”, and set your Horizontal and Vertical overflow values to Overflow. Finally, adjust the font size until you have something that looks good to you.
Next, go to the EasyPing script, and add a Text object variable to hold a reference to our text object. UI elements have their own Unity library, which we have to call first by adding the following line to the “using” commands at the very top:
using UnityEngine.UI;
Without this line, your calls to get and set text objects will return errors because Unity does not recognize that you will be using them.
Next, create the text object to hold the message text, and use our drag-and-drop method in the Unity editor to assign the text object to this field.
// define text objects public Text overlayMessage;
Now inside of the Start( ) function, we set our text message to read “Get Ready!!!”, and then set the enabled property to false so that it will not display until we tell it to.
// set the text message to read "Get Ready!!!" overlayMessage.text = "Get Ready!!!"; overlayMessage.enabled = false;
We want to make our message display while the GetReady coroutine is running, and turn off again when the stage is over and we move into the Playing state. We do this by adjusting the text object’s enabled property like so:
public IEnumerator GetReady() { overlayMessage.enabled = true // turn the message visibility on // tell the engine to come back in three seconds yield return new WaitForSeconds(3.0f); overlayMessage.enabled = false // turn the message visibility off // resumes after the pause StartBall(); }
Now we should see the message appear for 3 seconds, then disappear as the ball starts moving.
Part 3: Using the Renderer
Three seconds still feels like a long time, doesn’t it? Players might still think our game is broken or stalled. And just when they let their guard down, the ball shoots off!
Let’s help show our players that something is happening, and help them anticipate the beginning of the round. We will do this by making the ball and the message blink.
To achieve this, you might be tempted to the enabled property of the ball object, but don’t do that! Making the ball inactive can destroy the connections, and reactivating it will run the Start( ) command all over again. Instead, we want to manipulate only the visibility of the ball, which we do by enabling and disabling the Mesh Renderer component.
In our EasyPing declarations, we create a new private variable of the type Renderer, like so:
private Renderer ballRender;
… then in our Start( ) method we assign the Renderer component like this:
ballRender = ballObject.GetComponent<Renderer>();
Finally, we modify our GetReady( ) script to enable and disable the Renderer component, alternated with our overlay message. We will display one configuration for a half second, and then the other for another half second. The use of a for loop lets us cycle through this three times. Once the loop completes, StartBall( ) is called and our Coroutine ends.
public IEnumerator GetReady() { for (int i=0; i < 3; i++) { // turn on message, turn off the ball overlayMessage.enabled = true; ballRender.enabled = false; // wait for 1/2 second yield return new WaitForSeconds(0.5f); // turn on ball, turn off the message overlayMessage.enabled = false; ballRender.enabled = true; // wait for another 1/2 second yield return new WaitForSeconds(0.5f); } StartBall(); }
Now our ready message and ball alternate in quick half-second blinks that let the player get a good sense of the timing that is coming.
With the extra time leftover at the end of class, we also added a “none” state, and changed the initial message to “Press ‘S’ to Start”. We made sure that the Update loop only responded to the S key when in the “menu” state, and from there called InitRound( ).
Now we’ve covered everything that is needed to complete Assignment #2!
EasyPong.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public enum GameState { none, menu, setup, getReady, playing, gameOver}; public class EasyPong : MonoBehaviour { // ball object and components public GameObject ballObject; private Rigidbody ballRigidbody; private Renderer ballRender; public Vector3 direction; public float force; // define my gamestate variable public GameState myState = GameState.none; // score tracking private int playerOneScore; private int playerTwoScore; private int MAX_SCORE = 3; // ball start position private Vector3 ballStartPosition; // UI elements public Text overlayMessage; // Start is called before the first frame update void Start() { // connect to the ball ballRigidbody = ballObject.GetComponent<Rigidbody>(); // access the renderer ballRender = ballObject.GetComponent<Renderer>(); // reset the score playerOneScore = 0; playerTwoScore = 0; // set the ball position ballStartPosition = ballObject.transform.position; // set the overlay message overlayMessage.text = "Press 'S' to Start..."; overlayMessage.enabled = true; // put us in the menu state myState = GameState.menu; } private void Update() { if (myState == GameState.menu) { if (Input.GetKeyDown(KeyCode.S)) { // kick off my game InitRound(); } } } private void InitRound() { Debug.Log("InitRound Called"); // update the game state myState = GameState.setup; // Reset the ball position ResetBall(); // coroutine that delays 1 second then starts the ball StartCoroutine(GetReady()); } public IEnumerator GetReady() { // set and activate overlay overlayMessage.text = "Get Ready!!!"; // put us in the getReady state myState = GameState.getReady; for (int i=0; i < 3; i++) { // turn on message, turn off the ball overlayMessage.enabled = true; ballRender.enabled = false; // wait for 1/2 second yield return new WaitForSeconds(0.5f); // turn on ball, turn off the message overlayMessage.enabled = false; ballRender.enabled = true; // wait for another 1/2 second yield return new WaitForSeconds(0.5f); } // resume after the pause StartBall(); } private void ResetBall() { Debug.Log("ResetBall Called"); // make sure we have control of the ball, not physics. ballRigidbody.isKinematic = true; // position the ball ballObject.transform.position = ballStartPosition; } private void StartBall() { Debug.Log("Start Ball called"); // turn kinematics off, let physics control the ball ballRigidbody.isKinematic = false; // fix the direction vector direction.Normalize(); // push the ball ballRigidbody.AddForce((direction * force), ForceMode.VelocityChange); // Enter the playing state myState = GameState.playing; } public void RegisterScore(int playerNumber) { // stop the action ballRigidbody.isKinematic = true; // process the score if (playerNumber == 1) { playerOneScore++; // increment player one's score } else if (playerNumber == 2) { playerTwoScore++; // increment player two's score } else { Debug.Log("Player Number was not assigned correctly: " + playerNumber); } // display the score Debug.Log("The Score is: " + playerOneScore + " - " + playerTwoScore); // stop the gameplay myState = GameState.setup; // start the next round InitRound(); } }