Creating an RTS in Unity: Part X

Update: This can now also be found at stormtek.geek.nz/rts_tutorial/part10.php, where the entire tutorial is now being hosted.

The goal this time around is to introduce the ability to create new units from buildings. While we are at it we will also implement a build queue, enabling the player to create multiple units from a single building.

Build Queue

Let’s start out by defining a build queue for our building and methods for interacting with it. The first thing that we need to do is to add some variables to the top of Building.cs.

public float maxBuildProgress;
protected Queue< string > buildQueue;
private float currentBuildProgress = 0.0f;
private Vector3 spawnPoint;

We will use currentBuildProgress and maxBuildProgress to make sure that a unit takes time to build. This will be a standard production speed for all units produced from that building. We will probably want to upgrade this at a later date, but a flat production speed will be fine for now. By making maxBuildProgress public we can play around with it in Unity – set it to 10 for now.

To have access to the Queue we need to add

using System.Collections.Generic;

to the very top of Building.cs. We also need to make sure that our queue and spawn point are initialized so add

buildQueue = new Queue< string >();
float spawnX = selectionBounds.center.x + transform.forward.x * selectionBounds.extents.x + transform.forward.x * 10;
float spawnZ = selectionBounds.center.z + transform.forward.z + selectionBounds.extents.z + transform.forward.z * 10;
spawnPoint = new Vector3(spawnX, 0.0f, spawnZ);

to Awake(). We are setting the spawn point to be in the middle of the front wall of our building, set out by a bit to make sure that the unit is not created inside the building. To actually create a unit we need a method that will add a unit to our build queue.

protected void CreateUnit(string unitName) {
	buildQueue.Enqueue(unitName);
}

Once we can add items to the build queue we want a way to make sure that it gets emptied too, so add the following line to Update()

ProcessBuildQueue();

and then add the definition of this method as well.

protected void ProcessBuildQueue() {
	if(buildQueue.Count > 0) {
		currentBuildProgress += Time.deltaTime * ResourceManager.BuildSpeed;
		if(currentBuildProgress > maxBuildProgress) {
			if(player) player.AddUnit(buildQueue.Dequeue(), spawnPoint, transform.rotation);
			currentBuildProgress = 0.0f;
		}
	}
}

This method is saying that if we have items in our build queue then we need to update our build progress. If this is now greater than our maximum allowed build progress then we need to tell the Player that owns this building to create the unit at the front of the queue. Once this is done we need to reset our current build progress to 0 so that we can start building the next unit in the queue. You will notice the reference to ResourceManager.BuildSpeed. This is used to make sure that all buildings update their progress for building units at the same rate. By adjusting maxBuildProgress we can make some buildings take longer to complete their work than others. We should add that variable to ResourceManager now

public static int BuildSpeed { get { return 2; } }

and then add

using RTS;

to the top of Building to make sure that we can access it. We also need to add the method for the creation of a unit to Player.cs too, which we will come back to later in this post.

public void AddUnit(string unitName, Vector3 SpawnPoint, Quaternion rotation) {
	Debug.Log ("add " + unitName + " to player");
}

In order to draw things correctly in our HUD we also need to be able to retrieve some details about our build queue (from Building.cs).

public string[] getBuildQueueValues() {
	string[] values = new string[buildQueue.Count];
	int pos=0;
	foreach(string unit in buildQueue) values[pos++] = unit;
	return values;
}

public float getBuildPercentage() {
	return currentBuildProgress / maxBuildProgress;
}

This will allow us to find out what the current entries in the build queue are as well as how far through constructing the current Unit the Building is.

Prefabs and Global list of types

