Building a real-time strategy game in Unity 4.1 using C# scripting

<
>

Part 8: Basic Unit Movement

This time we will look at implementing something a little more interactive by adding some basic movement to our units.

Movement Cursor

First up, let's modify the cursor state to indicate a movement option for when we have a Unit selected. Before we can implement something specific for our Unit, we need to add in some general handling of a mouse hover. We start by adding this method to UserInput.cs

				private void MouseHover() {
					if(player.hud.MouseInBounds()) {
						GameObject hoverObject = FindHitObject();
						if(hoverObject) {
							if(player.SelectedObject) player.SelectedObject.SetHoverState(hoverObject);
							else if(hoverObject.name != "Ground") {
								Player owner = hoverObject.transform.root.GetComponent< Player >();
								if(owner) {
									Unit unit = hoverObject.transform.parent.GetComponent< Unit >();
									Building building = hoverObject.transform.parent.GetComponent< Building >();
									if(owner.username == player.username && (unit || building)) player.hud.SetCursorState(CursorState.Select);
								}
							}
						}
					}
				}

and calling it from the end of MouseActivity() in UserInput.cs. Now we need to implement the default behaviour for SetHoverState() in WorldObject.cs.

				public virtual void SetHoverState(GameObject hoverObject) {
					//only handle input if owned by a human player and currently selected
					if(player && player.human && currentlySelected) {
						if(hoverObject.name != "Ground") player.hud.SetCursorState(CursorState.Select);
					}
				}

With the default behaviour in place we can now add some specific behaviour for our Unit by adding the following override in Unit.cs.

				public override void SetHoverState(GameObject hoverObject) {
					base.SetHoverState(hoverObject);
					//only handle input if owned by a human player and currently selected
					if(player && player.human && currentlySelected) {
						if(hoverObject.name == "Ground") player.hud.SetCursorState(CursorState.Move);
					}
				}

If you run this from Unity now you will notice that no move cursor shows up if the Unit is selected. This is because the Unit does not actually belong to a Player at the moment. To fix this, drag your Unit object onto your Player object, making it a child of the Player. Unfortunately, you will now no longer be able to select the Unit, since we accidentally introduced a bug in an earlier post. Thankfully this is an easy bug to fix. The problem is that when we are determining which object was selected we are using

				hitObject.transform.root.GetComponent< WorldObject >()

to gain a reference to the WorldObject script (or a subclass of that). Unfortunately, when we add the object to a Player this reference no longer works. We actually need to use

				hitObject.transform.parent.GetComponent< WorldObject >()

instead. To fix our code we need to make this change in MouseClick() in WorldObject.cs and LeftMouseClick() in UserInput.cs. Now if you run your game from within Unity things should behave as expected. We can still select both the Building and the Unit. If we have the Building selected we are only ever shown the select cursor. But if we have the Unit selected we are shown the select cursor if we are over an object and the move cursor if we are hovering over the ground - complete with animation.

Movement Input

With that in place it is time to handle user input for movement. To initiate movement for a Unit we need to extend the functionality of our mouse click logic for a WorldObject. Add the following code to MouseClick() in Unit.cs.

				public override void MouseClick(GameObject hitObject, Vector3 hitPoint, Player controller) {
					base.MouseClick(hitObject, hitPoint, controller);
					//only handle input if owned by a human player and currently selected
					if(player && player.human && currentlySelected) {
						if(hitObject.name == "Ground" && hitPoint != ResourceManager.InvalidPosition) {
							float x = hitPoint.x;
							//makes sure that the unit stays on top of the surface it is on
							float y = hitPoint.y + player.SelectedObject.transform.position.y;
							float z = hitPoint.z;
							Vector3 destination = new Vector3(x, y, z);
							StartMove(destination);
						}
					}
				}

We start off here by making sure that the default implementation for handling a mouse click is handled first. Then, if the Player clicked on the ground, we want to start a move towards that position. To handle that logic we need to implement StartMove().

				public void StartMove(Vector3 destination) {
					this.destination = destination;
					targetRotation = Quaternion.LookRotation (destination - transform.position);
					rotating = true;
					moving = false;
				}

