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

<
>

Part 11: Rally Point and Selling Buildings

The plan this time is to add a rally point to our Buildings (rather than just having a hard-coded spawn point) and then giving the Player the ability to change that to suit them. While we are at it we will give the Player the ability to sell a Building if they wish.

Rally Point

If we are to have a rally point we need some way to portray where that is to the Player. To do so, let's create a flag that can easily be repositioned. We will then show / hide that flag depending on whether we have a Building selected or not. Inside the Player folder create a new folder and call it RallyPoint. Inside that folder create a new C# script called RallyPoint.cs. All we want this to be able to do is to show and hide our RallyPoint when required, which is actually very straightforward. It turns out that this is all the code we need in RallyPoint.cs.

				public void Enable () {
					Renderer[] renderers = GetComponentsInChildren< Renderer >();
					foreach(Renderer renderer in renderers) renderer.enabled = true;
				}

				public void Disable () {
					Renderer[] renderers = GetComponentsInChildren< Renderer >();
					foreach(Renderer renderer in renderers) renderer.enabled = false;
				}

These methods find all of the child objects of the RallyPoint object which can be rendered and enable or disable them.

Now create a new empty object and call it RallyPoint. Once again create some space by shifting the WarFactory belonging to our Player to (-30, 0, 30). (While you are at it make sure that the position for the Buildings and Units wrapper objects is set to (0, 0, 0) as well) Add two cubes to the RallyPoint object called Flag and Pole. Set their transform properties as follows:

To make our RallyPoint more distinguishable we will add some custom materials to each part of it. Inside the Assets folder create a new folder and call it Materials. Inside this folder create two new Materials called Metal and Flag. Now we need to customise our materials by changing some of their properties.

For the flag use the following values:

Then for the metal use these settings:

Once you have done this add the Metal Material to the Mesh Renderer for the Pole object and the Flag Material to the Mesh Renderer for the Flag object. If you run your game now you should see a flag sitting in the middle of the map. By default we actually don't want to see this RallyPoint. To change this we can turn off the renderer for both the Flag and the Pole objects (uncheck the check box next to the text MeshRenderer for each of them).

We have created the RallyPoint object, now we need to attach the RallyPoint script we created to it so that we can issue commands to the RallyPoint. Once this is done drag the the RallyPoint object down into the RallyPoint folder to create a Prefab that we can use at will. The idea is to have one RallyPoint object per Player (since they can only have one Building selected at a time), so add an instance of the RallyPoint Prefab to our Player.

The control for displaying the RallyPoint object is going to happen inside the SetSelection() method for a Building. This means that we are going to need to provide an overridden version of this method to Building.cs.

				public override void SetSelection(bool selected, Rect playingArea) {
					base.SetSelection(selected, playingArea);
					if(player) {
						RallyPoint flag = player.GetComponentInChildren< RallyPoint >();
						if(selected) {
							if(flag && player.human && spawnPoint != ResourceManager.InvalidPosition && rallyPoint != ResourceManager.InvalidPosition) {
								flag.transform.localPosition = rallyPoint;
								flag.transform.forward = transform.forward;
								flag.Enable();
							}
						} else {
							if(flag && player.human) flag.Disable();
						}
					}
				}

The only special thing to note here is that we are setting the position of the RallyPoint just before we enable it. This makes sure that the RallyPoint is always in the correct position for the selected Building. We are also doing so only if the Building is owned by a human Player. This does mean that we need to add a variable to the top of Building.cs to track the rally point

				protected Vector3 rallyPoint;

and then initialize it in Awake() by setting the default rally point to be the spawn point for that Building.

				rallyPoint = spawnPoint;

To allow this method to be overridden we must also change the method declaration of SetSelection() in WorldObject.cs.

				public virtual void SetSelection(bool selected, Rect playingArea) {
				...
				}

Now when you run your game you should only see the RallyPoint when you have the WarFactory selected, since that is the only Building that our Player owns at the moment.

Shifting the Rally Point

