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

<
>

Part 14: Basic Combat

This time round we will be covering combat, the fun part of a game where you get to destroy things! We will only initiate this from UserInput for now, although our WorldObjects will be set up in such a way that they could easily be told to start attacks automatically through triggers of some sort. An important part of this will also be determining what happens to a WorldObject as it loses hitpoints.

Attack Cursor

Let's start by detecting whether the Player can start an attack. We will indicate this to the Player by changing the mouse cursor to an attack cursor. We need to start by extending SetHoverState() in WorldObject.cs. Replace the existing code with the code below

				public virtual void SetHoverState(GameObject hoverObject) {
					//only handle input if owned by a human player and currently selected
					if(player && player.human && currentlySelected) {
						//something other than the ground is being hovered over
						if(hoverObject.name != "Ground") {
							Player owner = hoverObject.transform.root.GetComponent< Player >();
							Unit unit = hoverObject.transform.parent.GetComponent< Unit >();
							Building building = hoverObject.transform.parent.GetComponent< Building >();
							if(owner) { //the object is owned by a player
								if(owner.username == player.username) player.hud.SetCursorState(CursorState.Select);
								else if(CanAttack()) player.hud.SetCursorState(CursorState.Attack);
								else player.hud.SetCursorState(CursorState.Select);
							} else if(unit || building && CanAttack()) player.hud.SetCursorState(CursorState.Attack);
							else player.hud.SetCursorState(CursorState.Select);
						}
					}
				}

and add the method CanAttack() which we will use to determine whether a specific WorldObject is able to attack things or not.

				public virtual bool CanAttack() {
					//default behaviour needs to be overidden by children
					return false;
				}

Notice that we have declared the method virtual to allow a class which extends WorldObject to override the default behaviour if necessary. By default a WorldObject will not be able to attack.

This is the logic being used for setting the hover state:

If we wish to implement teams at some point then this code will need updating to make sure that the other Player is not on the same team as the Player that controls the selected WorldObject, rather than just checking that they are a different Player. There is also an assumption here that Players are not allowed to have the same name, since that is what we are using to differentiate between them.

For now we have just the one WorldObject that can attack things - our Tank. To allow it to be able to attack things add the following code to Tank.cs.

				public override bool CanAttack() {
					return true;
				}

We actually defined drawing of the attack cursor in part 7 when we first added in custom cursors, so the code we have just added is enough to handle the changing of the cursor. If your objects still match mine then you should have a Building that is not attached to the Player. If you run your game now, select your Tank, and then hover your cursor over this Building - you should be able to see the cursor change correctly to an attack cursor.

Extra Player

To make things a little easier to test, though, we should add in another Player and give them some Units and Buildings. I think we have also reached the stage now where we can actually create a Player Prefab too.

Before we create the Prefab we should strip out everything that we do not want a brand new Player to have. This means we want to delete all Units and Buildings currently attached to our Player. (Before you do this make sure that each of them has already been made into a Prefab, complete with BuildImages) You should be left with a Player object that contains a Buildings object and a Units object (both empty), an HUD object, and a RallyPoint object. Both the RallyPoint and the HUD should also be Prefabs, so that we can make changes to the Prefab and know that those changes will be applied to all Players that have an instance of those. Once your Player looks like this, turn it into a Prefab (stored in the Player folder).

Now that we have a Player Prefab we can add another instance of Player to the map. Set the name for one Player to Player1 and the name for the other Player to Player2. This makes sure that the two Players will be different if we compare them. Make Player1 a Human player (so that we can control that Player) and Player2 a Computer player (so that we have no control). With two Players in place add some Units and Buildings to each of them. We can do this by dragging new Building Prefabs onto the Buildings part of our Player object and new Unit Prefabs onto the Units part our our Player object. I have given each player a Refinery and a WarFactory, along with a Harvester, a Worker, and 2 Tanks. (Make sure that you spread these around the map a bit too. This is also a good time to make sure that the Position and Rotation for each Prefab is (0, 0, 0)).

If you run your game now you will see a collection of Units and Buildings scattered around. Unfortunately, we have no easy way to identify who owns what. We are only able to control the Units and Buildings of Player1, but the only way to tell what those are at the moment is to click on something and see if it responds. This is less than ideal, but thankfully there is a fairly easy way to fix this.

Team Color

To help an actual Player to determine what is theirs we are going to introduce the ability to color part of a WorldObject with a team color. We will leave it up to a specific object to determine what this will be. Let us start by adding

				if(player) SetTeamColor();