This methods sets the state for our Unit in preparation for moving. We need the destination that our Unit is to move towards and the amount of rotation needed by the Unit for it to be facing the destination. This rotation is given to us by the Unity method Quaternion.LookRotation(). It also appears that Unity is using the z-axis as the forward direction (which is why we constructed our Unit the way we did earlier). We then specify that the Unit is ready to rotate in preparation for moving. To allow our code to compile we need to add these global variables to the top of Unit.cs.

				protected bool moving, rotating;

				private Vector3 destination;
				private Quaternion targetRotation;

Unit Movement

Now that our Players have the ability to give destinations to Units it is time to implement some basic movement. This will involve telling the Unit how to get from it's current location to the destination that has been set. For now we will go with a very simple algorithm.

At a later date you will want to implement some form of pathfinding and collision avoidance so that your Unit does not run into anything along the way. But for now it is enough to be able to move around. Remember that at the end of StartMove() we introduced two state variables - rotating and moving. We will use these to determine what the Unit should currently be doing. Modify the Update() method in Unit.cs to be the following.

				protected override void Update () {
					base.Update();
					if(rotating) TurnToTarget();
					else if(moving) MakeMove();
				}

This implements all of the logic for our algorithm above (apart from stopping), leaving it up to the individual methods to fill in the blanks for how each part is to be implemented. Let's handle turning towards the destination first.

				private void TurnToTarget() {
					transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotateSpeed);
					//sometimes it gets stuck exactly 180 degrees out in the calculation and does nothing, this check fixes that
					Quaternion inverseTargetRotation = new Quaternion(-targetRotation.x, -targetRotation.y, -targetRotation.z, -targetRotation.w);
					if(transform.rotation == targetRotation || transform.rotation == inverseTargetRotation) {
						rotating = false;
						moving = true;
					}
				}

The Unity method Quaternion.RotateTowards() provides a smooth transition between the current rotation of the Unit and the desired rotation. The variable rotateSpeed is used to determine how quickly we want to get between those two rotation values. A numerical issue can cause the Unit to be stuck facing the wrong way, which we fix with the check against inverseTargetRotation. Once the current rotation of the Unit matches the desired (or inverse desired) rotation we stop rotating and declare that the Unit is now ready to move. Before we go any further let's declare a movement speed and a rotation speed at the top of Unit.cs, making them public so that we can tweak their values inside Unity.

				public float moveSpeed, rotateSpeed;

Now we need to provide the basic movement for a Unit.

				private void MakeMove() {
					transform.position = Vector3.MoveTowards(transform.position, destination, Time.deltaTime * moveSpeed);
					if(transform.position == destination) moving = false;
				}

Once again we make use of a Unity method to give us smooth movement to the destination, this time by using Vector3.MoveTowards(). And once again we have moveSpeed to help us determine how quickly that will happen. If we now set moveSpeed and rotateSpeed for the Unit to 1 you should be able to direct it around the map (taking care not to run into any other objects). Feel free to play around with these values until they match a speed that you are more comfortable with.

If you run your game you will notice an annoying fact when moving the Unit around: the selection box remains where the Unit was first located, which is no use at all. What we want is for the selection box to remain around the Unit at all times. The selection box is based on the Bounds for the Unit. Since these are never being updated the selection box does not move. We may want these bounds to remain up-to-date for other checks against the Unit, so let's make sure they stay current by adding

				CalculateBounds();

to the end of both TurnToTarget() and MakeMove(). This guarantees that whenever the position of the Unit has changed (whether through movement or rotation) it's Bounds remain correct.

And that brings us to the end of this part. We are now informing the Player that they can select a position to move the currently selected Unit to and allowing them to move that Unit there in a very basic fashion. We have also structured the code in such a way that we should be able to update the movement process reasonably easily without breaking the rest of our code. As usual, the code can be found on github under the post for Part 8. The next part will look into adding the concept of resources to a Player along with displaying those values in the HUD.

<
>