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

<
>

Part 12: Resources

It is surprising to look back and see how much we have working already. There is, however, one important thing missing - we have no way for Players to gain more Resources. The goal for this part is to remedy that shortcoming. First up we will define a Resource and place an instance of that Resource on our map. Then we will create a new Unit, a Harvester, whose role is to collect that Resource. Once we have a Harvester we will tell it how to collect the Resource and then how to deposit what it has collected in a special building. Along the way we will also add update our HUD by adding health bars in. I apologize in advance, since this is going to be another long part ...

Resource

The first stage is to create a Resource that the Player can collect. To do this we will introduce a new type of WorldObject called Resource. Inside the Resource folder (located inside WorldObject) create a new C# script called Resource.cs. We want this to inherit from WorldObject, to be of a specified type, to have a capacity that we can vary within Unity, and to have a private reference to how much of the Resource is left. While we are at it we will also provide a public method to get what type the Resource is as well as a way to remove a specified quantity from the Resource. The resulting class should look like this.

				using UnityEngine;
				using RTS;

				public class Resource : WorldObject {

					//Public variables
					public float capacity;

					//Variables accessible by subclass
					protected float amountLeft;
					protected ResourceType resourceType;

					/*** Game Engine methods, all can be overridden by subclass ***/

					protected override void Start () {
						base.Start();
						amountLeft = capacity;
						resourceType = ResourceType.Unknown;
					}

					/*** Public methods ***/

					public void Remove(float amount) {
						amountLeft -= amount;
						if(amountLeft < 0) amountLeft = 0;
					}

					public bool isEmpty() {
						return amountLeft <= 0;
					}

					public ResourceType GetResourceType() {
						return resourceType;
					}
				}

Notice that we are making sure that a resource cannot have a quantity of less than 0. Also notice that the default resource type is set to Unknown. This means we need to update our ResourceType Enum in Enums.cs

				public enum ResourceType { Money, Power, Unknown }

to allow this. This class now nicely provides a base implementation for any Resource we want to add to our world.

The Resource that we will collect by default for this tutorial is going to be Ore. This will be taken to a Refinery where we will convert the Ore to money that is then added to the Player's bank account. Create a new folder inside the Resource folder called it OreDeposit. Inside this folder create a new C# script called OreDeposit.cs and a new C# script called Ore.cs. The idea is to create a pile of Ore 'blocks' that the Player is harvesting. As the deposit shrinks we will begin to hide some of the individual blocks to give a visual sense of progress. This means that Ore.cs is actually just a wrapper file to allow us to access individual blocks of Ore.

				using UnityEngine;

				public class Ore : MonoBehaviour {
					//wrapper class for getting access to a part of an ore deposit
				}

All of the hard work for this Resource will actually be done inside OreDeposit.cs.

				using UnityEngine;
				using RTS;

				public class OreDeposit : Resource {

					private int numBlocks;

					protected override void Start () {
						base.Start();
						numBlocks = GetComponentsInChildren< Ore >().Length;
						resourceType = ResourceType.Ore;
					}

					protected override void Update () {
						base.Update();
						float percentLeft = (float)amountLeft / (float)capacity;
						if(percentLeft < 0) percentLeft = 0;
						int numBlocksToShow = (int)(percentLeft * numBlocks);
						Ore[] blocks = GetComponentsInChildren< Ore >();
						if(numBlocksToShow >= 0 && numBlocksToShow < blocks.Length) {
							Ore[] sortedBlocks = new Ore[blocks.Length];
							//sort the list from highest to lowest
							foreach(Ore ore in blocks) {
								sortedBlocks[blocks.Length - int.Parse(ore.name)] = ore;
							}
							for(int i = numBlocksToShow; i < sortedBlocks.Length; i++) {
								sortedBlocks[i].renderer.enabled = false;
							}
							CalculateBounds();
						}
					}
				}