To actually create a new Unit we need to a way to create a fully fledged Unit. The last thing we want to have to do is to painstakingly define how to create each of our units and buildings at runtime. Thankfully Unity gives us some help in this regard through the concept of prefabs. The basic overview is that a prefab serves as a template for an object instance. So we can define the collection of objects and scripts that make up our complex object, as we have done for Unit and for Building already. We can then drag our object from the Hierarchy view down into a folder in the project view, which will create a prefab object there. Once you have a prefab you can then drag it from your folder into your scene and have multiple identical copies of the same object without having to create each of them from scratch. There is also a way to create and instance of a prefab at runtime in our code (which we will get to later on in this post). Any changes you make to a prefab are immediately copied to all instances of that prefab in your scene. However, it is important to note that any changes made to an instance are not copied to the prefab. This is a very good thing, but it means that you need to remember to make any development changes to the prefab, not to an instance of it. If you want to know more details (which is always a good thing to want when using things like this) check out the Unity documentation.

Having prefabs is very useful. However, what we also need is a list of available units and buildings which we can create at runtime. Having them stored nicely in folders within our project is good, but it will not help us when our game is running. Therefore, we will create a new object whose purpose is to contain a reference to all the prefabs that we can use in our game. Inside your Assests directory create a new folder called Resources. Inside this folder create a new C# script called GameObjectList.cs. Now create a new empty object, rename it to GameObjectList, and attach the script to it.

Let us start off by defining the lists of objects that we wish to keep track of in GameObjectList.

public GameObject[] buildings;
public GameObject[] units;
public GameObject[] worldObjects;
public GameObject player;

All of these arrays need to be of type GameObject, since we will be storing prefabs in them. We are now able to drop appropriate prefabs into these lists from within Unity. We will add a couple of prefabs later in this tutorial, once we are ready to create some actual units from an actual building.

We want to make sure that we can only ever have one instance of our GameObjectList present on a map at all times. This will make more sense later on once we add in menu systems and the ability to save and load our maps. We will use a boolean variable to determine whether the GameObjectList has already been created.

private static bool created = false;

By defining this as a static variable we are making sure that it is not tied down to any particular instance of our GameObjectList. We now need to define the initialization logic inside the Unity Awake method.

void Awake() {
	if(!created) {
		DontDestroyOnLoad(transform.gameObject);
		ResourceManager.SetGameObjectList(this);
		created = true;
	} else {
		Destroy(this.gameObject);
	}
}

The Unity method call DontDestroyOnLoad makes sure that if we load a new map this object will persist. This is good because it means we will only need to initialise this once, at the entry point for our game. The call to Destroy when created is set to true guarantees that we only ever have one instance present on a map. We are also giving ResourceManager a reference to this object so that we have an easy way of calling any methods that we need to later on. We should add this method to ResourceManager now

public static void SetGameObjectList(GameObjectList objectList) {
	gameObjectList = objectList;
}

as well as the variable gameObjectList which is being set in this method.

private static GameObjectList gameObjectList;

We also need to add

using RTS;

to the top of GameObjectList to make sure that we can reference ResourceManager.

Now let us define some accessor methods within our GameObjectList to allow us to retrieve any object that we want.

public GameObject GetBuilding(string name) {
	for(int i=0; i<buildings.Length; i++) {
		Building building = buildings[i].GetComponent< Building >();
		if(building && building.name == name) return buildings[i];
	}
	return null;
}

public GameObject GetUnit(string name) {
	for(int i=0; i<units.Length; i++) {
		Unit unit = units[i].GetComponent< Unit >();
		if(unit && unit.name == name) return units[i];
	}
	return null;
}

public GameObject GetWorldObject(string name) {
	foreach(GameObject worldObject in worldObjects) {
		if(worldObject.name == name) return worldObject;
	}
	return null;
}

public GameObject GetPlayerObject() {
	return player;
}

public Texture2D GetBuildImage(string name) {
	for(int i=0; i<buildings.Length; i++) {
		Building building = buildings[i].GetComponent< Building >();
		if(building && building.name == name) return building.buildImage;
	}
	for(int i=0; i<units.Length; i++) {
		Unit unit = units[i].GetComponent< Unit >();
		if(unit && unit.name == name) return unit.buildImage;
	}
	return null;
}

We also need to add some wrapper methods to ResourceManager which we will use to access these methods in GameObjectList.

public static GameObject GetBuilding(string name) {
	return gameObjectList.GetBuilding(name);
}

public static GameObject GetUnit(string name) {
	return gameObjectList.GetUnit(name);
}