Given that we now have a rally point defined, it would be really useful (from the Player's perspective) to be able to place that at an aribitrary location. If we allow this Units will then be created at the spawn point and told to move towards the rally point. This will enable the Player to create Units from a variety of Buildings and then have them automatically group together in the same place. At the moment we are not handling collisions, so this army will end up literally in the same space, but that is a problem for another day (probably not as part of this tutorial).

But before we can move the RallyPoint we need a way of receiving this input from the Player. This will involve creating a new action that the Player can initiate from a Building. We then need to change some state so that when we are dealing with user input or drawing the cursor we can provide the correct behaviour. We will initiate this action from our HUD, so we need to draw some more 'buttons' in our actions bar. To handle this drawing we need to add

				DrawStandardBuildingOptions(selectedBuilding);

into DrawOrdersBar() in HUD.cs immediately after our call to DrawBuildQueue() and then define that method as follows.

				private void DrawStandardBuildingOptions(Building building) {
					GUIStyle buttons = new GUIStyle();
					buttons.hover.background = smallButtonHover;
					buttons.active.background = smallButtonClick;
					GUI.skin.button = buttons;
					int leftPos = BUILD_IMAGE_WIDTH + SCROLL_BAR_WIDTH + BUTTON_SPACING;
					int topPos = buildAreaHeight - BUILD_IMAGE_HEIGHT / 2;
					int width = BUILD_IMAGE_WIDTH / 2;
					int height = BUILD_IMAGE_HEIGHT / 2;
					if(building.hasSpawnPoint()) {
						if(GUI.Button(new Rect(leftPos, topPos, width, height), building.rallyPointImage)) {
							if(activeCursorState != CursorState.RallyPoint) SetCursorState(CursorState.RallyPoint);
							else {
								//dirty hack to ensure toggle between RallyPoint and not works ...
								SetCursorState(CursorState.PanRight);
								SetCursorState(CursorState.Select);
							}
						}
					}
				}

This basically just defines a button located at the bottom of our Orders bar (above the selection name) and what to do when that button is clicked. In this case we are saying that if the button is clicked we want to enable / disable the ability to change the rally point for our Building. But before we can test this there are some things we need to add. First up, we need some more textures so add

				public Texture2D smallButtonHover, smallButtonClick;

to the top of HUD.cs and

				public Texture2D rallyPointImage;

to the top of Building.cs. I have used the textures below in my project.

Add smallButtonClick.png and smallButtonHover. into the Images folder located inside your HUD folder. These then need to be added to your HUD (once we have fixed all the errors and Unity will allow you to). Add rallyPoint.png into your Buildings folder and add it to any Buildings which can have their rally point changed (remember to add these to the Prefab, not to instances you have in game).

Now add the method hasSpawnPoint() to Building.cs

				public bool hasSpawnPoint() {
					return spawnPoint != ResourceManager.InvalidPosition && rallyPoint != ResourceManager.InvalidPosition;
				}

so that we can check whether we need to draw this button or not. Finally, we need to add another value to our CursorState Enum found in Enums.cs, resulting in it having these states available.

				public enum CursorState { Select, Move, Attack, PanLeft, PanRight, PanUp, PanDown, Harvest, RallyPoint }

If you run your game now you should see the newly created button displaying when you select your WarFactory.

In order to display our new cursor state we need to add a new cursor texture to HUD.cs

				public Texture2D rallyPointCursor;

and assign an image to it. I have used the image below, stored in the Cursors folder.

Rally Point Cursor

Rally Point Cursor

We then need to add the following case to the switch statement in SetCursorState() (found in HUD.cs)

				case CursorState.RallyPoint:
					activeCursor = rallyPointCursor;
					break;

to make sure that we can draw that image when necessary. Add

				else if(activeCursorState == CursorState.RallyPoint) topPos -= activeCursor.height;

to the end of the if...else block in GetCursorDrawPosition() to make sure that the cursor is drawn where we are expecting it to be (in this case we expect the click point for the cursor to be at the base of the flag pole).

There is one more piece of the puzzle needed to allow us to see our cursor. At the moment the default behaviour for hovering over the ground is to display the select cursor. We need to modify this for our Building by overriding SetHoverState() in Building.cs.

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

We check to see if the previous cursor state for the HUD was set to RallyPoint, since the current state would have been set to Select (or something else) by now. If it was we want to change it back to RallyPoint so that our rally point cursor is shown (which will then enable the Player to change the rally point for the selected Building). We need to implement this method

				public CursorState GetPreviousCursorState() {
					return previousCursorState;
				}

in HUD.cs as well as adding this variable to the top of the class

				private CursorState previousCursorState;

and then make sure that it gets set at the top of SetCursorState().

				if(activeCursorState != newState) previousCursorState = activeCursorState;

Now that we are storing the previous cursor state we should also update the check in DrawStandardBuildingOptions() that handles the button click to this

				if(activeCursorState != CursorState.RallyPoint && previousCursorState != CursorState.RallyPoint) SetCursorState(CursorState.RallyPoint);

to enable us to cancel the RallyPoint state by clicking the RallyPoint button again. We should really change the state of this button in some way if we are currently allowing the Player to change the RallyPoint, but I will leave it up to you to figure that one out for yourself. Run your game now and you should be able to see the cursor changing state correctly now (when it is inside the playing area of course).

The only thing left to do now is to enable the actual change of the RallyPoint location once the Player clicks a new spot on the ground. To do so we need to override the MouseClick() method inside Building.cs.

				public override void MouseClick(GameObject hitObject, Vector3 hitPoint, Player controller) {
					base.MouseClick(hitObject, hitPoint, controller);
					//only handle iput if owned by a human player and currently selected
					if(player && player.human && currentlySelected) {
						if(hitObject.name == "Ground") {
							if((player.hud.GetCursorState() == CursorState.RallyPoint || player.hud.GetPreviousCursorState() == CursorState.RallyPoint) && hitPoint != ResourceManager.InvalidPosition) {
								SetRallyPoint(hitPoint);
							}
						}
					}
				}

This brings up two new methods that we need to create. The first is GetCursorState() in HUD.cs

				public CursorState GetCursorState() {
					return activeCursorState;
				}

and the second is SetRallyPoint() in Building.cs.

				public void SetRallyPoint(Vector3 position) {
					rallyPoint = position;
					if(player && player.human && currentlySelected) {
						RallyPoint flag = player.GetComponentInChildren< RallyPoint >();
						if(flag) flag.transform.localPosition = rallyPoint;
					}
				}

This adjusts the RallyPoint for the Building and then moves the RallyPoint object if the Building is selected.

Now that we have a spawn point and a rally point we need to get newly created Units moving from one to the other, otherwise there is no point to all the work we have just done. We will do this by adding the rally point as a parameter to AddUnit() in Player.cs and then telling the Unit that has been added to move towards the rally point, but only if that is not the same as the spawn point. This is the resulting method.

				public void AddUnit(string unitName, Vector3 spawnPoint, Vector3 rallyPoint, Quaternion rotation) {
					Units units = GetComponentInChildren< Units >();
					GameObject newUnit = (GameObject)Instantiate(ResourceManager.GetUnit(unitName),spawnPoint, rotation);
					newUnit.transform.parent = units.transform;
					Unit unitObject = newUnit.GetComponent< Unit >();
					if(unitObject && spawnPoint != rallyPoint) unitObject.StartMove(rallyPoint);
				}

Of course, now we need to update the call to AddUnit() in ProcessBuildQueue() found in Building.cs.

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

Run your game now, select the WarFactory, change the RallyPoint, create a new Tank, and watch as it moves from the spawn point to the new rally point. Now that is what I call progress!

Selling a Building

While we are on the topic of specific actions that a Building can perform, let's grant the Player the ability to sell each of their Buildings. With all the work that we have done already this is actually almost trivial. Add

				if(GUI.Button(new Rect(leftPos, topPos, width, height), building.sellImage)) {
					building.Sell();
				}

to DrawStandardBuildingOptions() in HUD.cs immediately above the check to draw the button for moving the rally point. Then add the extra texture to the top of Building.cs

				public Texture2D sellImage;

along with the method Sell().

				public void Sell() {
					if(player) player.AddResource(ResourceType.Money, sellValue);
					if(currentlySelected) SetSelection(false, playingArea);
					Destroy(this.gameObject);
				}

Here we grant the Player whatever we have set the sell value for the Building, we make sure that we deselect it if necessary, and then we destroy it. I have used this image for the sell image for my Buildings.

Sell Image

Sell Image

Since we are now always showing a sell button for our Building, we should make sure that we change the left position of the button we created for shifting the rally point. Add this code

				leftPos += width + BUTTON_SPACING;

to DrawStandardBuildingOptions() in HUD.cs, as the first thing to do if we detect the building has the option to change the rally point. Add the sell image to the WarFactory, set a sell value, and then you can test selling it. It should disappear when you click the button and the Player should receive the amount of money that you specified as the sell value.

Right, that wraps things up for this time. We have added two standard options for Buildings - the ability to change the rally point for created Units and the ability to sell the Building. The full source code for this time can be found on github under the commit for part 11. When we come back next time we will look at adding resources to our world and enabling collection of those.

<
>