The OreDeposit keeps track of the number of visible blocks that it has. As the amount of Ore left in the Deposit goes down we hide particular blocks. The sneaky thing being done here is that we are actually going to give each block a number. We can then use the name of the block to define the order in which the blocks will be hidden - giving us a way to guarantee a sensible way in which the deposit will shrink. Once a block has been removed we then recalculate the bounds of the object to make sure that any collision detection we perform will be accurate at all times. Since the ResourceType for the OreDeposit is being set to Ore we need to make sure that we add this to our ResourceType Enum in Enums.cs.

				public enum ResourceType { Money, Power, Ore, Unknown }

Now that we have the behaviour for an OreDeposit defined, we should actually create one. Start by creating a new empty object and calling it OreDeposit. Make sure that it's position is set to (0, 0, 0). We are going to add 22 cubes to our OreDeposit object, named 1 through 22, with the scale for each of these set to (0.2, 0.2, 0.2). Create them all, add them to the OreDeposit object, and then set the position and rotation for specific cubes as follows:

  1. Position = (0.11, 0.25, -0.13), Rotation = (337.5, 278.5, 18.5)
  2. Position = (0.1, 0.23, 0.05), Rotation = (31.5, 335, 33)
  3. Position = (0.25, 0.21, -0.1), Rotation = (307, 301.5, 339.5)
  4. Position = (0.25, 0.21, 0.01), Rotation = (21.5, 249, 23)
  5. Position = (0.27, 0.18, 0.15), Rotation = (355, 238.5, 58.5)
  6. Position = (0.06, 0.1, 0.22), Rotation = (0, 335, 57.5)
  7. Position = (0.39, 0.05, 0.15), Rotation = (304.5, 224, 342)
  8. Position = (0.34, 0.09, 0.03), Rotation = (332.5, 223, 35)
  9. Position = (0.29, -0.01, 0.25), Rotation = (348, 211, 295.5)
  10. Position = (0.35, -0.02, -0.07), Rotation = (322, 268, 59.5)
  11. Position = (0.19, 0, 0.25), Rotation = (5.5, 20.5, 51.5)
  12. Position = (-0.01, -0.03, 0.24), Rotation = (39.5, 336, 53)
  13. Position = (-0.05, 0.07, 0.14), Rotation = (317, 311, 123.5)
  14. Position = (0.2, 0.07, 0.1), Rotation = (19.5, 321.5, 290.5)
  15. Position = (0.27, 0.07, -0.09), Rotation = (11, 323.5, 308)
  16. Position = (0.18, 0.1, -0.04), Rotation = (0, 335, 0)
  17. Position = (-0.04, 0.17, -0.2), Rotation = (-5.5, 335, 25.5)
  18. Position = (0, 0.1, 0), Rotation = (0, 0, 0)
  19. Position = (-0.17, 0, 0.04), Rotation = (325.5, 25, 21)
  20. Position = (-0.07, 0.04, -0.13), Rotation = (11, 341, 9.5)
  21. Position = (0.09, 0.02, -0.21), Rotation = (337.5, 335, 340)
  22. Position = (0.2, -0.01, -0.2), Rotation = (313.5, 306, 324.5)
Now set the scale for the OreDepost to (5, 5, 5). The stock material looks really boring, so let's create a new material that looks a bit more like iron ore. Inside the materials folder create a new Material and call it Ore. Set the shader to Specular, the main colour to (R: 35, G: 35, B:35), and the specular colour to (R: 130, G: 160, B: 175). Add this new Material to each of the numbered cubes along with the script Ore.cs. Finally add the script OreDepost.cs to the OreDeposit object. Make sure to set the capacity for the OreDeposit to something greater than 0 (e.g. 1000). Having finished creating our OreDeposit object drag it down into the OreDeposit folder to create an OreDeposit Prefab. Make sure that you shift the OreDeposit out of the way before continuing.

Harvester

The next stage in collecting Resources is to create a new Unit which can collect Resources from the OreDeposit we just created. We will start by creating the object in Unity, beginning with a new empty object called Harvester with it's position set to (0, 0, 0). To this object add 6 cubes (called ArmC, ArmL, ArmR, Cab, Body, and Tray) and 4 spheres (called WheelFL, WheelFR, WheelRL, WheelRR). Set the properties for each as follows:

