image05

Unity 5 – Incrementing Mvmt., Turns, Win State

This tutorial demonstrates incremental movement, turns, cheating prevention, win conditions, and an end screen.

ABOUT THE AUTHOR

Jonathan Cecil is a visual artist working and living in Los Angeles. His work focuses on the reconstitution and transformation of media through digital processes. He graduated from University of California Santa Barbara with a BA in Studio Art in 2000, and is currently pursuing an MFA in Design | Media Art at UCLA.

Overview
This is part 5 of the board game tutorials and will build off the project created in part 4. In part 1, we created a rolling six-sided die, and in part 2 we added some scripts to read the value of the die and display on screen, in part 3 we made a responsive board, and in part 4 we used timers to fix our die. If you haven’t looked at parts 1, 2, 3, and 4, you should probably go back and do that now. In this fifth tutorial we will extend our scripts to provide a more game-like experience.

This tutorial was made for Unity 3.0 and assumes that the user has at least watched the first few Unity tutorial videos. For this tutorial,
download the base Unity project, or build a new, bigger, circular game board in Unity. For this tutorial we will use a circular pond board with 16 tiles.

This tutorial attempts to be more than a list of steps to follow; I tried to record some of the reasons for why I make choices. If you don’t want to follow my logic and just want steps, look at the stuff in bold. At the end of each section is a list of the new Game Objects, Components or Functions covered.

We left off the last tutorial using timers to help improve functionality and keep track of total time spent playing our game. For this tutorial, we will build some game logic into the scene that will allow us to move tile-by-tile, detect when play has started and when a “turn” has started, and determine whether the player “wins.”

The state of the scene and game goals
The scene should look familiar. If you are using the base project, you should see a new bigger pond GameObject; it has tiles and triggers like before, with embedded targets for our PlayerPiece to move toward. There are now three different textures for the pond to help differentiate between the different tiles of the circular board. The timer is counting time as it did before, and the die roll value is showing in another corner of the screen. The only difference between this scene and the scene from tutorial four is the bigger pond with more tiles.

If you run the scene, the player piece should move as before, from tiles one through six, but that’s not exactly a game. Our game should have a goal: to move a number of spaces indicated by a die roll until the player piece has made a lap of the track.

Making a player piece race around a track
The biggest problem so far is that our PlayerPiece doesn’t move around the track at all, it just moves
between tiles one through six depending on what die value is rolled. We need a place to keep a running total of which tile to move to, something we can add a dice roll to. Because the MovePlayer script is already handling movement, that seems like a good place to start.

Alter the MovePlayer script by adding to the top:

 

public var currentLocation = 1;


This will allow us to save the location of the tile that the PlayerPiece should move to next. Now update the part that finds the next target to examine the currentLocation rather than the current dieValue. In the Update function, just before we look for the new tileGameObject, add the current die roll to the currentLocation:

 

currentLocation += dieValueComponent.currentValue;


And just below that, change:

 

tileGameObject = GameObject.Find(“tile” + dieValueComponent.currentValue.ToString() +
“target” );


to this:

 

tileGameObject = GameObject.Find(“tile” + currentLocation + “target” );


That allows the target to update to the currentLocation, and since we are adding the dieValue back to the currentLocation, we get a cumulative total that can be greater than 6. Now the PlayerPiece should be able to advance until its cumulative total is equal to 16 (after which you’ll get an error that we will address in a second).

First we need our Player Piece to start play on tile 1, so set that up properly in the Start() function of the MovePlayer script:

 

// look for the starting position and move there

tileGameObject = GameObject.Find(“tile” + currentLocation.ToString() + “target” );

transform.position = tileGameObject.transform.position;

transform.rotation = tileGameObject.transform.rotation;

target = tileGameObject.transform;


This will find the transform values for the tile target for the default currentLocation, which we set earlier to 1. That is where we want to start play, so we set the PlayerPiece transform.position and
transform.rotation to be the same as the target. Remember that this script is attached to a GameObject, which has a transform component, so when we use transform.position, it refers to the transform for the GameObject this script is attached to.

You can see by running the scene that the movement is not quite right: PlayerPiece makes moves in a straight line to its final destination instead of moving along each tile. To remedy this, add a second location value to the MovePlayer script. This new value will keep track of which tile number is the final target of the cumulative move; we call it targetLocation:

 