public static GameObject GetWorldObject(string name) {
	return gameObjectList.GetWorldObject(name);
}

public static GameObject GetPlayerObject() {
	return gameObjectList.GetPlayerObject();
}

public static Texture2D GetBuildImage(string name) {
	return gameObjectList.GetBuildImage(name);
}

This layer of abstraction also allows us to forget about how these values are being stored when it comes to the majority of our code base. All we care about is that our ResourceManager knows how to retrieve the items that we want whenever we want them.

Initiate Unit Creation

We now have a way to access all of the objects we might want to use in our world (even though this is not yet populated). We have also defined a way for a building to add units to a build queue and then to initiate construction of those. What we are still lacking is a way to tell a building when to add a unit to it’s build queue, as well as what that unit is. To do this we will make use of actions, which we loosely defined quite a while back for a WorldObject.

But before we get onto that, we should first define our first actual building. Once we have this we will define a build action for it along with how to handle it. We will start with a WarFactory, since we have what looks like a tank floating round. Besides, what better way to advance our cause than by building an army? Let’s first create some space to put our new WarFactory in by setting the z-position for our existing unit to 25. Now create a new empty object, rename it to WarFactory, and make sure that it’s position is set to (0, 0, 0). Add 5 cubes to this object called GroundFloor, Corner1, Corner2, Corner3, and Corner4. Set their transform properties as follows:

  • GroundFloor: position = (0, 1.5, 0), scale = (10, 3, 10)
  • Corner1: position = (-4, 3.5, -4)
  • Corner2: position = (-4, 3.5, 4)
  • Corner3: position = (4, 3.5, 4)
  • Corner4: position = (4, 3.5, -4)

Inside the Building folder create a new folder called WarFactory. Inside that folder create a new C# script called WarFactory. Make sure that this script inherits from Building and overrides the Start method, defining an action that we are going to want to perform at some point.

public class WarFactory : Building {
	protected override void Start () {
		base.Start();
		actions = new string[] { "Tank" };
	}
}

Here each action we want to perform is the name of a unit to construct. These names will need to match the names of any prefabs that we are wanting to build. Since we have Tank as part of this list we are going to need to create a prefab later for a Tank unit. Now attach this script to the WarFactory object we just created. Finally, drag the WarFactory object down into the WarFactory folder in the project view. This will create a prefab object called WarFactory in that folder. Any further changes that we make to WarFactory should be made to this prefab.

For completeness we should also create a build image for our WarFactory. To satisfy Unity’s import ratios we will make this to be 64×64, but we will make use of some transparency to give us a smaller image than that. (This is to help make sure that we do not get weird scaling of the image) I have gone with a transparent border of 13 pixels (which will make more sense by the end of this post) to give me the image below.

War Factory

War Factory

It is up to you to make your images as ornate or simplistic as possible. All we want is a representation of the building so that by glancing at it players will be able to tell what they are choosing to make. Now drag this image onto the Build Image property of your WarFactory (remember to use the prefab).

We should create our Tank object now too, but for this we will cheat slightly. Rename your Unit object to Tank. Now create a new folder inside the Unit folder called Tank and create a C# script called Tank inside that. We want this to inherit from Unit and to override the Start method (similar to what we did with our WarFactory just before).