Set the material for the arms to Flag, the material for the wheels to Ore, and the material for the rest of the objects to Metal. (I know, the names of the materials don't make sense in this context, but it is not worth creating new materials at this stage.)

Now create a new folder inside the Unit folder called Harvester. Create a new C# script inside this folder called Harvester.cs. We will start the code with the following framework (note that it inherits from Unit).

				using UnityEngine;
				using RTS;

				public class Harvester : Unit {

					public float capacity;

					private bool harvesting = false, emptying = false;
					private float currentLoad = 0.0f;
					private ResourceType harvestType;

					/*** Game Engine methods, all can be overridden by subclass ***/

					protected override void Start () {
						base.Start();
						harvestType = ResourceType.Unknown;
					}

					protected override void Update () {
						base.Update();
					}

					/* Public Methods */

					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") {
								Resource resource = hoverObject.transform.parent.GetComponent< Resource >();
								if(resource && !resource.isEmpty()) player.hud.SetCursorState(CursorState.Harvest);
							}
						}
					}

					public override void MouseClick(GameObject hitObject, Vector3 hitPoint, Player controller) {
						base.MouseClick(hitObject, hitPoint, controller);
						//only handle input if owned by a human player
						if(player && player.human) {
							if(hitObject.name != "Ground") {
								Resource resource = hitObject.transform.parent.GetComponent< Resource >();
								if(resource && !resource.isEmpty()) {
									//make sure that we select harvester remains selected
									if(player.SelectedObject) player.SelectedObject.SetSelection(false, playingArea);
									SetSelection(true, playingArea);
									player.SelectedObject = this;
									StartHarvest(resource);
								}
							} else StopHarvest();
						}
					}

					/* Private Methods */

					private void StartHarvest(Resource resource) {

					}

					private void StopHarvest() {

					}
				}

Here we are defining the base behaviour for a Harvester. If we have our Harvester selected and we hover over a Resource that is not empty then we want to change the cursor. If we then left click with the mouse on a Resource we want to start harvesting. We need to make sure that the Harvester remains selected when we click on the Resource, since it will be deselected by the default behaviour of a Unit.

Attach this script to the Harvester object we created and then attach the Harvester to our Player (under Units) to make sure that we can control it. Since this is a Unit we need to set the rotate speed and move speed if we want the Harvester to be able to move. We should also set a name for the Harvester (I have just used Harvester), as well as a build image (I have used the one below).

Harvester

Harvester

Collecting Resources

Now that we have a Harvester we need to define how it collects Resources. We will start by adding this code to the StartHarvest() method that we created just before in Harvester.cs.

				private void StartHarvest(Resource resource) {
					resourceDeposit = resource;
					StartMove(resource.transform.position, resource.gameObject);
					//we can only collect one resource at a time, other resources are lost
					if(harvestType == ResourceType.Unknown || harvestType != resource.GetResourceType()) {
						harvestType = resource.GetResourceType();
						currentLoad = 0.0f;
					}
					harvesting = true;
					emptying = false;
				}

We want to keep track of the Resource deposit the Harvester is currently working on, which requires a reference to be added to the top of Harvester.cs.

				private Resource resourceDeposit;

We then want the Harvester to move over to the Resource, since we will only collect the Resource when we are next to it. We then make sure that the current harvest type for the Harvester is the same as that of the Resource. If the harvest type changes we empty all Resources we might have had of another type. Finally we set the state of the Harvester to be harvesting rather than emptying.

Before we can carry on with our Harvester behaviour we need to make some changes to Unit.cs. We need to add another version of the StartMove() method (so add it, rather than replacing the existing StartMove() method already there)

				public void StartMove(Vector3 destination, GameObject destinationTarget) {
					StartMove(destination);
					this.destinationTarget = destinationTarget;
				}

which then requires that we add a reference to destinationTarget to the top of Unit.cs.

				private GameObject destinationTarget;

We also need to add

				destinationTarget = null;

to our original version of StartMove() to make sure that we remove any reference to a target object if it is not needed. We now need to adjust the destination position for our Unit if it is moving towards a world object - this makes sure that we do not run into that object. We start by adding this line

				if(destinationTarget) CalculateTargetDestination();