to the end of the Start() method in WorldObject.cs and then defining the method SetTeamColor().

				protected void SetTeamColor() {
					TeamColor[] teamColors = GetComponentsInChildren< TeamColor >();
					foreach(TeamColor teamColor in teamColors) teamColor.renderer.material.color = player.teamColor;
				}

This simply finds all the child objects of the WorldObject with the script TeamColor.cs attached and changes their color to the team color specified for the Player. Of course, we need to create this script before we can use this code. Create a new C# script in the WorldObject folder, name it TeamColor.cs, and set it's contents as follows.

				using UnityEngine;

				public class TeamColor : MonoBehaviour {
					//wrapper class to allow a user-defined
					//part of a model to receive a team color
					//in the material of that part
				}

Next we need to assign a team color to a Player. Add

				public Color teamColor;

to the top of Player.cs. This allows us to now set a team color for each Player inside Unity. I have made Player1 to be a light blue (R: 100, G: 200, B: 250) and Player2 to be a medium red (R: 235, G: 20, B: 20).

Finally we need to define the parts of each WorldObect that we want to be the team color. To do this is as easy as attaching the script TeamColor.cs to the desired object. The thing is, we want to perform this attachment on the Prefab, not on an object already assigned to a Player. This is because we want to be able to guarantee that we do not have to perform this action every time we add a new object to a Player. To do so we need to create an instance of each Prefab, add the script to the parts we want colored, then resave the Prefab (choosing to overwrite the existing version). I will leave it up to you to decide which parts should be the team color, though feel free to have a look at my version (found on github). Now when you run your game it should be easy to see which Player controls which Units and Buildings.

Note: If we actually delete a Prefab that is referenced in GameObjectList we will need to remove the entry for it there. It is probably worthwhile at this stage checking to see that GameObjectList is up-to-date, since we are using it as the storage container for all objects we wish to create in our world.

If you have been paying close attention to how things ran prior to this post you will notice that we have introduced a subtle bug. Our Units and Buildings now all have a part of them designated to be a team color, which is great for identifying which Player it belongs to. Unfortunately, when we create a new Building it is transparent during the build process. Once construction is completed it reverts to the colors it had, but it forgets the team color. We need to fix this, since construction is going to be the primary way that our Players gain new Buildings. It actually turns out that this is really easy to fix. All we need to do is add a call to SetTeamColor() to the end of Construct() in Building.cs, resulting in the following code.

				public void Construct(int amount) {
					hitPoints += amount;
					if(hitPoints >= maxHitPoints) {
						hitPoints = maxHitPoints;
						needsBuilding = false;
						RestoreMaterials();
						SetTeamColor();
					}
				}

Initiate Attack

Now that we can distinguish between Players and we can indicate to a Player that an attack can be launched it is time to actually start that attack. We start by updating the code in MouseClick() found in WorldObject.cs.

				public virtual void MouseClick(GameObject hitObject, Vector3 hitPoint, Player controller) {
					//only handle input if currently selected
					if(currentlySelected && hitObject && hitObject.name != "Ground") {
						WorldObject worldObject = hitObject.transform.parent.GetComponent< WorldObject >();
						//clicked on another selectable object
						if(worldObject) {
							Resource resource = hitObject.transform.parent.GetComponent< Resource >();
							if(resource && resource.isEmpty()) return;
							Player owner = hitObject.transform.root.GetComponent< Player >();
							if(owner) { //the object is controlled by a player
								if(player && player.human) { //this object is controlled by a human player
									//start attack if object is not owned by the same player and this object can attack, else select
									if(player.username != owner.username && CanAttack()) BeginAttack(worldObject);
									else ChangeSelection(worldObject, controller);
								} else ChangeSelection(worldObject, controller);
							} else ChangeSelection(worldObject, controller);
						}
					}
				}

There are a couple of things to note here. We only allow a Player to attack objects that belong to another Player, an attack is only launched if the object can launch attacks. Also, the default behaviour is to change the selected object. With this code in place we now need to define the method BeginAttack(), which is where all the work of starting an attack will actually happen.

				protected virtual void BeginAttack(WorldObject target) {
					this.target = target;
					if(TargetInRange()) {
						attacking = true;
						PerformAttack();
					} else AdjustPosition();
				}

The logic here is to only attack if the target is close enough. If not the WorldObject needs to move closer to the target. But before we do that we want to save the target that is to be attacked. This means that we need to add

				protected WorldObject target = null;

to the top of WorldObject.cs so that we can store this reference. Next we need to add

				protected bool attacking = false;

