Unity Game Development: Interactions (Part 2)

0
101
13 min read

Opening the outpost

In this section, we will look at the two differing approaches for triggering the animation giving you an overview of the two techniques that will both become useful in many other game development situations. In the first approach, we’ll use collision detection—a crucial concept to get to grips with as you begin to work on games in Unity. In the second approach, we’ll implement a simple ray cast forward from the player.

Approach 1—Collision detection

To begin writing the script that will trigger the door-opening animation and thereby grant access to the outpost, we need to consider which object to write a script for.

In game development, it is often more efficient to write a single script for an object that will interact with many other objects, rather than writing many individual scripts that check for a single object. With this in mind, when writing scripts for a game such as this, we will write a script to be applied to the player character in order to check for collisions with many objects in our environment, rather than a script made for each object the player may interact with, which checks for the player.

Creating new assets

Before we introduce any new kind of asset into our project, it is good practice to create a folder in which we will keep assets of that type. In the Project panel, click on the Create button, and choose Folder from the drop-down menu that appears.

Rename this folder Scripts by selecting it and pressing Return (Mac) or by pressing F2 (PC).

Next, create a new JavaScript file within this folder simply by leaving the Scripts folder selected and clicking on the Project panel’s Create button again, this time choosing JavaScript.

By selecting the folder, you want a newly created asset to be in before you create them, you will not have to create and then relocate your asset, as the new asset will be made within the selected folder.

Rename the newly created script from the default—NewBehaviourScript—to PlayerCollisions. JavaScript files have the file extension of .js but the Unity Project panel hides file extensions, so there is no need to attempt to add it when renaming your assets.

You can also spot the file type of a script by looking at its icon in the Project panel. JavaScript files have a ‘JS’ written on them, C# files simply have ‘C#’ and Boo files have an image of a Pacman ghost, a nice little informative pun from the guys at Unity Technologies!

Scripting for character collision detection

To start editing the script, double-click on its icon in the Project panel to launch it in the script editor for your platform—Unitron on Mac, or Uniscite on PC.

Working with OnControllerColliderHit

By default, all new JavaScripts include the Update() function, and this is why you’ll find it present when you open the script for the first time. Let’s kick off by declaring variables we can utilise throughout the script.

Our script begins with the definition of four variables, public member variables and two private variables. Their purposes are as follows:

  • doorIsOpen: a private true/false (boolean) type variable acting as a switch for the script to check if the door is currently open.
  • doorTimer: a private floating-point (decimal-placed) number variable, which is used as a timer so that once our door is open, the script can count a defined amount of time before self-closing the door.
  • currentDoor: a private GameObject storing variable used to store the specific currently opened door. Should you wish to add more than one outpost to the game at a later date, then this will ensure that opening one of the doors does not open them all, which it does by remembering the most recent door hit.
  • doorOpenTime: a floating-point (potentially decimal) numeric public member variable, which will be used to allow us to set the amount of time we wish the door to stay open in the Inspector.
  • doorOpenSound/doorShutSound: Two public member variables of data type AudioClip, for allowing sound clip drag-and-drop assignment in the Inspector panel.

Define the variables above by writing the following at the top of the PlayerCollisions script you are editing:

private var doorIsOpen : boolean = false;
private var doorTimer : float = 0.0;
private var currentDoor : GameObject;

var doorOpenTime : float = 3.0;
var doorOpenSound : AudioClip;
var doorShutSound : AudioClip;

Next, we’ll leave the Update() function briefly while we establish the collision detection function itself. Move down two lines from:

function Update(){
}

And write in the following function:

function OnControllerColliderHit(hit : ControllerColliderHit){
}

This establishes a new function called OnControllerColliderHit. This collision detection function is specifically for use with player characters such as ours, which use the CharacterController component. Its only parameter hit is a variable that stores information on any collision that occurs. By addressing the hit variable, we can query information on the collision, including—for starters—the specific game object our player has collided with.

We will do this by adding an if statement to our function. So within the function’s braces, add the following if statement:

function OnControllerColliderHit(hit: ControllerColliderHit){
if(hit.gameObject.tag == "outpostDoor" && doorIsOpen == false){
}
}

