6. Making a Kart-Racing Game 101
6.5 Drawing the In-Game User Interface
The instance of the material is unique to that renderer, which means that changing its texture will only change the texture on that one renderer, not all of them. (To change the original material, you would need to use renderer.sharedMaterial, although this is not a recommended practice and Unity recommends that you stick to changing only individual materials.)
Once the function is in place to set our car to a random texture, all that is left to do is make a call to the function upon initialization. This now happens in the Start() function of the Car_Controller.js script, the final line of the function being:
setCarTexture();
6.5 Drawing the In-Game User Interface
The UI (shown in Figure 6.8) is a mix of GUIText objects and some graphics drawn from the OnGUI function within the Game_Controller.js script. In the level scenes, all of the GameObjects and components for UI can be found as children of the UI empty GameObject in the scene:
y count_1 y count_2 y count_3 y lap y position y race_Complete y wrong_way
Figure 6.8. The game HUD (Heads Up Display) in the iWhiskerKarts game.
In all honesty, this is not the way that I would deal with UI on a commercial project.
Time constraints and simplicity’s sake have led to using this GUIText-based UI, but I would normally use many more images and most likely some kind of bitmap font for text display.
Each UI item represents an extra draw call for the engine to deal with, so it’s not exactly the most efficient method either. By using a graphic-based system, it is possible to reduce the draw calls significantly. The easiest way to accomplish this would be to use one of the third-party libraries available from the Unity Asset Store, and, if you are going to go ahead with your own kart racer and you will be demanding more complex UI, that’s exactly the route I would recommend until you are confident enough to build your own sprite management system.
Race position display. The race position indicator is a GUIText object. All that happens is that the car controller script calls on the game controller to update it, and the game controller simply resets the text of the GUIText object to reflect the current position:
function updateRacePositionText() {
posText.text="Pos "+focusPlayerRacePosition.ToString()+" of "+numberOfRacers;
}
Race start counting in. There are three GUIText GameObjects used to display the count- down (which is displayed just before the race starts):
y Count_1, y Count_2, and y Count_3.
Open up the Game_Controller.js script in the “iWhiskerKarts – Complete” example project and look at the Start() function. Near the bottom of that function is where we deal with the countdown. I am still quite surprised by how simple this is to implement.
// start the game in 3 seconds from now Invoke("StartRace",4);
When the game controller script has finished creating the players and setting every- thing up for the race, the actual race doesn’t start right away. It starts four seconds from the execution of the Start() function, as set up by the Invoke("StartRace.,4);
statement.
// update positions throughout the race, but we don't need to do this every // frame, so just do it every half a second instead
InvokeRepeating("updatePositions",0.5,0.5);
// hide our count in numbers hideCount();
6.5. Drawing the In-Game User Interface 169
The InvokeRepeating statement makes repeat calls like this an absolute breeze to do. We don’t need to check and update the race position display every single update or frame, so we schedule the repeating Invoke call to update the race positions every half second instead.
The hideCount() function is called next, which hides all of the GUIText objects.
(We’ll look at the actual hide and show functions a little later in this section.)
// schedule count in messages Invoke("showCount3",1);
Invoke("showCount2",2);
Invoke("showCount1",3);
Invoke("hideCount",4);
Yes, dealing with the countdown is really that simple! We just schedule calls to each function at one-second intervals until we reach the fourth second, at which point we make another call to hideCount() to hide everything as the race will be starting (remember that the call to StartRace() was scheduled to happen at four seconds).
// hide final position text
finalPositionText.gameObject.active=false;
doneFinalMessage=false;
// start by hiding our wrong-way message UpdateWrongWay(false);
}
The other messages in our UI are incidental and only need to be active (displayed) when required. Here, we hide both the final position message and the wrong-way message.
Later in the function, as the race completes, we will be setting the text of the final posi- tion message object (finalPositionText). We only need to do this once, so we have a simple Boolean flag called doneFinalMessage to make sure that happens.
Although Boolean variables will return false by default, I like to set them to false in initialization functions anyway. One reason for this is that we may, in the future, end up reusing the initialization functions to reset the game state (although in this particular game we don’t), so it’s good practice to keep things tidy in case we need them later.
If you’re wondering how those show and hide functions work, it’s really simple.
Here are all of the functions we use for the countdown:
function showCount1() {
count1.active=true;
count2.active=false;
count3.active=false;
}
function showCount2() {
count1.active=false;
count2.active=true;
count3.active=false;
}
function showCount3()
{
count1.active=false;
count2.active=false;
count3.active=true;
}
function hideCount() {
count1.active=false;
count2.active=false;
count3.active=false;
}
Setting a GameObject’s active property to false both hides it from the scene and stops the regular update calls to any components attached to it. If other scripts call functions on those inactive scripts, the functions will still run, but automatically up- dated functions such as Update, FixedUpdate, or LateUpdate will not be called by the system.
Wrong-way indicator. The wrong-way indicator takes the form of a simple GUIText object. The game controller takes care of showing or hiding it whenever it detects that the angle of the player to its next waypoint is too far in either direction to be the right way (as seen in Section 6.2.4; see Figure 6.9).
The last time we looked at the wrong-way detection in the Car_Controller.js script, we left it having a simple Debug.Log message when the wrong way was detected, but in the final version, you will see this in its place (just under halfway down through the script):
if(oldWrongWay!=goingWrongWay) {
// we need to update the UI
gameControl.UpdateWrongWay(goingWrongWay);
}
Figure 6.9. A wrong-way text object in the kart-racing game UI.
6.5. Drawing the In-Game User Interface 171
As you can see, when the state of whether or not we are going the wrong way changes, we make a call to the game controller’s UpdateWrongWay() function to tell it what the current state is. Our game controller takes care of the display within that function, like this:
public function UpdateWrongWay(isWrongWay : boolean) {
if(isWrongWay) {
wrongWaySign.SetActiveRecursively(true);
} else {
wrongWaySign.SetActiveRecursively(false);
} }
SetActiveRecursively is used to make GameObjects inactive or active, which may or may not have child objects under them. When you use SetActiveRecursively, it will make the specified object and all its child objects inactive (meaning they’re no longer visible or running scripts).
Since we are referring to a GameObject, rather than a particular type of object like a text object or a graphic, the GameObject could take any form you like. If you wanted to use a GUITexture, you could. Or if it was a script that used OnGUI (not advisable be- cause it will slow performance), you could. Or if you were using a separate third-party library to display 2D images, you could, and our code would stay the same. The type of object doesn’t matter when we use SetActiveRecursively; it will just make the GameObject and its children inactive regardless.
Final race result message. At the end of the race, our users will want to find out which position they came in at. That’s why we put up a huge text message to tell them if they finished in first, second, or third position. Anything after that just gets a “Race finished”
message, instead.
That final result message is a single GUIText object that gets its text set at the end of the race by the game controller script. Each time the LapCompleted() function is called, it checks to see if the race has finished. (We discussed this in Section 6.2.3 when we dealt with player lap counting.) If you open up that script to take a look at it, scroll down to the section that deals with the final race result message:
if(theLaps==totalLaps) {
// race is finished! tell this car to stop now aScript.setUserInput(false);
aScript.setAIInput(true);
if(finalPosition==1)
finalPositionText.text="FINISHED 1st";
if(finalPosition==2)
finalPositionText.text="FINISHED 2nd";
if(finalPosition==3)
finalPositionText.text="FINISHED 3rd";
if(finalPosition>=4)
finalPositionText.text="FINISHED";
finalPositionText.gameObject.active=true;
doneFinalMessage=true;
Invoke("finishRace",10);
} }
}
In this code, we look to see if this player has finished the race by comparing theLaps to totalLaps. If this is true and the player has completed the right number of laps, it’s time to display that final message and bring the race to a close. This couldn’t be simpler, as we just check the finalPosition and set the text appropriately:
if(finalPosition==1)
finalPositionText.text="FINISHED 1st";
Since finalPositionText is a GUIText object, to make it active and visible onscreen we need to quickly access its GameObject and set the active property:
finalPositionText.gameObject.active=true;
Pedals. In the final version of our karts game, we have two pedals (one on either side of the screen) to show players where to press to accelerate and brake. The images are .png format, stored in the “2DAssets” folder of the project. They don’t animate or do anything fancy, but they do provide users with a clue as to where to put their thumbs to make the karts go and stop. The two public variables, acceleratePedalGraphic and brakePedalGraphic, are set in the editor to reference our pedal images.
Using OnGUI is not recommended at all for Unity iOS due to how processor-inten- sive it is. That said, we only need to display two images, and our game is hardly pushing the limits of the device right now, so I would say that as long as we don’t see a huge impact on the frame rate, it’s okay to use.
Note that you should test your projects on older devices before making this con- clusion! Just because it doesn’t affect things on the iPad 2 doesn’t mean that a second- generation iPhone won’t crumble under the pressure. Of course, regardless of how you draw your UI, you should test on as many different devices and types of devices as possible!
public var acceleratePedalGraphic : Texture2D;
public var brakePedalGraphic : Texture2D;
function OnGUI() {
// okay, so this breaks the golden rule of iOS Unity dev, but using // OnGUI means that we can easily resize the GUI to fit different // resolutions. we are only using it for two UI elements, so the // performance impact (hopefully) won't grind the game to a // complete halt
// first, enter the matrix to resize the GUI
GUI.matrix = Matrix4x4.TRS (Vector3.zero, Quaternion.identity,
6.5. Drawing the In-Game User Interface 173
new Vector3(Screen.width / 480f, Screen.height / 320f, 1));
GUI.DrawTexture(Rect(25,240,32,64),brakePedalGraphic);
GUI.DrawTexture(Rect(430,240,32,64),acceleratePedalGraphic);
}
Our code starts out by setting up GUI.matrix. This is a 4 × 4 transforma- tion matrix that, in the case of GUI, can be used to automatically resize everything drawn within the OnGUI() function to suit whatever screen resolution or aspect ratio is required. I am not even going to attempt to ex- plain 4 × 4 transformation matrices—if you need to know that, please buy a math textbook. All we need to know is that we put in the desired width and height and the UI will automatically be proportionally scaled based on the correct proportion at that resolution. For example, non-retina iPhone resolution (in landscape view) is 480 × 320 pixels. Figures 6.10 and 6.11 show the iPhone landscape versus the iPhone portrait views and how it affects the scaling of the UI.
Again, if the game were scaled to any other resolution, the UI would appear in the same place proportionally, although the width and height of the elements would also be stretched proportionally, result- ing in sometimes ugly results. On the other hand, if the aspect ratio doesn’t change too much, it’s a nice, quick way to deal with multiple resolutions!
GUI.DrawTexture(Rect(25,240,32,64),brakePedalGraphic);
Drawing the texture is straightforward, with a call to GUI.DrawTexture. Here, we pass in a Rect object to define the rectangle we are going to draw (with x and y positions and height) and the 2D texture we want to draw (stored in the brakePedalGraphic variable).
Figure 6.10. The HUD stretched by an incorrect aspect ratio.
Figure 6.11. The HUD at normal proportions, as it should look.