to the top of WorldObject.cs, which we will use to indicate whether this object is attacking something or not. Finally, we need to add the three methods which handle the working out of the logic for this method. We will start with working out whether the target is in range or not.

				private bool TargetInRange() {
					Vector3 targetLocation = target.transform.position;
					Vector3 direction = targetLocation - transform.position;
					if(direction.sqrMagnitude < weaponRange * weaponRange) {
						return true;
					}
					return false;
				}

The target is in range if the distance to the target is less than the range of the weapon for the WorldObject. For us to know what this range is we need to add

				public float weaponRange = 10.0f;

to WorldObject. By specifying a value here we can guarantee that each WorldObject has a default range (I have gone through and added defaults to most values now so that a new type of WorldObject will work without weird errors). Since it is public it also means that we can play around with the range inside Unity.

While looking into how best to work out the distance to the target I found docs.unity3d.com/Documentation/Manual/DirectionDistanceFromOneObjectToAnother.html which notes that for a simple distance calculation we can use the square of the magnitude, since that takes less work to calculate. This is what makes a seemingly simple check look a little more complicated, but every little bit of time saved in processing is worth it in the long run.

Now that we know whether a target is in range or not, let's specify how to adjust the position of our WorldObject.

				private void AdjustPosition() {
					Unit self = this as Unit;
					if(self) {
						movingIntoPosition = true;
						Vector3 attackPosition = FindNearestAttackPosition();
						self.StartMove(attackPosition);
						attacking = true;
					} else attacking = false;
				}

At the moment the only object that can move closer to a target is a Unit. If the WorldObject is not a Unit then we cancel the attack (since it is not possible anyway). Otherwise we want to find the closest point between the WorldObject and the target which is in range and then start moving towards that. This method that performs this calculation needs to be added in now.

				private Vector3 FindNearestAttackPosition() {
					Vector3 targetLocation = target.transform.position;
					Vector3 direction = targetLocation - transform.position;
					float targetDistance = direction.magnitude;
					float distanceToTravel = targetDistance - (0.9f * weaponRange);
					return Vector3.Lerp(transform.position, targetLocation, distanceToTravel / targetDistance);
				}

We know where the WorldObject is, and we know where the target is. From this we can calculate the vector between the two objects. This vector gives us the distance to the target, from which we want to subtract the weapon range. We will actually subtract 90% of the weapon range so that once the Unit is in position it will not need to move immediately if the target should move. This gives us the distance that we need to travel along the vector between the WorldObject and the target. We then use all of these details to execute the Unity linear interpolation method on a Vector3 to calculate the position in the world that is the correct distance along the vector. This is the position that needs to be returned as the nearest attack position.

We also need to add

				protected movingIntoPosition = false;

to the top of WorldObject.cs to help with us working out what the current state of our WorldObject is. To make sure that a Unit updates it's state upon completion of movement we also need to add

				movingIntoPosition = false;

into MakeMove() in Unit.cs when the Unit has reached it's destination. The resulting method is as follows.

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

Now that we know how to get into range it is time to define how to perform an attack in WorldObject.cs.

				private void PerformAttack() {
					if(!target) {
						attacking = false;
						return;
					}
					if(!TargetInRange()) AdjustPosition();
					else if(!TargetInFrontOfWeapon()) AimAtTarget();
					else if(ReadyToFire()) UseWeapon();
				}

This method is going to be called regularly while the WorldObject is attacking, so we will add in some extra checks that are useful at the same time. The first one is to make sure that the target has not been destroyed already (by this WorldObject or some other WorldObject). If it has we need to stop the attack at once. Again we check to see whether the target is in range still, since it might have managed to move away from the WorldObject. Next we check to see whether the target is in front of the weapon the WorldObject has and adjust position accordingly. Finally we check to see whether the weapon is ready to fire (since we want to enforce a regular rate of fire) and only actually use the weapon if the WorldObject is ready to.

Once more we need to add some more code to WorldObject.cs. Let's start with aiming the weapon, since we already have code in place that handles whether the target is in range.

				private bool TargetInFrontOfWeapon() {
					Vector3 targetLocation = target.transform.position;
					Vector3 direction = targetLocation - transform.position;
					if(direction.normalized == transform.forward.normalized) return true;
					else return false;
				}