In this if statement, we are checking two conditions, firstly that the object we hit is tagged with the tag outpostDoor and secondly that the variable doorOpen is currently set to false. Remember here that two equals symbols (==) are used as a comparative, and the two ampersand symbols (&&) simply say ‘and also’. The end result means that if we hit the door’s collider that we have tagged and if we have not already opened the door, then it may carry out a set of instructions.

We have utilized the dot syntax to address the object we are checking for collisions with by narrowing down from hit (our variable storing information on collisions) to gameObject (the object hit) to the tag on that object.

If this if statement is valid, then we need to carry out a set of instructions to open the door. This will involve playing a sound, playing one of the animation clips on the model, and setting our boolean variable doorOpen to true. As we are to call multiple instructions—and may need to call these instructions as a result of a different condition later when we implement the ray casting approach—we will place them into our own custom function called OpenDoor.

We will write this function shortly, but first, we’ll call the function in the if statement we have, by adding:

OpenDoor();

So your full collision function should now look like this:

function OnControllerColliderHit(hit: ControllerColliderHit){
if(hit.gameObject.tag == "outpostDoor" && doorIsOpen == false){
OpenDoor();
}
}

Writing custom functions

Storing sets of instructions you may wish to call at any time should be done by writing your own functions. Instead of having to write out a set of instructions or “commands” many times within a script, writing your own functions containing the instructions means that you can simply call that function at any time to run that set of instructions again. This also makes tracking mistakes in code—known as Debugging—a lot simpler, as there are fewer places to check for errors.

In our collision detection function, we have written a call to a function named OpenDoor. The brackets after OpenDoor are used to store parameters we may wish to send to the function—using a function’s brackets, you may set additional behavior to pass to the instructions inside the function. We’ll take a look at this in more depth later in this article under the heading Function Efficiency. Our brackets are empty here, as we do not wish to pass any behavior to the function yet.

Declaring the function

To write the function we need to call, we simply begin by writing:

function OpenDoor(){
}

In between the braces of the function, much in the same way as the instructions of an if statement, we place any instructions to be carried out when this function is called.

Playing audio

Our first instruction is to play the audio clip assigned to the variable called doorOpenSound. To do this, add the following line to your function by placing it within the curly braces after { “and before” }:

audio.PlayOneShot(doorOpenSound);

To be certain, it should look like this:

function OpenDoor(){
audio.PlayOneShot(doorOpenSound);
}

Here we are addressing the Audio Source component attached to the game object this script is applied to (our player character object, First Person Controller), and as such, we’ll need to ensure later that we have this component attached; otherwise, this command will cause an error.

Addressing the audio source using the term audio gives us access to four functions, Play(), Stop(), Pause(), and PlayOneShot(). We are using PlayOneShot because it is the best way to play a single instance of a sound, as opposed to playing a sound and then switching clips, which would be more appropriate for continuous music than sound effects. In the brackets of the PlayOneShot command, we pass the variable doorOpenSound, which will cause whatever sound file is assigned to that variable in the Inspector to play. We will download and assign this and the clip for closing the door after writing the script.

Checking door status

One condition of our if statement within our collision detection function was that our boolean variable doorIsOpen must be set to false. As a result, the second command inside our OpenDoor() function is to set this variable to true.

This is because the player character may collide with the door several times when bumping into it, and without this boolean, they could potentially trigger the OpenDoor() function many times, causing sound and animation to recur and restart with each collision. By adding in a variable that when false allows the OpenDoor() function to run and then disallows it by setting the doorIsOpen variable to true immediately, any further collisions will not re-trigger the OpenDoor() function.

Add the line:

doorOpen = true;

to your OpenDoor() function now by placing it between the curly braces after the previous command you just added.

Playing animation

We have already imported the outpost asset package and looked at various settings on the asset before introducing it to the game in this article. One of the tasks performed in the import process was the setting up of animation clips using the Inspector. By selecting the asset in the Project panel, we specified in the Inspector that it would feature three clips:

  • idle (a ‘do nothing’ state)
  • dooropen
  • doorshut

In our openDoor() function, we’ll call upon a named clip using a String of text to refer to it. However, first we’ll need to state which object in our scene contains the animation we wish to play. Because the script we are writing is to be attached to the player, we must refer to another object before referring to the animation component. We do this by stating the line:

