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

<
>

Part 5: Buildings and Basic Selection

Now that we have created a framework for WorldObjects, let's begin an implementation for a Building. We will create the basic framework for a Building, attached to a simple object for now. We will then enable selection of that a WorldObject (using our Building as the test case) - along with a basic display in the HUD of what has been selected.

Basic Building

First off, let's create a new script in the Building folder that we created last time called Building.cs. The first thing we need to do is to make sure that our Building inherits from WorldObject rather than from MonoBehavior. To do so, change the class definition to the following.

				public class Building : WorldObject {

This is the C# way of saying that we inherit from another class. Once we have done this, we need to override the Unity methods that we declared virtual in our WorldObject. For now we will simply get them to call their base implementation. You should have the following inside your Building class now.

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

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

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

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

This forms the most basic framework that we need for our building, but we will be adding to this over time. Now create a new empty object in Unity and rename it Building. (GameObject->Create Empty) Add two cubes to this new object - we will call them Floor1 and Floor2. (GameObject->Create Other->Cube) Make sure that the Transform properties for the Building object have the following settings:

For Floor1 set them to: And for Floor2 set them to: You will notice that the cube which we created in the first part is now in the way. To move it, set the Position property for it's Transform to (0, 2, -20). This moves the cube 20 units down the z-axis and out of the way of our new Building. The final thing we need to do with our Building is to attach the Building script that we created just before to our Building object.

World Object Selection

The first thing we need to add when we want to look at implementing selection of WorldObjects is a method for handling that input. You will recall that we attached a script called UserInput.cs to our Player back in Part 2. We will make use of this script to handle responding to mouse input for our Player, beyond the simple control of our camera we have at the moment. In the Update() method of UserInput.cs, after the call to CameraRotation(), add a call to MouseActivity(). Then create that method with the following implementation.

				private void MouseActivity() {
					if(Input.GetMouseButtonDown(0)) LeftMouseClick();
					else if(Input.GetMouseButtonDown(1)) RightMouseClick();
				}

This method is simply detecting whether the left or right mouse button has been clicked and is passing on handling of that event to the appropriate method. Now we must implement both of these methods to complete our handling of mouse input from the Player. We will use the left mouse button for selecting objects and performing actions for a selected object and then we will use the right mouse button for cancelling the selection of that object. Let us look at the left mouse click first. Add the following code into a private method called LeftMouseClick().

				private void LeftMouseClick() {
					if(player.hud.MouseInBounds()) {
						GameObject hitObject = FindHitObject();
						Vector3 hitPoint = FindHitPoint();
						if(hitObject && hitPoint != ResourceManager.InvalidPosition) {
							if(player.SelectedObject) player.SelectedObject.MouseClick(hitObject, hitPoint, player);
							else if(hitObject.name!="Ground") {
								WorldObject worldObject = hitObject.transform.root.GetComponent< WorldObject >();
								if(worldObject) {
									//we already know the player has no selected object
									player.SelectedObject = worldObject;
									worldObject.SetSelection(true);
								}
							}
						}
					}
				}

At the moment, most of this code will fail since we have not implemented the appropriate properties and methods. But before we get onto that, I want to spend just a moment going over the logic of this piece of code. The first thing to take note of here is that we only wish to handle mouse clicks which happen inside our playing area. If the mouse is somewhere inside our HUD we will let the HUD handle mouse input. Now, since our left click is being used for selection and action, we need to determine what the user hit. There are two distinct possibilities here - either the Player clicked on a WorldObject, or they clicked on the ground somewhere(at the moment this is simply a flat plane, but this could potentially become actual terrain with hills, etc.). We will create a pair of helper methods to determine what the point in the world was that the Player clicked on, and which object (if any) they clicked on. By adding the field InvalidPosition to our ResourceManager we are able to handle clicks in unidentifiable places (for example the sky), which can then be ignored. So if we have a valid object that we clicked on (even if it was only the ground), and the point which we clicked on was valid, we want to do something with that mouse click. It is important to note that what happens with the mouse click will change depending on whether or not there is already an object selected. If there is, we need to let that object handle the mouse click. If not, we need to determine whether an object needs to be selected or not. If the object clicked on was not the ground, and it was a WorldObject, we can select it. If not we can ignore the mouse click altogether. It turns out that this small piece of code is actually handling a lot of different scenarios.

Now it is time to add in all the pieces of code that this method requires. The first check we have here is to see whether the mouse is inside the playing area or not. Add a reference to the HUD for a Player to the top of the Player.cs

				public HUD hud;

and then initialize this in the Start() method for the Player.

				hud = GetComponentInChildren< HUD >();

This finds the HUD script that we added to our Player. Now we need to add the method to the HUD which actually determines whether the mouse is inside the playing area or not. Adding the following code to HUD.cs should do it.

				public bool MouseInBounds() {
					//Screen coordinates start in the lower-left corner of the screen
					//not the top-left of the screen like the drawing coordinates do
					Vector3 mousePos = Input.mousePosition;
					bool insideWidth = mousePos.x >= 0 && mousePos.x <= Screen.width - ORDERS_BAR_WIDTH;
					bool insideHeight = mousePos.y >= 0 && mousePos.y <= Screen.height - RESOURCE_BAR_HEIGHT;
					return insideWidth && insideHeight;
				}

Note here that Unity is a little weird in that the origin for the screen coordinates is in a different place than the origin for the drawing coordinates is. This is an annoyance, but it can be worked around as long as you are aware of it. This method simply finds the current position of the mouse and determines whether it is inside the playing area or if it is over part of the HUD.

Let us now find out which object, if any, the user clicked on by adding the following method to UserInput.cs.

				private GameObject FindHitObject() {
					Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
					RaycastHit hit;
					if(Physics.Raycast(ray, out hit)) return hit.collider.gameObject;
					return null;
				}

By default this method will return a null object. To find which object was hit we make use of some Unity methods. The ray is a line running from the point on screen where the Player clicked into the world, from the perspective of our main camera (the only camera that we have in our world). Physics.Raycast() then traces this line and finds the first object in the world to be hit. If it finds it this is stored in the variable hit, if not the method call returns false. If we do find an object we return the parent of that object. Slightly complicated, but all of the dirty work is actually being handled by Unity for us, which is nice.

We do a very similar thing to find the point in the world on which the Player clicked. This can be seen in the following method, which also needs to be added to UserInput.cs.

				private Vector3 FindHitPoint() {
					Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
					RaycastHit hit;
					if(Physics.Raycast(ray, out hit)) return hit.point;
					return ResourceManager.InvalidPosition;
				}

There are a couple of places now where we are referencing the value InvalidPosition, so we should add it to ResourceManager.cs now with the following code.

				private static Vector3 invalidPosition = new Vector3(-99999, -99999, -99999);
				public static Vector3 InvalidPosition { get { return invalidPosition; } }

By creating the private variable invalidPosition we are saving ourselves from needing to create a new Vector3 every time InvalidPosition is referenced. Remember, every little bit can add up very quickly to lots of unnecessary work which in turn slows our game down. Now it is time to add a reference to the current selection to our Player for the current selection. Add the following variable to the public variables at the top of Player.cs.

				public WorldObject SelectedObject { get; set; }

This is C#'s way of creating a hidden private variable exposed through a getter and setter method. Our Player is now ready to store a WorldObject that it has selected. With this in place, let us now create the MouseClick() method 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.root.GetComponent< WorldObject >();
						//clicked on another selectable object
						if(worldObject) ChangeSelection(worldObject, controller);
					}
				}

