Creating an RTS in Unity: Part XI

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

The plan this time is to add in a rally point (rather than just having a hard-coded spawn point) and then give the player the ability to change that. While we are at it we will add in the ability to sell a Building.

Rally Point

Having a rally point means we need some way to portray where that is to the user. To do so, let’s create a flag that can easily be repositioned. We will then show / hide that 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. All we want this to be able to do is to show and hide our RallyPoint when required.

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

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

These methods find all of the child objects 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 to (0, 0, 0) as well) Add two cubes to the RallyPoint object called Flag and Pole. Set their transform properties as follows:

  • Flag: position = (0, 3, 2), scale = (0.1, 2, 4)
  • Pole: position = (0, 1.5, 0), scale = (0.1, 6, 0.1)

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.

Flag

  • Shader: Specular
  • Main Colour: (125, 170, 220, 255) – for (RGBA)
  • Specular Colour: (200, 245, 255)
  • Shininess: about 25%

Metal

  • Shader: Specular
  • Main Colour: (120, 120, 120, 255)
  • Specular Colour: (150, 140, 140, 255)
  • Shininess: about 50%

Now add the Metal Material to the Mesh Renderer for the Pole object that is part of our new RallyPoint object and the Flag Material to the Mesh Renderer for the Flag object. If you run your game now you should see a flag in the middle of the map. By default we actually don’t want to see this RallyPoint. To do this we can turn off the renderer for both the Flag and the Pole objects (uncheck the check box next to the text MeshRenderer).

We have created the RallyPoint object, now attach the RallyPoint script we created to it so that we can issue commands to the RallyPoint. Then 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 Building an overridden version of this method.

public override void SetSelection(bool selected, Rect playingArea) {
	base.SetSelection(selected, playingArea);
	if(player) {
		RallyPoint flag = player.GetComponentInChildren();
		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 Building 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.

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 it is the only Building that our Player owns at the moment.

Shifting the Rally Point

Now that we have a rally point defined, it would be really useful from the player’s perspective to be able to place that wherever they like. Our units will then be created at the spawn point and told to move towards the rally point. This allows the player to create units from a variety of buildings and then have them automatically group together as an army. 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 rally point we need a way of receiving this input from the user. This will involve creating a new action that the Player can initiate from a Building and changing some state accordingly for when we are dealing with user input (and for drawing the cursor as well). We will initiate this action from our HUD, so we need to draw some more ‘buttons’ in our actions bar. We will handle the drawing of this by adding

DrawStandardBuildingOptions(selectedBuilding);

into DrawOrdersBar immediately after our call to DrawBuildQueue() and then defining this 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. 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.

  • Small button click

    SmallButtonClick

  • Small button hover

    SmallButtonHover

  • Rally point

    RallyPoint

Add smallButtonClick 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 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 to give us these states.

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

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

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

public Texture2D rallyPointCursor;

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

Rally point image

RallyPointImage

We then need to add the following case to the switch statement in SetCursorState() 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.

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, add 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 button again. We should really change the state of this button in some way if we are currently allowing the player to change the rally point, 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 is 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 in a new spot on the ground. To do so we need to override the MouseClick() method inside Building.

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 rally point 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 and then telling the Unit that has been added to move towards the rally point, but only if it 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() for Building.

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!

Sell 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 immediately above the check to draw the button for moving the rally point. Then add the extra texture to the top of Building

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 value we have set 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

SellImage

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, 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.

Right, I think 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.

Advertisements

15 thoughts on “Creating an RTS in Unity: Part XI

  1. Dude from the Interwebz says:

    Hello Mr. Elgar,

    Ive read/watched/created loads of unity3d tutorials.
    This one is by far the most useful, comprehensive and thoughtful piece of work I found. Great Work!

  2. Donki says:

    Hello..I have a problem…When i am creating new tank from warfactory i am getting this error <>…What i have forgot..?

  3. Donki says:

    This Error :
    NullReferenceException: Object reference not set to an instance of an object
    Player.AddUnit (System.String unitName, Vector3 spawnPoint, Vector3 rallyPoint, Quaternion rotation) (at Assets/Player/Player.cs:58)

    • Could you please post the entirety of the AddUnit method that is in Player.cs as well as any places where player.addUnit() is called? I just need a little bit more context as to what is causing the error.

      At a guess I would say that your warfactory is not attached to a player though. If this is the case then the player object for warfactory will be null. And then calling player.AddUnit(…) from warfactory will give a NullReferenceException since you are trying to call a method on a null object.

  4. anon says:

    “We then need to add the following case to the switch statement in SetRallyCursor()”

    Method name should be SetCursorState()?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s