to the end of TurnToTarget() in Unit.cs, where we are changing the Unit state from rotating to moving. And now we also need to define the method being called there.

				private void CalculateTargetDestination() {
					//calculate number of unit vectors from unit centre to unit edge of bounds
					Vector3 originalExtents = selectionBounds.extents;
					Vector3 normalExtents = originalExtents;
					normalExtents.Normalize();
					float numberOfExtents = originalExtents.x / normalExtents.x;
					int unitShift = Mathf.FloorToInt(numberOfExtents);

					//calculate number of unit vectors from target centre to target edge of bounds
					WorldObject worldObject = destinationTarget.GetComponent< WorldObject >();
					if(worldObject) originalExtents = worldObject.GetSelectionBounds().extents;
					else originalExtents = new Vector3(0.0f, 0.0f, 0.0f);
					normalExtents = originalExtents;
					normalExtents.Normalize();
					numberOfExtents = originalExtents.x / normalExtents.x;
					int targetShift = Mathf.FloorToInt(numberOfExtents);

					//calculate number of unit vectors between unit centre and destination centre with bounds just touching
					int shiftAmount = targetShift + unitShift;

					//calculate direction unit needs to travel to reach destination in straight line and normalize to unit vector
					Vector3 origin = transform.position;
					Vector3 direction = new Vector3(destination.x - origin.x, 0.0f, destination.z - origin.z);
					direction.Normalize();

					//destination = center of destination - number of unit vectors calculated above
					//this should give us a destination where the unit will not quite collide with the target
					//giving the illusion of moving to the edge of the target and then stopping
					for(int i = 0; i < shiftAmount; i++) destination -= direction;
				 	destination.y = destinationTarget.transform.position.y;
				}

The algorithm being used for the calculation is outlined in the comments inside that method. The one thing we need to add for this to work is the method GetSelectionBounds() in WorldObject.cs.

				public Bounds GetSelectionBounds() {
					return selectionBounds;
				}

With these additions in place we need to make some adjustments to Update() inside Harvester.cs.

				protected override void Update () {
					base.Update();
				 	if(!rotating && !moving) {
						if(harvesting || emptying) {
							Arms[] arms = GetComponentsInChildren< Arms >();
							foreach(Arms arm in arms) arm.renderer.enabled = true;
							if(harvesting) {
								Collect();
								if(currentLoad >= capacity || resourceDeposit.isEmpty()) {
									//make sure that we have a whole number to avoid bugs
									//caused by floating point numbers
									currentLoad = Mathf.Floor(currentLoad);
									harvesting = false;
									emptying = true;
									foreach(Arms arm in arms) arm.renderer.enabled = false;
									StartMove (resourceStore.transform.position, resourceStore.gameObject);
								}
							} else {
								Deposit();
								if(currentLoad <= 0) {
				  					emptying = false;
				  					foreach(Arms arm in arms) arm.renderer.enabled = false;
				  					if(!resourceDeposit.isEmpty()) {
				  						harvesting = true;
				  						StartMove (resourceDeposit.transform.position, resourceDeposit.gameObject);
				  					}
				  				}
				  			}
				  		}
				  	}
				}

One of the things being done here is to adjust our Harvester slightly to give some more visual feedback on what it is currently up to. We will reveal the arms when it is collecting or depositing resources and hide them when the Harvester is moving or idle. In order to enable this we need to add a new wrapper script Arms.cs inside our Harvester folder

				using UnityEngine;  

				public class Arms : MonoBehaviour {
				  	//wrapper class for the arms of a harvester
				}

and attach this to each of the arm objects in our Harvester object. We then need to turn off the renderer for each of the arm object by default (the checkbox next to the renderer element for each Arm object). We now need to add two new methods to Harvester.cs for collecting and deposting resources. We will leave them both empty for the moment.

				private void Collect() {
				}

				private void Deposit() {
				}

The other thing we need to add to the top of Harvester.cs is a Building that it will deposit the Resources it collects in.

				public Building resourceStore;