public var targetLocation = 1;


and switch the value that gets updated on a new die roll from currentLocation to targetLocation. Replace

 

currentLocation += dieValueComponent.currentValue;


with

 

targetLocation += dieValueComponent.currentValue;


There needs to be a section of code that will update the currentLocation incrementally, so add this block of code onto the end of our if (dist > allowedDistance) statement in the Update function, just before the if (lastDieValue != 0) statement closes:

 

else if (currentLocation != targetLocation) {

// reset the target

currentLocation += 1;

tileGameObject = GameObject.Find(“tile” + currentLocation.ToString() + “target” );

target = tileGameObject.transform;

}


If all goes well, the PlayerPiece should now be moving slowly around the pond track one space at a time.

Integrating “turns” into the scene
Now the PlayerPiece is moving around the track, but it’s super easy to cheat at this game: just rock the die back and forth between the five and six to speed to the end. We want to make sure that the die has to be tossed with the mouse in order to register a valid turn. We can do this in the dieValue script: when the player clicks on the die, the turn begins, and the turn ends when the value is recorded by the MovePlayer script.

In DieValue, use OnMouseUp to reset die value to 0. We can also make a new variable to show whether the turn has begun:

 

private var turnBegun = false;


and a OnMouseUp function:

 

function OnMouseUp() {

currentValue = 0;

turnBegun = true;

}


If we wanted to know whether the game has started, we could add a playstarted boolean, and use
OnMouseUp to start the game. At the top of the script add:

 

private var playStarted = false;

 

and in OnMouseUp():

 

if (!playStarted) playStarted = true;

 

Run the scene to check the results. You should see that the die is still causing a move when dropped on board, which is because we haven’t told the MovePlayer script not to check the dieValue if the turn hasn’t started. Alter the MovePlayer script to check whether turn has started:

 

if (lastDieValue != dieValueComponent.currentValue && dieValueComponent.currentValue != 0 && dieValueComponent.turnBegun) {


Make sure you move these old lines inside this new if() statement:

 

targetLocation += dieValueComponent.currentValue;

currentLocation += 1;

if (currentLocation > 16) currentLocation = 16;

if (targetLocation > 16) targetLocaiton = 16;

tileGameObject = GameObject.Find(“tile” + currentLocation.ToString() + “target” );

target = tileGameObject.transform;

 

and then end the turn in that block:

 

dieValueComponent.turnEnded = false;

}


In the last line we reach into the die value script and turn set the value back to false. This is actually bad programming form because we’re manipulating the insides of another object from this script, but since our scripts are so small, we won’t worry about that too much.

Moving the piece around the track is a good step, but there is a problem when we get near tile 16. If the targetLocation value exceeds 16, Unity gives us a null pointer exception. This happens in Unity when a script has an empty variable, like trying to find a GameObject which doesn’t exist, and the script is trying to access a member function or value. Because the variable doesn’t actually have an associated object (there is no tile17), the value of it is Null, and Unity throws an error. This problem can be solved in our case by making a maximum value of 16 tiles in MovePlayer. In the MovePlayer script, when you increment the targetLocation in Update(), just check the value:

 

if (targetLocation > 16) targetLocaiton = 16;

 

and when you increment the currentLocation, also check the value:

 

if (currentLocation > 16) currentLocation = 16;


This should complete the steps necessary to get the basic movement happening in the scene. Now for the game part.

Setting an end game
The scene still needs a few changes to make it a “game”: let’s define a win situation. Now that the player piece won’t go past space 16, we should stop the clock when we get to the end. Our timer needs a way to stop working, like an on/off switch. Add a new variable to TrackTime to accomplish this:

 

private var timerOn = true;


and to the Update() function add this bit:

 

if (timerOn) {

totalTime += Time.deltaTime;

textMeshComponent.text = totalTime.ToString().Substring(0,4);

}


This will allow the timer to accumulate time only when turned “on”, but it updates the text mesh as
before.

There are a couple ways to indicate the end of game to Unity; we can solely use scripts, or a
combination of scripts and triggers from the physics engine. Since we are using triggers already, let’s just continue with that method. Since we coded the TileTrigger script, we can use that as a base. Add a WinTrigger script and copy the contents of TileTrigger. We just need to change one function – the OnTriggerEnter should look like this:

 