This method will define the basic handling for a mouse click on a WorldObject. Any special details are left up to any subclasses to define. You will note that we are passing in a Player to this method. That Player is the one who is controlling input at the moment. It may not be the Player which owns this WorldObject, which is why we need a reference to them. The basic idea here is to determine whether the object clicked on was a WorldObject and to select it. For now this will allow us to change what has been selected, although that will not be obvious until next time (feel free to play around with adding more buildings and trying it out though). Later on we will add some more logic in here, but for now we will implement the ChangeSelection() method that is being called here.

				private void ChangeSelection(WorldObject worldObject, Player controller) {
					//this should be called by the following line, but there is an outside chance it will not
					SetSelection(false);
					if(controller.SelectedObject) controller.SelectedObject.SetSelection(false);
					controller.SelectedObject = worldObject;
					worldObject.SetSelection(true);
				}

We need to make sure that the object the controlling Player has selected is deselected (which should be this object, though there is a chance that it might not be). Once we have done so we can set the current selection for the controlling Player to be the object which was clicked on. We then make sure the object is told that it has been selected.

HUD Display

Now that we have code in place to select a WorldObject, it would be good to add some basic visual feedback to the Player. For now we will make this as simple as displaying the name of the selected object at the top of the orders bar in our HUD. To do so, add the following code to HUD.cs inside the DrawOrdersBar() method. Note: this should go between the call to GUI.Box() and GUI.EndGroup().

				string selectionName = "";
				if(player.SelectedObject) {
					selectionName = player.SelectedObject.objectName;
				}
				if(!selectionName.Equals("")) {
					GUI.Label(new Rect(0,10,ORDERS_BAR_WIDTH,SELECTION_NAME_HEIGHT), selectionName);
				}

If the Player has nothing selected then we do not wish to draw anything. However, if they do we wish to get the name of that object and draw that in a label at the top of our orders bar. You will need to create the constant variable for selection height at the top of HUD.cs as follows.

				private const int SELECTION_NAME_HEIGHT = 15;

The other thing you might like to do is to define the settings for a Label under the skin OrdersSkin. This will allow you to set up whatever styling you want. I have used the settings shown in the screenshot below to get some nicely centred text.

Label Settings

Label Settings

There is just one more thing to do before running your code to test the new WorldObject selection. As you will have noticed, we are going to display the name of the Building that we select. But before we can do this, we actually need to set the name for our Building. To do so, click on the Building object that you created in Unity back near the start of this part. In the inspector you will see a number of variables for which you can set values (the public variables declared at the top of our Building script and our WorldObject script). Enter a value, any value, in the object name field. Now when you run the project in Unity and select your Building this name should appear at the top of your orders bar area.

Deselection

The last thing left to do this week is to implement the right click method for handling input from the mouse. Remember, we are going to use this to cancel whatever selection the Player currently has. The snippet of code below should do just nicely in UserInput.cs.

				private void RightMouseClick() {
					if(player.hud.MouseInBounds() && !Input.GetKey(KeyCode.LeftAlt) && player.SelectedObject) {
						player.SelectedObject.SetSelection(false);
						player.SelectedObject = null;
					}
				}

Once again we make sure that the mouse is inside the playing area. Also, remember that if we are holding down the left alt button and clicking with the right mouse button we are rotating our camera. Therefore, if that is the case we need to ignore the right mouse click. If not, we can deselect whatever the Player had selected and then set their selection to null (which indicates nothing selected).

Right, another longer part out of the way, but we have achieved lots this time. We are now in a position to define specific types of Buildings. And we can now select objects in our world - whether they belong to our Player or not - and display the name of the currently selected object to the Player. As always, the complete code from the end of this post can be found on github under the commit for Part 5. Next time we will look at creating a basic Unit and drawing a selection box around things when the Player selects them.

<
>