For now we will set our WarFactory to be this Building, but we will change that by the end of this post. We should also set the capacity of our Harvester to be something greater than 0, say 20 for now. If you now run your game, select your Harvester, and click on the ResourceDeposit you should see it turn and move to the edge of the ResourceDeposit and then extend its arms. In order to actually collect Resources, though, we need to fill in the Collect() method we created in Harvester.cs.

				private void Collect() {
				  	float collect = collectionAmount * Time.deltaTime;
				  	//make sure that the harvester cannot collect more than it can carry
				  	if(currentLoad + collect > capacity) collect = capacity - currentLoad;
					resourceDeposit.Remove(collect);
					currentLoad += collect;
				}

We need to do something very similar for depositing resources by filling in the Deposit() method in Harvester.cs.

				private void Deposit() {
					currentDeposit += depositAmount * Time.deltaTime;
					int deposit = Mathf.FloorToInt(currentDeposit);
					if(deposit >= 1) {
						if(deposit > currentLoad) deposit = Mathf.FloorToInt(currentLoad);
						currentDeposit -= deposit;
						currentLoad -= deposit;
						ResourceType depositType = harvestType;
						if(harvestType == ResourceType.Ore) depositType = ResourceType.Money;
						player.AddResource(depositType, deposit);
					}
				}

We need to add a few variables to the top of Harvester.cs in order for these methods to work.

				public float collectionAmount, depositAmount;

				private float currentDeposit = 0.0f;

We are using the combination of collectionAmount / depositAmount and deltaTime to give a constant rate for collecting or depositing Resources. Make sure that you set collectionAmount and depositAmount from inside Unity - I have used 5 and 10 for these, to give a quicker deposit time. Notice also that we are converting Ore to Money when we go to deposit it.

If you run your game now you should be able to tell your Harvester to start collecting Ore and see the Money that the Player owns go up as the Harvester deposits the Resources it has collected.

Health Bar and Resource Bar

This whole process works very nicely, but it is hard for the Player to determine how much the Harvester has currently collected. We have all the information that we need, but we need to display some more of this to the Player. We will do this by adding some more details to our SelectionBox being drawn around a selected WorldObject. For all of our WorldObjects we want to display a health bar along the top of the selection box. For our Harvester we want to display the current load along one side as well. We will start with the health bar by modifying DrawSelectionBox() in WorldObject.cs.

				protected virtual void DrawSelectionBox(Rect selectBox) {
					GUI.Box(selectBox, "");
					CalculateCurrentHealth();
					GUI.Label(new Rect(selectBox.x, selectBox.y - 7, selectBox.width * healthPercentage, 5), "", healthStyle);
				}

All of the calculation of the details for the health bar will be done inside CalculateCurrentHealth(), which we need to add to WorldObject.cs. Once we know these we can draw the health bar along the top of the selection box.

				protected virtual void CalculateCurrentHealth() {
					healthPercentage = (float)hitPoints / (float)maxHitPoints;
					if(healthPercentage > 0.65f) healthStyle.normal.background = ResourceManager.HealthyTexture;
					else if(healthPercentage > 0.35f) healthStyle.normal.background = ResourceManager.DamagedTexture;
					else healthStyle.normal.background = ResourceManager.CriticalTexture;
				}

This requires us to define a couple more variables at the top of WorldObject.cs

				protected GUIStyle healthStyle = new GUIStyle();
				protected float healthPercentage = 1.0f;

and some extra textures inside ResourceManager.cs.

				private static Texture2D healthyTexture, damagedTexture, criticalTexture;
				public static Texture2D HealthyTexture { get { return healthyTexture; } }
				public static Texture2D DamagedTexture { get { return damagedTexture; } }
				public static Texture2D CriticalTexture { get { return criticalTexture; } }

We will modify the method in ResourceManager.cs used to store select box items to make sure that we initialize these textures.

				public static void StoreSelectBoxItems(GUISkin skin, Texture2D healthy, Texture2D damaged, Texture2D critical) {
					selectBoxSkin = skin;
					healthyTexture = healthy;
					damagedTexture = damaged;
					criticalTexture = critical;
				}