var myOutpost : GameObject = GameObject.Find("outpost");

Here we are declaring a new variable called myOutpost by setting its type to be a GameObject and then selecting a game object with the name outpost by using GameObject.Find. The Find command selects an object in the current scene by its name in the Hierarchy and can be used as an alternative to using tags.

Now that we have a variable representing our outpost game object, we can use this variable with dot syntax to call animation attached to it by stating:

myOutpost.animation.Play("dooropen");

This simply finds the animation component attached to the outpost object and plays the animation called dooropen. The play() command can be passed any string of text characters, but this will only work if the animation clips have been set up on the object in question.

Your finished OpenDoor() custom function should now look like this:

function OpenDoor(){
audio.PlayOneShot(doorOpenSound);
doorIsOpen = true;
var myOutpost : GameObject = GameObject.Find("outpost");
myOutpost.animation.Play("dooropen");
}
Reversing the procedure

Now that we have created a set of instructions that will open the door, how will we close it once it is open? To aid playability, we will not force the player to actively close the door but instead establish some code that will cause it to shut after a defined time period.

This is where our doorTimer variable comes into play. We will begin counting as soon as the door becomes open by adding a value of time to this variable, and then check when this variable has reached a particular value by using an if statement.

Because we will be dealing with time, we need to utilize a function that will constantly update such as the Update() function we had awaiting us when we created the script earlier.

Create some empty lines inside the Update() function by moving its closing curly brace } a few lines down.

Firstly, we should check if the door has been opened, as there is no point in incrementing our timer variable if the door is not currently open. Write in the following if statement to increment the timer variable with time if the doorIsOpen variable is set to true:

if(doorIsOpen){
doorTimer += Time.deltaTime;
}

Here we check if the door is open — this is a variable that by default is set to false, and will only become true as a result of a collision between the player object and the door. If the doorIsOpen variable is true, then we add the value of Time.deltaTime to the doorTimer variable. Bear in mind that simply writing the variable name as we have done in our if statement’s condition is the same as writing doorIsOpen == true.

Time.deltaTime is a Time class that will run independent of the game’s frame rate. This is important because your game may be run on varying hardware when deployed, and it would be odd if time slowed down on slower computers and was faster when better computers ran it. As a result, when adding time, we can use Time.deltaTime to calculate the time taken to complete the last frame and with this information, we can automatically correct real-time counting.

Next, we need to check whether our timer variable, doorTimer, has reached a certain value, which means that a certain amount of time has passed. We will do this by nesting an if statement inside the one we just added—this will mean that the if statement we are about to add will only be checked if the doorIsOpen if condition is valid.

Add the following code below the time incrementing line inside the existing if statement:

if(doorTimer > doorOpenTime){
shutDoor();
doorTimer = 0.0;
}

This addition to our code will be constantly checked as soon as the doorIsOpen variable becomes true and waits until the value of doorTimer exceeds the value of the doorOpenTime variable, which, because we are using Time.deltaTime as an incremental value, will mean three real-time seconds have passed. This is of course unless you change the value of this variable from its default of 3 in the Inspector.

Once the doorTimer has exceeded a value of 3, a function called shutDoor() is called, and the doorTimer variable is reset to zero so that it can be used again the next time the door is triggered. If this is not included, then the doorTimer will get stuck above a value of 3, and as soon as the door was opened it would close as a result.

Your completed Update() function should now look like this:

function Update(){
if(doorIsOpen){
doorTimer += Time.deltaTime;

if(doorTimer > 3){
shutDoor();
doorTimer = 0.0;
}
}
}

Now, add the following function called shutDoor() to the bottom of your script. Because it performs largely the same function as openDoor(), we will not discuss it in depth. Simply observe that a different animation is called on the outpost and that our doorIsOpen variable gets reset to false so that the entire procedure may start over:

function shutDoor(){
audio.PlayOneShot(doorShutSound);
doorIsOpen = false;

var myOutpost : GameObject = GameObject.Find("outpost");
myOutpost.animation.Play("doorshut");
}

LEAVE A REPLY

Please enter your comment!
Please enter your name here