This check is quite straightforward. If the vector between the WorldObject and the target equals the forward vector for the WorldObject then the target is in front of the WorldObject. At this stage we are assuming that the weapon for a WorldObject always points out the front of the WorldObject. If you wish to change this for a specific object then that object will need to override this method (in which case this will need to become a protected virtual method, rather than a private method).

				protected virtual void AimAtTarget() {
					aiming = true;
					//this behaviour needs to be specified by a specific object
				}

We will leave most of the behaviour for aiming at a target up to a specific object. All we want to do here is to set a state variable so that we can work out whether the WorldObject is aiming their weapon or not. This needs to be added to the top of WorldObject.cs now.

				protected bool aiming = false;

The one WorldObject we have at the moment which can attack at the moment is our Tank. To make sure that this turns towards a target properly we need to add an overridden version AimAtTarget() to Tank.cs.

				protected override void AimAtTarget () {
					base.AimAtTarget();
					aimRotation = Quaternion.LookRotation (target.transform.position - transform.position);
				}

Here we are working out what rotation is needed to turn the Tank towards a target. To store this we need to add

				private Quaternion aimRotation;

to the top of Tank.cs. To actually perform the rotation we need to adjust the Update() method for our Tank.

				protected override void Update () {
					base.Update();
					if(aiming) {
						transform.rotation = Quaternion.RotateTowards(transform.rotation, aimRotation, weaponAimSpeed);
						CalculateBounds();
						//sometimes it gets stuck exactly 180 degrees out in the calculation and does nothing, this check fixes that
						Quaternion inverseAimRotation = new Quaternion(-aimRotation.x, -aimRotation.y, -aimRotation.z, -aimRotation.w);
						if(transform.rotation == aimRotation || transform.rotation == inverseAimRotation) {
							aiming = false;
						}
					}
				}

Now let's find out whether the weapon of a WorldObject is ready to fire or not by adding the following code to WorldObject.cs.

				private bool ReadyToFire() {
					if(currentWeaponChargeTime >= weaponRechargeTime) return true;
					return false;
				}

To track the weapon readiness we also need to add

				public float weaponRechargeTime = 1.0f;
				private float currentWeaponChargeTime;

to the top of WorldObject.cs. This allows us to vary the time taken to recharge the weapon within Unity. The current charge time will be modified in the Update() method (which we will get to in just a minute). If the current charge time is greater than the required charge time then the weapon is ready to use.

Once we know that the weapon is ready it is time to actually use it.

				protected virtual void UseWeapon() {
					currentWeaponChargeTime = 0.0f;
					//this behaviour needs to be specified by a specific object
				}

Again we will leave the actual implementation of using a weapon up to a specific object. What we do want to do is to reset the current charge time for the weapon back to 0 each time the weapon is used. This guarantees that the weapon needs to recharge after use.

Before we get to implementing the firing of the weapon for our Tank, we need to make sure the Update() method for WorldObject.cs is adjusted so that we can use the weapon regularly while the WorldObject is attacking.

				protected virtual void Update () {
					currentWeaponChargeTime += Time.deltaTime;
					if(attacking && !movingIntoPosition && !aiming) PerformAttack();
				}

Here we are increasing the current charge time for the weapon each update. It does not matter if this gets too high, since as long as it is greater than the weapon charge time we are able to fire the weapon. We also want to perform an attack if the WorldObject is attacking but is not also moving into position or aiming. If you run this now you should be able to see your attack vehicles move into position, although they will not yet be using any weapons.

Use Tank Weapon

Before we finish up this time it would nice to be able to destroy something. To do this, let's provide an implementation of UseWeapon() in Tank.cs.

				protected override void UseWeapon () {
					base.UseWeapon();
					Vector3 spawnPoint = transform.position;
					spawnPoint.x += (2.1f * transform.forward.x);
					spawnPoint.y += 1.4f;
					spawnPoint.z += (2.1f * transform.forward.z);
					GameObject gameObject = (GameObject)Instantiate(ResourceManager.GetWorldObject("TankProjectile"), spawnPoint, transform.rotation);
					Projectile projectile = gameObject.GetComponentInChildren< Projectile >();
					projectile.SetRange(0.9f * weaponRange);
					projectile.SetTarget(target);
				}

Our Tank is going to make use of a very simple projectile weapon. This needs to be spawned at the barrel of the gun (which is now pointing at the target) and sent off to hit the target. We need to make use of the forward vector for the Tank to help us find the position where the Projectile needs to be spawned. To find this I positioned a Tank at (0, 0, 0) and then positioned a Projectile where I wanted it to appear. Since everything still has a fixed y position (on the plane representing our 'ground') that remains the same. The x and z positions need to be multiplied by the x and z positions of the forward vector (not the location vector) of our Tank. This gives us the position in the world where we want to create a new Projectile. We are then using the Unity method Instantiate() to create the object we want, getting the Projectile object attached to that, and setting the target and range appropriately. The range is set to a little less than the actual weapon range since it was appearing out the far side of the target for some weird reason.