As we change this method we also need to change the call to this method from within Start() in HUD.cs

				ResourceManager.StoreSelectBoxItems(selectBoxSkin, healthy, damaged, critical);

and declare these textures at the top of HUD.cs.

				public Texture2D healthy, damaged, critical;

I have used these images for those 3 textures

and stored them in the Images folder located inside the HUD folder.

Set the hit points and max hit points for all the objects currently in our world to 100. Now if you run your game and select a WorldObject you should see a green bar sitting neatly along the top of the selection box. As you drop the value of hit points (manually at this stage) you should see the colour of the bar change and the length of the bar shrink. This is a very simple way of giving a rough indication of the amount of damage that a WorldObject has sustained so far.

For our Resources we want to handle the display of the health bar slightly differently. Let's override the CalculateCurrentHealth() method in Resource.cs, so that any new Resource we might add later on behaves the same way.

				protected override void CalculateCurrentHealth () {
					healthPercentage = amountLeft / capacity;
					healthStyle.normal.background = ResourceManager.GetResourceHealthBar(resourceType);
				}

We have actually changed the way that health percentage is calculated, along with the image being used. We need to add this method to ResourceManager.cs

				public static Texture2D GetResourceHealthBar(ResourceType resourceType) {
					if(resourceHealthBarTextures != null && resourceHealthBarTextures.ContainsKey(resourceType)) return resourceHealthBarTextures[resourceType];
					return null;
				}

along with a method that allows us to store the images in ResourceManager in the first place.

				public static void SetResourceHealthBarTextures(Dictionary< ResourceType, Texture2D > images) {
					resourceHealthBarTextures = images;
				}

We also need to add

				using System.Collections.Generic;

to the top of ResourceManager.cs and then declare the dictionary we are using to store the textures in as well.

				private static Dictionary< ResourceType, Texture2D > resourceHealthBarTextures;

Then we need to make sure that we call this method as part of the initialization process for our HUD. Add this code

				Dictionary< ResourceType, Texture2D > resourceHealthBarTextures = new Dictionary< ResourceType, Texture2D >();
				for(int i = 0; i < resourceHealthBars.Length; i++) {
					switch(resourceHealthBars[i].name) {
					case "ore":
						resourceHealthBarTextures.Add(ResourceType.Ore, resourceHealthBars[i]);
						break;
					default: break;
					}
				}
				ResourceManager.SetResourceHealthBarTextures(resourceHealthBarTextures);

to the end of Start() in HUD.cs and declare the array of textures at the top of the file.

				public Texture2D[] resourceHealthBars;

Make sure that each image you add to this array through Unity matches a case in the switch statement we just added. If you add more resource types (and want to display a health bar for them) you need to remember to add a texture to the array and a new case in that switch statment.

I have used the image below for the health bar for our OreDeposit, stored in the Images folder inside the HUD folder.

Ore Health Bar

Ore Health Bar

Running your game now should show a different health bar for the OreDeposit than the health bars being shown for your Units and Buildings (assuming you have attached your image to the HUD correctly).

The last adjustment to make here is to the selection box for our Harvester. It would be really nice to have a visual cue as to how full the Harvester currently is. To achieve this we will add another bar along the right-hand side of the selection box. This can be done by overriding DrawSelectionBox() in Harvester.cs.

				protected override void DrawSelectionBox (Rect selectBox) {
					base.DrawSelectionBox(selectBox);
					float percentFull = currentLoad / capacity;
					float maxHeight = selectBox.height - 4;
					float height = maxHeight * percentFull;
					float leftPos = selectBox.x + selectBox.width - 7;
					float topPos = selectBox.y + 2 + (maxHeight - height);
					float width = 5;
					Texture2D resourceBar = ResourceManager.GetResourceHealthBar(harvestType);
					if(resourceBar) GUI.DrawTexture(new Rect(leftPos, topPos, width, height), resourceBar);
				}

It is amazing to see how much more engaging a simple addition like this makes when playing the game.

Harvester Creation