public class Tank : Unit {
	protected override void Start () {
		base.Start ();
	}

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

Now we want to remove the Unit script from our Tank object and replace it with the Tank script that we just created. Dragging the Tank object into the Tank folder will give us a Tank prefab. We also need a BuildImage for our tank, something like the one below,

Tank

Tank

which we need to attach to the Build Image property of our Tank prefab. Let’s now add our two newly created prefab’s to our GameObjectList, making sure to add them to the appropriate lists.

Now that we finally have a building which will create a type of unit, it is time to display that available action to the player and to allow them to initiate that action. This will be done through our HUD, but only if the player has selected the WarFactory. We should first add a WarFactory to our player, since we will only be displaying actions to them if they own the building / unit that is selected. The last thing we want is for all players to have full control of all buildings and units on the map. Inside the DrawOrdersBar method we want to add the following code immediately after we have set the selectionName.

if(player.SelectedObject.IsOwnedBy(player) {
	//reset slider value if the selected object has changed
	if(lastSelection && lastSelection != player.SelectedObject) sliderValue = 0.0f;
	DrawActions(player.SelectedObject.GetActions());
	//store the current selection
	lastSelection = player.SelectedObject;
}

The method IsOwnedBy will allow us to determine whether to show actions etc. in the HUD. Create that method in WorldObject now.

public bool IsOwnedBy(Player owner) {
	if(player && player.Equals(owner)) {
		return true;
	} else {
		return false;
	}
}

We are wanting to call the method DrawActions() and pass it the actions that the currently selected object has. Since there may be more actions than can fit on screen we are going to implement a scroll bar for our list of actions. I mention this now, since if the selection changes we want to make sure that we are showing the list from the top, not from where a previous list may have been scrolled to, which is why we are making use of the variable lastSelected. The variable sliderValue is what we are using to determine what position the scrollbar should be drawn at. We should define these at the top of HUD now.

private WorldObject lastSelection;
private float sliderValue;

Obviously we now need to define DrawActions().

private void DrawActions(string[] actions) {
	GUIStyle buttons = new GUIStyle();
	buttons.hover.background = buttonHover;
	buttons.active.background = buttonClick;
	GUI.skin.button = buttons;
	int numActions = actions.Length;
	//define the area to draw the actions inside
	GUI.BeginGroup(new Rect(0,0,ORDERS_BAR_WIDTH,buildAreaHeight));
	//draw scroll bar for the list of actions if need be
	if(numActions <= MaxNumRows(buildAreaHeight)) DrawSlider(buildAreaHeight, numActions / 2.0f);
	//display possible actions as buttons and handle the button click for each
	for(int i=0; i<numActions; i++) {
		int column = i % 2;
		int row = i / 2;
		Rect pos = GetButtonPos(row, column);
		Texture2D action = ResourceManager.GetBuildImage(actions[i]);
		if(action) {
			//create the button and handle the click of that button
			if(GUI.Button(pos, action)) {
				if(player.SelectedObject) player.SelectedObject.PerformAction(actions[i]);
			}
		}
	}
	GUI.EndGroup();
}

There are a lot of things going on in this method, and a lot of things we need to define in order for it to work. First up we are defining a custom skin for just the buttons (this is actually just a modification of whatever skin happens to be active). This is making use of two textures to simulate a button hover and a button click for our build actions. I have used the following two images

Button Click

Button Click

Button Hover

Button Hover

in my project. We then need fields for these at the top of HUD,

public Texture2D buttonHover, buttonClick;

and then we need to assign the textures we added to the fields. (Add the textures into the Images folder found inside the HUD folder) You may need to wait with adding the textures from inside Unity until we have added all the pieces of code that our method still needs …

We need to define some constant values for the dimensions of our build image

private const int BUILD_IMAGE_WIDTH = 64, BUILD_IMAGE_HEIGHT = 64;

and a value for determining the height of the area we will draw our actions in.

private int buildAreaHeight = 0;

By using this value we can make sure that we always fill the available screen space with our build area. We need to initialize this in our Start method, which is called when our HUD is created.

buildAreaHeight = Screen.height - RESOURCE_BAR_HEIGHT - SELECTION_NAME_HEIGHT - 2 * BUTTON_SPACING;

I have added this initialization immediately after we have added all of our resource types to the HUD. This initialization introduces another constant that we need to add.

private const int BUTTON_SPACING = 7;

With all the variables now declared, we are left with 3 methods that need to be added to HUD.

private int MaxNumRows(int areaHeight) {
	return areaHeight / BUILD_IMAGE_HEIGHT;
}

private Rect GetButtonPos(int row, int column) {
	int left = SCROLL_BAR_WIDTH + column * BUILD_IMAGE_WIDTH;
	float top = row * BUILD_IMAGE_HEIGHT - sliderValue * BUILD_IMAGE_HEIGHT;
	return new Rect(left,top,BUILD_IMAGE_WIDTH,BUILD_IMAGE_HEIGHT);
}

private void DrawSlider(int groupHeight, float numRows) {
	//slider goes from 0 to the number of rows that do not fit on screen
	sliderValue = GUI.VerticalSlider(GetScrollPos(groupHeight),sliderValue,0.0f,numRows-MaxNumRows(groupHeight));
}

This in turn introduces one more method that we need to add

private Rect GetScrollPos(int groupHeight) {
	return new Rect(BUTTON_SPACING,BUTTON_SPACING,SCROLL_BAR_WIDTH,groupHeight - 2 * BUTTON_SPACING);
}

and this has a final constant to add.

private const int SCROLL_BAR_WIDTH = 22;

We are drawing our build options from the top of the order bar area, so let’s just change the display of the name to the bottom of that area (the rest of our draw code is actually already expecting this). We do this by changing the top position of the label for our selection name (found at the end of DrawOrdersBar()).

if(!selectionName.Equals("")) {
	int topPos = buildAreaHeight + BUTTON_SPACING;
	GUI.Label(new Rect(0,topPos,ORDERS_BAR_WIDTH,SELECTION_NAME_HEIGHT), selectionName);
}

Of course, if we want to see a name for our WarFactory we are going to need to set a value for it’s ObjectName field. We also need to make sure this is set for our tank. If you now run your project from inside Unity and select your WarFactory you should see the build image for the tank being displayed. To see that the scroll bar works (and the full two column display in action) we can change the number of times Tank is present in the actions list for our WarFactory up to something like 17. If you entered 17 instances of “Tank” to actions, you should see 17 images of a tank in your orders bar, though you may need to scroll down to see them all.

To actually start the construction of the unit we are going to make use of the PerformAction method. If you recall from when we were writing DrawActions(), the actual click of the button that is being made from the build images is calling player.SelectedObject.PerformAction(). So to initiate the build from our WarFactory add the following override for PerformAction.

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

If we run this now we should see the debug message we added to Player.AddUnit() showing almost immediately after we click on the buildImage for Tank in our WarFactory. If we change the value of Max Build Progress for the WarFactory up to 5 we can see that it now takes some 5 seconds for the message to be displayed.

Create Actual Unit

Now that we can add units into our build queue, it is time to actually create an instance of that Unit and add it to our Player. This needs to take place in AddUnit, where we just tested that the code reaches once a Unit reaches the end of the build queue. We need to add this code to that method.

Units units = GetComponentInChildren< Units >();
GameObject newUnit = (GameObject)Instantiate(ResourceManager.GetUnit(unitName),spawnPoint, rotation);
newUnit.transform.parent = units.transform;

Units is simply a wrapper object used within our player to give ready access to all of the Units that they currently have. Create a new C# script Units inside the folder for Unit and also strip out all the code in that class.

public class Units : MonoBehaviour {
	//wrapper class for unit listing for a player
}

We will do the same thing now for Buildings. Inside the Building folder create a new C# script called Buildings.cs and strip all of the code in the class.

public class Buildings : MonoBehaviour {
	//wrapper class for building listing for a player
}

Now create two new empty objects inside Unity, one called Units and the other called Buildings. Add both of these objects to your Player object. Now add WarFactory to Buildings and Tank to Units.

If we take a look back at the AddUnit method now I will explain briefly where the magic happens. The Unity method Instantiate takes a prefab, a position, and a rotation and returns us an instance of that prefab at that location. We then tell that new object that it’s parent object is to be the wrapper objects for the Units that our Player owns. Just like that we have added the newly created Unit to our Player. Feel free to run this in Unity now. You should see a new Tank appearing in front of the WarFactory once it has finished it’s time in the build queue (assuming you just told the WarFactory to make a new Tank). If you now look at the Units object for the Player you should see that it now contains Tank and Tank(Clone).

Display Build Queue

There is one last thing that I want to add in this time (and I know this post has turned out really long …) – display of our build queue. At the moment there is no indication that a Unit is being created, or how much longer that is going to take either, it simply appears. This final addition also needs to take place within HUD. Add this code

Building selectedBuilding = lastSelection.GetComponent< Building >();
if(selectedBuilding) {
	DrawBuildQueue(selectedBuilding.getBuildQueueValues(), selectedBuilding.getBuildPercentage());
}

to DrawOrdersBar() right after we set lastSelection. Note that we are making sure that the player has a building selected before attempting to draw the build queue. And now for the definition of DrawBuildQueue.

private void DrawBuildQueue(string[] buildQueue, float buildPercentage) {
	for(int i=0; i<buildQueue.Length; i++) {
		float topPos = i * BUILD_IMAGE_HEIGHT - (i+1) * BUILD_IMAGE_PADDING;
		Rect buildPos = new Rect(BUILD_IMAGE_PADDING,topPos,BUILD_IMAGE_WIDTH,BUILD_IMAGE_HEIGHT);
		GUI.DrawTexture(buildPos,ResourceManager.GetBuildImage(buildQueue[i]));
		GUI.DrawTexture(buildPos,buildFrame);
		topPos += BUILD_IMAGE_PADDING;
		float width = BUILD_IMAGE_WIDTH - 2 * BUILD_IMAGE_PADDING;
		float height = BUILD_IMAGE_HEIGHT - 2 * BUILD_IMAGE_PADDING;
		if(i==0) {
			//shrink the build mask on the item currently being built to give an idea of progress
			topPos += height * buildPercentage;
			height *= (1 - buildPercentage);
		}
		GUI.DrawTexture(new Rect(2 * BUILD_IMAGE_PADDING,topPos,width,height),buildMask);
	}
}

Once again we need another constant value

private const int BUILD_IMAGE_PADDING = 8;

and access to two more textures.

public Texture2D buildFrame, buildMask;

Both of these textures need to be 64×64, and can look something like this.

Build Frame

Build Frame

Build Mask

Build Mask

These are used to provide a little more definition to the build queue – buildFrame provides a border around each buildImage, and buildMask is used to show the user how far through the current build we are on the object at the front of the queue. If you run this now you will see that the display works, it is just in a strange place. The idea is to have our build queue displayed just to the left of our orders area. But to do that we are going to need to tweak some of our existing positioning code for our orders bar. Basically we need to shift the start of the drawing area left by BUILD_IMAGE_WIDTH and then shift all of the other drawing right by that much. In DrawOrdersBar we need to change these two lines for setup of the draw area

GUI.BeginGroup(new Rect(Screen.width-ORDERS_BAR_WIDTH-BUILD_IMAGE_WIDTH,RESOURCE_BAR_HEIGHT,ORDERS_BAR_WIDTH+BUILD_IMAGE_WIDTH,Screen.height-RESOURCE_BAR_HEIGHT));
GUI.Box(new Rect(BUILD_IMAGE_WIDTH+SCROLL_BAR_WIDTH,0,ORDERS_BAR_WIDTH,Screen.height-RESOURCE_BAR_HEIGHT),

as well as the position of our selection name,

if(!selectionName.Equals("")) {
	int leftPos = BUILD_IMAGE_WIDTH + SCROLL_BAR_WIDTH / 2;
	int topPos = buildAreaHeight + BUTTON_SPACING;
	GUI.Label(new Rect(leftPos,topPos,ORDERS_BAR_WIDTH,SELECTION_NAME_HEIGHT), selectionName);
}

and this line fixes the draw area for DrawActions.

GUI.BeginGroup(new Rect(BUILD_IMAGE_WIDTH,0,ORDERS_BAR_WIDTH,buildAreaHeight));

There, that looks better. Feel free to run it now and check it out. If you add lots of build actions to the WarFactory now (e.g 17 odd entries of “Tank”) you will also notice that the scroll bar now sits nicely outside our orders area as well.

Well, I think that about does it for this time. Sorry for the length of the post, but I felt that all of these steps needed to be presented together. If you want to enable building of new units from a building it is now as simple as copying what we did with WarFactory. Just remember that each action should correspond to the name of a prefab that you want to create, and that the prefabs for the units you want need to be added to GameObjectList. Make those small changes and all of the hard work of build queues and display etc. have been taken care of already. The full code for this post is up on github under the commit for Part10.

Advertisements

Creating an RTS in Unity: Part IX

Update: This can now also be found at stormtek.geek.nz/rts_tutorial/part9.php, where the entire tutorial is now being hosted.

It is now almost time to begin allowing the player to produce more units and buildings. But before we can do that we need to add in resources for the player, along with a display of what they currently have into the HUD.

Resources for the Player

To keep things simple for this tutorial we will introduce two basic resource types: money and power. Money will be used to buy everything that the player will want to build or upgrade. Power will be used by buildings (although we will probably not introduce any penalties as part of this tutorial). The main reason for adding power is that we will want to handle power a little differently from resources like money. It provides things for the base, rather than acting as a consumable resource like money. Once we are handling power correctly it should not be hard to add consequences for running out of power.

The first thing we need to do for a player is to give them the ability to store resources. As part of this, we are going to make it so that the amount of resources that a player can have is limited by storage capacity. So to gain lots of money they are going to need somewhere sensible to store this. For a player we will determine a base storage capacity (they can always hold $1000 for example) that can then be expanded during the game. Add the following variables to the top of Player.cs.

public int startMoney, startMoneyLimit, startPower, startPowerLimit;
private Dictionary<ResourceType,int> resources, resourceLimits;

To allow the code to make use of Dictionary add

using System.Collections.Generic;

to the very top of the file. We will use this to map a resource type to a quantity. By doing things this way we can actually write most of our code to handle details independent of the particular resource in question. This allows us to easily add / remove resource types or change what those are. We will use this concept for both the amount of resources a player has and their capacity for that resource. The other 4 variables allow us to play around with how much money and power the player starts with, as well as how much of each of those they can store before needed dedicated storage facilities. If we wish to add more resource types later on we will also need to add more variables for the start values for that resource type.

Let us now add another entry to Enums.cs to define our resource types. We will use the following entry for now.

public enum ResourceType { Money, Power }

Remember that to access this from Player we also need to add

using RTS;

to the top of the file.

Now that we have somewhere to store resources, let’s add in the logic for initializing those values. We need to add the Unity method Awake() to our Player with the following code in it.

void Awake() {
	resources = InitResourceList();
	resourceLimits = InitResourceList();
}

We will use the method InitResourceList to provide the initialization for any collection of resources that needs to be handled – for now just our resources and the storage capacities for those resources. Let’s implement this method now.

private Dictionary<ResourceType, int> InitResourceList() {
	Dictionary<ResourceType, int> list = new Dictionary<ResourceType, int>();
	list.Add(ResourceType.Money, 0);
	list.Add(ResourceType.Power, 0);
	return list;
}

The last thing we need to do when setting up our Player is to add their start resources (and limits). We do this by adding

AddStartResourceLimits();
AddStartResources();

to the end of the Start method and then implementing each of these methods as follows.

private void AddStartResourceLimits() {
	IncrementResourceLimit(ResourceType.Money, startMoneyLimit);
	IncrementResourceLimit(ResourceType.Power, startPowerLimit);
}

private void AddStartResources() {
	AddResource(ResourceType.Money, startMoney);
	AddResource(ResourceType.Power, startPower);
}

Once again, each of these methods uses helper methods to add values for each resource type, which also need to be implemented.

public void AddResource(ResourceType type, int amount) {
	resources[type] += amount;
}

public void IncrementResourceLimit(ResourceType type, int amount) {
	resourceLimits[type] += amount;
}

Resource display in HUD

Our player now has a way to store resources and the limits for those, as well as start values for both of those. Now it is time to display this amount to the user, which is done through our HUD. We will start by adding

if(human) {
	hud.SetResourceValues(resources, resourceLimits);
}

to the Update method for our Player. Now, obviously, we need to provide an implementation of this within HUD.cs.

public void SetResourceValues(Dictionary<ResourceType, int> resourceValues, Dictionary<ResourceType, int> resourceLimits) {
	this.resourceValues = resourceValues;
	this.resourceLimits = resourceLimits;
}

This simply sets the values of the HUD version of resourceValues and resourceLimits to be those specified. This means that we need to declare those at the top of HUD

private Dictionary<ResourceType,int> resourceValues, resourceLimits;

and initialize them at the beginning of Start().

resourceValues = new Dictionary<ResourceType, int>();
resourceLimits = new Dictionary<ResourceType, int>();

Now that we have current values for the player being sent to the HUD we are ready to present that information to the player. This means updating our DrawResourceBar method for HUD. Add the following code between GUI.Box() and GUI.EndGroup()

int topPos = 4, iconLeft = 4, textLeft = 20;
DrawResourceIcon(ResourceType.Money, iconLeft, textLeft, topPos);
iconLeft += TEXT_WIDTH;
textLeft += TEXT_WIDTH;
DrawResourceIcon(ResourceType.Power, iconLeft, textLeft, topPos);

and then implement the method DrawResourceIcon.

private void DrawResourceIcon(ResourceType type, int iconLeft, int textLeft, int topPos) {
	Texture2D icon = resourceImages[type];
	string text = resourceValues[type].ToString() + "/" + resourceLimits[type].ToString();
	GUI.DrawTexture(new Rect(iconLeft, topPos, ICON_WIDTH, ICON_HEIGHT), icon);
	GUI.Label (new Rect(textLeft, topPos, TEXT_WIDTH, TEXT_HEIGHT), text);
}

This simply finds the appropriate image and text for the resource and then draws them in the specified location. If we ever want to change where a particular resources is displayed on screen all we need to change is the values being passed to DrawResourceIcon().

For both of these methods to work properly we need to declare some more constants at the top of HUD.

private const int ICON_WIDTH = 32, ICON_HEIGHT = 32, TEXT_WIDTH = 128, TEXT_HEIGHT = 32;

We also need to provide a way to drop in and then access textures for our resource icons. We will use an array of textures so that we can drop in any resources we like from within Unity

public Texture2D[] resources;

and then a dictionary to provide us easy access to those images at runtime.

private Dictionary<ResourceType,Texture2D> resourceImages;

Now we just need to add a final bit of initialization into Start() before we can do any drawing.

resourceImages = new Dictionary<ResourceType, Texture2D>();
for(int i=0; i<resources.Length; i++) {
	switch(resources[i].name) {
		case "Money":
			resourceImages.Add(ResourceType.Money, resources[i]);
			resourceValues.Add(ResourceType.Money, 0);
			resourceLimits.Add(ResourceType.Money, 0);
			break;
		case "Power":
			resourceImages.Add(ResourceType.Power, resources[i]);
			resourceValues.Add(ResourceType.Power, 0);
			resourceLimits.Add(ResourceType.Power, 0);
			break;
		default: break;
	}
}

Here we are making sure that the appropriate textures are associated with the appropriate resources. While we are at it we make sure that we have default values for the amount of resources and capacities to be displayed.

Now we need to provide the textures from inside Unity. First we should create somewhere to store these … Create a folder inside the HUD folder called Icons. Inside here we want to place two 32×32 textures (called Money and Power) to identify the resources to the player. In keeping with the theme of simplicity I have gone with the icons below (but you are more than welcome to construct your own).

  • Power IconPower
  • Money IconMoney

Once you have some images in Unity, drag them onto the resources field of the HUD. If you run your game now you will see the icons showing up, along with 0/0 for each resource (since we have not set starting values for the player yet), but the text is weirdly positioned. This is because we need to change the settings for Label inside our ResourceSkin. We want to set the border to 2, margin and padding to 0, and the alignment to MiddleCenter. Now when you run your game things will sit much nicer. There is quite a large gap between the text and the image since we need to allow space for when the player has lots of resources (although feel free to play around with the positioning yourself). Let’s start the player off with $1000, the ability to hold $2000, 500 units of power, and a power capacity of 500 units. If you run the game now you should see that the player has 1000/2000 for money and 500/500 for power.

I think that brings us neatly to then end of this part. For the final version of the code from this part check out the commit for Part 9 on github.