Author: Brian A. Ree
1: Introduction and Tutorial Goals
Hello and welcome to our fifth little Unity tutorial. This tutorial requires that you have that latest Unity software installed, at the time of this writing the current version
is 2017.3.0f3. If you're new to Unity or you've been away for a while you might want to brush up your skills a little bit. Below are some links to get you up to speed.
I know I've said that sooo many times already. The goals of this tutorial are to control 'Robot Kyle' via a script such that Kyle will turn and walk to a destination point
on the plane he's standing on. We'll also set that destination based on a mouse click. So we'll be doing some scripting in the tutorial but it'll be light and also a great way to
get some scripting experience.
This tutorial doesn't have a starting project, I'll walk you through the setup of you can follow along with the final project. We're basicallt going to setup 'Robot Kyle'
in the same way we did for tutorial 0. I would recommend checking the Unity package exports above, I sometimes update
them. Just do a quick re-import and you'll have the latest version.
2: Getting Setup, Dive In
First thing we're going to do is import some standard assets, we're going to be moving quickly here because we've already done this before in tutorial 0.
Find the 'Assets' menu option, select 'Import' and choose 'Characters'. Import everything in the package by clicking the 'All' button. Now make sure you're signed into your Unity account, and either find 'Robot Kyle'
in the 'Asset Store' by opening up the 'Asset Store' window, 'Window' -> 'Asset Store', or if you've already picked up this package click the 'Downloads' button on the 'Asset Store' window, as pictured below.
And import the 'Robot Kyle' package into your project. Again choose 'All' to bring it all in.
You can view all your purchased packages right from inside Unity, awesome.
Next up we'll have to setup up the 'Robot Kyle' import settings. Find the prefab in the packages you just imported for Kyle. Make sure he's rigged as a 'Mechanim', save those changes.
All the other default options should be fine. Setup a plane for Kyle to stand on and drag an instance of the prefab onto the Hierarchy window. Make sure Kyle is a little bit above the plane
as depicted below. We'll need to add a few Components to him, an Animator, a Rigidbody, and a Capsule Collider. The Animator, and Rigidbody we can leave as is for now.
Below are screen shots for Kyle's transform and Capsule Collider Components.
Again with the glaring white plane huh.
Give Kyle a little room for his feet.
Setting up Kyle's Capsule Collider.
3: Adding Some Scripts
Alright now we have to add some scripts to Kyle, we'll be adding the 'ThirdPersonCharacter' and the 'ThirdPersonUserControl' scripts that are part of the
Standard Assets we've imported. Add them to Kyle, the screen shot below shows their location. By now you're probably an expert at dragging and dropping scripts!
We also have to set Kyle's animation controller, and since he's now being imported as a Mechanim, we'll just borrow the 'ThirdPersonAnimationController'.
So also drag and drop that into place as shown below. Now you should have a nicely swaying animated Robot Kyle just like we setup in tutorial 0.
I feel guilty if I don't make that an active link.
Setup Kyle's Animation Controller using the standard asset.
Add 3rd person control scripts to Kyle, also using the standard assets.
Before we get into the scripting let's add some color to the plane so we can see things a little easier. Import the
simple materials package, again select 'All' and import everything.
All five or so matieral, lol. Ok now asign one to the place to help with your visualization. I chose grey for the plane. We're also going to need a cube for our 3rd person character control script
so create a new one in the Hierarchy view and also give it a color. I chose orange for the cube. You can assign new materials by dragging them onto the Mesh Component of the GameObject
you're trying to alter. See the screen shot below for the new setup. We'll want to adjust the cube's properties slightly, just follow the settings in the image below.
The colors I chose for the Scene.
Add a new cube to the scene and position it somewhere away from Kyle, also give it a color.
Make sure to adjust the cube's properties.
4: Time for Some Scripting
Now let's add a new script to 'Robot Kyle', find the 'CtrlTest' script in the Project window and drag it onto Kyle.
We're going to attach the transform from an instance of a GameObject already in our scene to the script. I know that sounds confusing, lol.
Basically we're creating a reference to a GameObject in our scene that the script can use. You'll see why in a second. There are different way to do
this but a direct reference like we're doing is one of the simpler, more efficient ways, because it doesn't require any searching.
Drag the cube GameObject from the Hierarchy window into the 'Target Marker' setting in Kyle's 'CtrlScript'.
You should see something like in the screen shot below.
Find the 'CtrlTest' script and drag it onto Kyle.
Drag and drop the scene's cube GameObject onto the CtrlTest scripts 'Target Marker' field like so.
Now we're ready to look into the actual script code, don't run the scene yet, I want it to be a surprise. Oh wait! I forgot we must set our camera
to a nice 3rd person camera view, no worries, grab the 'Main Camera' GameObject and drag it so that it's a child of the 'Robot Kyle' GameObject.
See the screen shot below. You can adjust your 'Main Camera' so that it's looking over the shoulder of Kyle. Take a look at the camera's postion information below.
Remember that you can view what the camera is seeing by selecting it in the Hierarchy window. Ok cool. Next we'll go over the code piece by piece.
Below we have the import section of our C#, C-Sharp, script file. You can program Unity with Javascript or C#. I prefer C# because I find it
to be a bit more structured and rigid in ways that make larger projects easier. You may find JS to be quicker for quick scripting.
When you want to reference other classes in your script you may have to import them. You do so by specifying the namespace that the class
belongs to at the top of your script with an import tag. For all MonoBehaviours, think GameObject, you need 'using UnityEngine;'
Remember that we want to control our 3rd person character, remember the scripts we added to Kyle? This call 'using UnityStandardAssets.Characters.ThirdPerson;'
gives us access the those scripts in a programmatic way. It's out of the scopt of this tutorial to go over every namespace here but I wanted to cover the basics.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityStandardAssets.Characters.ThirdPerson;
The import section of our CtrlTest script.
The next section of code contains our class declaration and class variables. The class declaration can also include a namespace declaration but we've skipped
that part and simply allowed for the default namespace, probably a bad idea on my part. Just to explain the usage of it, if I defined the namespace for this class to be
'ThirdPerson.Character.Tests' then another script outside of the namespace would use an import statement like this, 'using ThirdPerson.Character.Tests;' before they can use
our class. See the links below for information on C# namespaces and imports.
public class CtrlTest : MonoBehaviour {
public Transform targetMarker;
private const int MOUSE_BUTTON = 0;
private Ray ray;
private RaycastHit hitInfo;
private Vector3 targetPosition;
private bool targetFound = false;
private bool targetReached = false;
public float proximity = 0.2f;
private ThirdPersonCharacter tpc;
private float dist = 0.0f;
The class declaration and class variable declaration parts of our code. See if you can figure out why
the attributes visible in the Unity Inspector window for this script are as they are.
Next up we introduce the 'Start' method. Unity has some default methods, or functions, that are part of the MonoBehaviour
parent class. If you look closely at our class declaration above, 'CtrlTest : MonoBehaviour', you'll see the word MonoBehaviour
in there. This is the way we tell C# that our new class, 'CtrlTest', is actually an extension of the Unity MonoBehaviour class.
It's beyond the scope of this tutorial to get into the realm of inheritence in code but you can google it and explore on your own.
Basically though scripts we attach to GameObjects are always going to extend the MonoBehaviour class and as such they have to have a
standard way to initialize, as you've seen we can have a lot of scripts attached to a GameObject. I've provided some links below
the explain the start methods and what they are used for. In our method we simply want to get a reference to the script that
provides the 3rd person motion controller of our character. You'll see what we do with it in a minute.
// Use this for initialization
void Start () {
tpc = gameObject.GetComponent<ThirdPersonCharacter> ();
}
In the initialization method of our class we try to grab a reference to the 'ThirdPersonCharacter' script that is also
present on our Robot Kyle game object.
We're going to get into the guts of this little, but powerful, script. I've included links below to most of the more involved aspects of the code so you can dig deeper on your own.
Ok so let's get started first off the 'Update' method is similar to the 'Start' method we saw earlier in that it is part of the MonoBehaviour class we are extending. The first thing we
do in this method is to check the game input by using the Input class, we're testing to see if there has been a mouse click. Now we must always remember that the 'Update' method is
called once every game frame, so code we put in here has to be very efficient and we should always keep in mind it will be running many times a second. That being said we have purposely tried
to detect the mouse up input and not the mouse down input. Take a moment to think about. If we were trying to detect the mouse down input it would return true for a few frames, all the frames
were the mouse button is held down, because you wouldn't click a mouse so fast that it only took one game frame. So to prevent the first part of our if statement from triggering many times
we use the mouse up input.
The next bit is a little magical. Using unity built in functions for the 'Main Camera' GameObject we can find a ray, think of this as a straight line pointing from one point to another.
In our case the ray starts at the point on the screen where the mouse click happened and points in a direction that has to do with the orientation of that camera in our scene. Just try to imagine
it as a straight line from the mouse click point down into our scene somewhere. Since we don't know exactly where we use the 'Physics.Raycast' class and method. We tell it the starting point of our ray, think
line in 3d space, and the direction that ray is pointing in. It doesn't return a value to us rather it sets the value of a passed in variable, that is the out directive. This seems a little bit tricky
but if you think of 'out hitInfo' as a cardboard box we want the return data to go in, then 'Physics.Raycast' just fills that box for us. We store the point in our 3d scene where the ray intersects something.
I only click on the plane, ground, when running this scene but I'm sure you can get it to do some weird stuff clicking elsewhere. The position of the intersection is stored in 'targetPosition'.
Now remember when we dragged the cube object over to the 'Target Marker' property of our script? If we did so, and the targetMarker variable isn't null then we set the position of that marker with the
position of th eray intersection. This has the effect of moving the cube in our scene to the point of intersection of our mouse click. We then set some boolean flags, targetFound and targetReached.
We've found our target but we assume we haven't reached it yet. We're gonna make Kyle walk over to it in an animated way, very cool. As a precaution if the targetMarker variable is null we set all our
flags to false. Take a look at the code below we'll be going over last piece of code next.
// Update is called once per frame
void Update () {
if (Input.GetMouseButtonUp (MOUSE_BUTTON)) {
ray = Camera.main.ScreenPointToRay (Input.mousePosition);
if (Physics.Raycast (ray.origin, ray.direction, out hitInfo)) {
targetPosition = hitInfo.point;
if (targetMarker != null) {
targetMarker.position = targetPosition;
Utils.wr ("TargetMarker.position: " + targetMarker.position);
targetFound = true;
targetReached = false;
} else {
targetFound = false;
targetReached = false;
Utils.wrErr ("Target marker is null...");
}
}
Detecting a mouse click on our scene and using ray casting to determine the intersection of the mouse click, woot!
Wow finally, almost done. The last bit of code that we check every frame is if there is a target and we haven't reached it yet.
If so and our ThirdPersonController, tpc, isn't null we want to make our character walk to the point where the cube now is, the point where our mouse click
intersects out scene. As an escae if 'tpc' is null then we set all our boolean flags to false. In order to find the proper vector that Kyle needs to move towards we have to
subtract the targetPosition from the current position of Kyle, since this script is attached to Kyle the current position of Kyle will be 'transform.position'.
We can call the 'Move' method on the ThirdPersonController, tpc, also attached to Kyle. It takes 3 arguments, the position to move towards, and if Kyle is jumping or crouching.
We'll set the jumping and crouching booleans to false for this test. Hold on we're not done yet. We need to tell if we want Kyle to stop. So use a method of the Vector3 class, Vector3 because
we're in three dimensions, to give us the distance between Kyle and the targetPosition. If this distance is close enough, as determined by our proximity variable, then we set targetReached to true
and Kyle will stop. Look over the code below and make sure you understand it.
} else if (targetFound == true && !targetReached) {
if (tpc != null) {
tpc.Move ((targetPosition - transform.position), false, false);
dist = Vector3.Distance (transform.position, targetPosition);
Utils.wr ("Dist: " + dist);
if (dist <= proximity) {
targetReached = true;
}
} else {
targetFound = false;
targetReached = false;
Utils.wrErr ("ThirdPersonCharacter is null...");
}
}
Determining where to move Kyle and when to tell Kyle to stop moving.
Now run the scene and click the plane in some empty area, you'll see the cube jump to that point and then you'll see Kyle turn and walk towards that point.
Pretty cool, really powerful stuff for the amount of code we've written. This is why Unity is so awesome because it gives you the ability to do so much in a quick
and efficient way.