Now, I can hear you thinking "Wait a minute, we don't have a Projectile yet", and this is true. But that is something that is easily fixed. First, let us start by creating the necessary scripts. Inside the WorldObject folder create a new C# script called Projectile.cs and set it's code as follows.

				using UnityEngine;
				using System.Collections;

				public class Projectile : MonoBehaviour {

					public float velocity = 1;
					public int damage = 1;

					private float range = 1;
					private WorldObject target;

					void Update () {
						if(HitSomething()) {
							InflictDamage();
							Destroy(gameObject);
						}
						if(range>0) {
							float positionChange = Time.deltaTime * velocity;
							range -= positionChange;
							transform.position += (positionChange * transform.forward);
						} else {
							Destroy(gameObject);
						}
					}

					public void SetRange(float range) {
						this.range = range;
					}

					public void SetTarget(WorldObject target) {
						this.target = target;
					}

					private bool HitSomething() {
						if(target && target.GetSelectionBounds().Contains(transform.position)) return true;
						return false;
					}

					private void InflictDamage() {
						if(target) target.TakeDamage(damage);
					}
				}

We are assigning each Projectile a velocity and damage variable which can be tweaked inside Unity. These determine how fast the Projectile moves and how much damage it deals when it hits something. We also have a range and a target which can only be set by another object (normally the one that creates the Projectile in the first place). This makes sense, since the Projectile itself does not know how far it can travel, or what it is meant to hit.

The logic of what the Projectile can do is all handled inside the Update() method. First off, if the Projectile hit something we need to inflict damage on that object and then destroy the Projectile. It would be at this point that we would initiate an explosion sequence if we so desired, but I will leave that up to you. Rather than using another variable to keep track of how far the Projectile has traveled we will reduce the range as it moves instead. This means that the Projectile should be moved forward if it still has range left. We need to work out how far to move the Projectile, subtract that value from the range, and then adjust the position of the Projectile. If there is no range left then the Projectile needs to be destroyed (this makes sure that we do not clutter our world with Projectiles which have missed their targets).

To check whether the Projectile has hit something we are going to keep things very simple for now. Ideally we would be checking for a collision with any object in our world. But for now we will just check to see if the Projectile has hit the specified target. This does mean that we will have glitches like being able to shoot through other objects to hit our target. But for now, this will be sufficient. (If we were to detect that the Projectile had hit something else here we could change target to be that object, which would mean that the rest of the code will still work as expected). Dealing damage is as simple as telling the target object how much damage to lose. This does mean, however, that we need to add a new method to WorldObject.cs to handle this.

				public void TakeDamage(int damage) {
					hitPoints -= damage;
					if(hitPoints<=0) Destroy(gameObject);
				}

Once a WorldObject has no hitPoints left we need to destroy it. Once again, a fancy death sequence would be added here if we wanted it. This is another thing that you can play around with in your own time.

Ok, now that we have code to handle a Projectile we need an object to attach that to. Create a new empty object called TankProjectile. Add to this a new capsule with the following properties: Position = (0, 0, 0), Rotation = (0, 90, 90), Scale = (0.4, 0.3, 0.4). I added the Metal material that we created a while back to the capsule so that it looks more like a Tank shell.

To allow us to potentially extend things later on I created a new C# script called TankProjectile.cs inside the Tank folder which simply extends Projectile.cs (for now).

				using UnityEngine;
				using System.Collections;

				public class TankProjectile : Projectile {}

Attach this script to the TankProjectile object that you created. Now drag this object down into the Tank folder to create a TankProjectile Prefab that we can instantiate. Next add that Prefab to the WorldObjects list in our GameObjectList to allow the code we have above to actually find this Prefab when the Tank goes to instantiate a new TankProjectile. I have set the damage for this Prefab to 10 and the velocity to 30. This gives us a moderately fast Projectile that does light damage. You should now be able to run your game and take one of your Tanks out to destroy some of the 'enemy'.

And that wraps things up for this time. We now have the ability to tell our Buildings / Units to initiate attack against objects belonging to opposing Players. As usual the code can be found at github under the commit for Part 14. In the next part we will begin to introduce a simple menu system to our game.

<
>