function OnTriggerEnter (other : Collider) {

timerGameObject = GameObject.Find(“TimerText”);

trackTimeComponent = timerGameObject.GetComponent(“TrackTime”);

trackTimeComponent.timerOn = false;

}


Now remove the old TileTrigger component from tile16Trigger and add the new WinTrigger script.
Remember to enter the name of the target tile in the Unity IDE. If you run the scene and move the piece to end of the track, the timer should now stop.

One problem you may notice is that the die can now triggers the win condition if it hits Tile 16, which makes sense because the die is a rigid body and fires trigger events. We could alter the WinTrigger script to look at the name of the other collider (remember the other : Collider passed to the OnTrigger functions). If we added more player pieces in the future, checking for names with multiple pieces might end up being a little clunky.

Unity has a simpler solution for this: Tags. We can define groups in the Unity IDE and access group
information for GameObjects in scripts. Click on the PlayerPiece and find the Tag dropdown, it should be initially set to “Untagged”. Pull down the list and select “Player” from the list of predefined Tags. You could also define your own tag.

Each GameObject has a variable called “tag”, and now the PlayerPiece tag should be set to “Player”. If a second player piece were added, we could also label it with the tag “Player”. The last step for us is to check the tag in OnTriggerEnter of the WinTrigger script:

 

if (other.tag == “Player”) {

timerGameObject = GameObject.Find(“TimerText”);

trackTimeComponent = timerGameObject.GetComponent(“TrackTime”);

trackTimeComponent.timerOn = false;

}

 

The die should no longer trigger the win condition because the die is not labeled with the “Player” tag.

Functions Used in this Section:

GameObject.tag (more info)

Making an end screen
Now that the game is ending when the PlayerPiece hits the last tile, we can start building more exciting feedback events. A win screen would be a great improvement; we’ll use a flat raster image with the words “You Win!”. Open up Adobe Photoshop and make your new win screen. Remember that the screen resolution for the game is 1024×768, so use those pixel dimensions for your size. Save your document as a PNG and drop into your Unity project. Make a new material and give it a descriptive name, like MessageScreenMaterial. Be sure to add the new texture to the material.

Unity provides a GUI system to handle 2D rendering, but we’ll review a technique for making flat
graphics in the 3D space using 3D objects. Unity allows us have multiple cameras in a scene and gives us the ability to switch between them.

Add a new plane to the scene and set the material to the MessageScreenMaterial. Because the image
we created is 1024×768, we can scale the plane to 4 in the x direction, and 3 in the z direction to get the proper proportions (as 1024×768 is a 4:3 image). Position the plane far enough away from the main scene so that it won’t be affected by the lighting. Make a new camera and name it MessageCamera; position it so that it points downward toward the new plane.

If you run the scene, you should no longer see the pond and die, but the win message should be visible. If you set the MessageScreenMaterial shader to “Diffuse”, it is most likely too dark. There is a shader that doesn’t need light to be illuminated and it’s great for 2D graphics: Particle/Alpha Blended. Set material shader to that and run the scene again.

The game isn’t showing properly anymore, and this is because Unity is setting the message camera as the active camera. We change this by telling unity to turn off the Camera component in the camera GameObjects; take a look at the camera in the inspector to see the different components of a Unity camera. We can add a Start() function to the WinTrigger script:

 

GameObject.Find(“MessageCamera”).GetComponent(“Camera”).enabled = false;

GameObject.Find(“Main Camera”).GetComponent(“Camera”).enabled = true;

 

The WinCamera only needs to be active when the last tile is triggered, so we just reverse the camera
settings for the win condition, which is triggered inside the if (other.tag == “Player”) statement in the OnTriggerEnter function:

 

GameObject.Find(“Main Camera”).GetComponent(“Camera”).enabled = false;

GameObject.Find(“MessageCamera”).GetComponent(“Camera”).enabled = true;

 

Now the plane is displayed when the game is “won.”

Functions Used in this Section:

Behaviour.enabled (more info)

We could further extend the message screen to display other textures by simply switching out the
texture on the material with a new one. We could also use the function OnMouseUp to make the
message screen start a new game when clicked on, even starting the game on an Intro screen.

The script is this tutorial is a little more complicated than in previous sections – if you are having trouble with your script, check my script files in the completed version of the project files.