The final thing to cover this time is the creation of a new Harvester. We will do this from a dedicated Refinery building. As part of the creation we will also register the Refinery as the resourceStore for the Harvester. Inside the Building folder create a new folder called Refinery and create a new C# script inside there called Refinery.cs.

				using UnityEngine;

				public class Refinery : Building {

					protected override void Start () {
						base.Start();
						actions = new string[] {"Harvester"};
					}

					public override void PerformAction(string actionToPerform) {
						base.PerformAction(actionToPerform);
						CreateUnit(actionToPerform);
					}
				}

Now create a new empty object called Refinery and set it's position to (0, 0, 0). Add to this a cube and a capsule, called Floor and Tower respectively. Set the transform settings for each as follows:

Now attach the Refinery.cs script to the Refinery object. Set the object name to Refinery, max build progress to 5, hit points and max hit points to 50, and attach the rally point and sell images. Now add the Refinery object to the Buildings object of your Player object. In order to be able to build our Harvester from the Refinery we need to make it into a Prefab (stored in the Harvester folder) and then attach this Prefab to the GameObjectList under Units. Run the game now and you should be able to create a Harvester from the Refinery.

The last thing to do here is to reference the Refinery from the Harvester when we create a new Harvester. We will start by adding the Building creator as a parameter to AddUnit() in Player.cs

				public void AddUnit(string unitName, Vector3 spawnPoint, Vector3 rallyPoint, Quaternion rotation, Building creator) {
				...
				}

and adjusting the call to this in ProcessBuildQueue() in Building.cs.

				if(player) player.AddUnit(buildQueue.Dequeue(), spawnPoint, rallyPoint, transform.rotation, this);

Now we want to add an initialization method to Unit.cs that can be called after a Unit is created,

				public virtual void Init(Building creator) {
					//specific initialization for a unit can be specified here
				}

add an override for this method in Harvester.cs,

				public override void Init (Building creator) {
					base.Init (creator);
					resourceStore = creator;
				}

and then call it from the end of AddUnit() in Player.cs.

				public void AddUnit(string unitName, Vector3 spawnPoint, Vector3 rallyPoint, Quaternion rotation, Building creator) {
					...
					if(unitObject) {
						unitObject.Init(creator);
						if(spawnPoint != rallyPoint) unitObject.StartMove(rallyPoint);
					}
				}

Running your game now you should be able to create a Harvester from your Refinery, set it collecting resources from the OreDeposit, and see it return to the Refinery to deposit the Resources it has collected.

Final Fix

There is one situation we need to handle before we close - the case where the OreDeposit is empty. Our Harvester is already stopping collection once the Resources are all gone. However, as our project stands at the moment we can still select the OreDeposit even when it is empty. We can fix this by changing MouseClick() 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;
							ChangeSelection(worldObject, controller);
						}
					}
				}

to stop the Player from being able to select the Resource when it is empty. Also change SetHoverState() 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) {
						bool moveHover = false;
						if(hoverObject.name == "Ground") {
							moveHover = true;
						} else {
							Resource resource = hoverObject.transform.parent.GetComponent< Resource >();
							if(resource && resource.isEmpty()) moveHover = true;
						}
						if(moveHover) player.hud.SetCursorState(CursorState.Move);
					}
				}

to make sure that we show the move cursor over the space where an empty Resource sits, since it is empty ground now. Finally change 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) {
						bool clickedOnEmptyResource = false;
						if(hitObject.transform.parent) {
							Resource resource = hitObject.transform.parent.GetComponent< Resource >();
							if(resource && resource.isEmpty()) clickedOnEmptyResource = true;
						}
						if((hitObject.name == "Ground" || clickedOnEmptyResource) && 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);
						}
					}
				}

to make sure that we can tell a Unit to move to the space occupied by an empty Resource.

And that wraps it up for this time. We have created a Resource, a Harvester and a Refinery. The Harvester can collect from the Resource and return this to the Refinery for money. While we were at it we added display of health bars to the selection box. A long part, but I hope that it has been worthwhile. The full code for this part can be found at github under the commit for Part 12. Next time we will continue to extend what the Player can do by enabling them to construct new Buildings.

<
>