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

<
>

Part 2: Players and Camera

Last time we installed Unity and got a basic scene up and running. Now it is time to begin to craft our game. Here we will look at creating a Player to interact with and using that Player to move the camera around. This will also involve our first brush with coding using C# scripting.

Players

We are creating a strategy game that centres around players building up bases and armies in the hopes of map-wide domination. This means that almost everything we do will be through players, whether human or computer controlled. For the purposes of this tutorial we will assume that there will be one human player and one or more computer players. At a later date multiplayer support may be added in, but that is very low on the list of priorities for now. All user input, as well as displaying details back to the user, will be handled by the human player. Later on we will add in some basic Artificial Intelligence to control the computer player(s) so that we can at least have some basic interactivity.

Therefore, before we can do anything useful, we must create a Player that we can begin to interact with. Create an empty object that will serve as a wrapper for everything related to our player and rename it Player. (GameObject -> Create Empty)

Since we wish to interact with a Player, we are going to need some scripts to define what the Player can and cannot do. We will use two scripts to do this - one for the Player, and one for handling user input for the Player. Firstly, to keep things organized as the project grows, lets create a folder inside our Assets directory and call it Player. Anything to do with a Player will be put inside this folder. Now create two C# scripts inside this folder - call them Player.cs and UserInput.cs.

Drag both of these newly created scripts onto the empty object we created just before to add them to our Player object. This will allow us to begin defining behaviour etc. for our Player. Well done, you now have the basic framework in place for interacting with a Player in your game - although nothing has been specified just yet.

Player Details

For now, we don't need to define much for our new player. In fact, all we want is a way to identify different players, and to determine whether a player is human or not. To do this, we simply need to add two variables to our script.

Open Player.cs in your editor of choice. The only thing that really matters here is the ability to edit the file, since all of our interactions with the code itself happen through Unity. I just use the default editor that ships with Unity since that is all set up to talk with Unity. Add the following two variables to the top of the class.

				public string username;
				public bool human;

Once done your copy of Player.cs should like the following:

				using UnityEngine;
				using System.Collections;

				public class Player : MonoBehaviour {

					public string username;
					public bool human;

					// Use this for initialization
					void Start () {

					}

					// Update is called once per frame
					void Update () {

					}
				}

We will leave Start() and Update() empty for now, but as our Player gets becomes complex later on some extra things will be added. Now return to Unity and select your Player object. You should see in the Inspector that you can enter a name for your Player and define whether it is a human player or not. Enter your name as the username and set your Player to be human. The Inspector for your Player should look similar to the image below.

Initial Player Settings

Initial Player Settings

Resource Manager

There is one more thing to add before we get started on the camera. In a number of places we are going to encounter the desire to use some constant values - for things like camera pan, height of the camera off the ground, width of buttons etc. We are also going to want to be able to have easy access to some global variables (there is a time and a place for these) and we will want all parts of the game to be able to access them - eg current map name. Therefore, we are going to create a static class to handle all of this for us. This will be the Resource Manager for our game.

Now, to handle things that are not part of Unity, we are going to create our own namespace - RTS. This will allow us to give any class access to our code by simply adding

				using RTS;

to the top of the file. We will store all of our namespaced files in a folder called RTS. Create this folder inside your Assests directory now. Inside this folder create a new C# script, rename it ResourceManager.cs, and open it for editing.

There are a number of things we need to do straight away to make this script useful to us. First off, we can get rid of the automatically generated Unity methods, since we will never be using them. Also, make sure that the class no longer inherits from MonoBehaviour (since none of that functionality makes sense) and change it to a static class. Finally, we need to add the namespace declaration around the class definition. The resulting file should look like this:

				using UnityEngine;
				using System.Collections;

				namespace RTS {
					public static class ResourceManager {

					}
				}

Note that we still want to use UnityEngine, since that gives us access to all of the Unity framework. Remember, also, that any variables or methods we add to ResourceManager will also need to be declared as static. We will add some details further on as we need them, but for now the basic frame is there for us to build on.

Camera Input

Now it is time to do our first interesting thing for this tutorial - handle user input for the camera. Once this has been sorted, we should not need to touch the camera again, so let's try to get it right first time. Open UserInput.cs for editing. As a start let's reference our ResourceManager by adding

				using RTS;

to the top of the file. Next up we want a reference to the Player for whom we are actually handling the input. Add

				private Player player;

to the top of your class. This is a private variable, since we only want this class to be able to interact with it. We will initialize this Player when the class is created, since it will never be changing. To do so, add the following line to the Start() method:

				player = transform.root.GetComponent< Player >();

This code basically tells Unity to go to the root of the object that this script belongs to (in this case the empty object we created and called Player) and then find the Player component that we added (referenced by the Player.cs script we created just before). Now we can interact with our Player directly whenever we want / need to.

A quick sidenote here ... To make things easier on my end for displaying the source code I need to make sure that things inside angle brackets have a space either side of the thing in the angle brackets. I do believe that C# ignores these spaces at runtime, so simply copying the code from here should work with no problems. I know it looks a little weird, but it is the easiest solution by far.

Now inside the Update() method add

				if(player.human) {
					MoveCamera();
					RotateCamera();
				}

Note here that we are only going to handle input for a human Player. This means that our update method will do nothing for all computer Players (at the moment anyway), which will help reduce the amount of work that we are doing each update. By keeping work low to begin with we can help to keep our framerate up, making sure that our game runs as smoothly as possible.

We now need to define the two methods that we are calling from inside our Update() method. MoveCamera() will handle moving the camera around our world. Since we are working in 3 dimensions, it will also be useful to be able to rotate the camera to look behind us, which is what RotateCamera() will handle. Create these now, leaving them empty, and making sure that they are both private to ensure that only this class can interact with them.

				private void MoveCamera() {

				}

				private void RotateCamera() {

				}

Before we fill in either of those methods, let's add some speed modifiers to ResourceManager.cs. For now we will set them to be a reasonable speed for us. At a later date these could be updated via an options menu to allow players to customize the environment to suit their taste. The following values should do it ...

				public static float ScrollSpeed { get { return 25; } }
				public static float RotateSpeed { get { return 100; } }

Note: this is the standard C# way of define getters for private variables within a class. Here we are defining that we always want to get a constant value back. This value could easily be stored in a private variable within the class. Doing that would also make things easier to customise later on (e.g. from an options menu), but this is fine for now.

Camera Movement

Now it is time to make the camera move around the map. We will initiate this whenever the mouse moves to the edges of the screen. To make it a little more forgiving, let's define an area a few pixels in from the edge of the screen where scrolling will happen. We will do this by adding the following

				public static int ScrollWidth { get { return 15; } }

to our ResourceManager. This will allow us to get the width of our scrollable area, but not to change it - which is just how we want it work.

Now it is time to add code to MoveCamera(). These next blocks of code should be added there, in the sequence that they are mentioned.

The first thing to do is to define the current mouse position as well as a new vector for our movement, like so:

				float xpos = Input.mousePosition.x;
				float ypos = Input.mousePosition.y;
				Vector3 movement = new Vector3(0,0,0);

Now it is time to handle the moving of the camera around the map. We are going to use the y-axis for height, so the x-axis and the z-axis are to be used for direction in our world. In terms of user input we will use the x-axis for the horizontal screen movement and the z-axis for the vertical screen movement. We use this

				//horizontal camera movement
				if(xpos >= 0 && xpos < ResourceManager.ScrollWidth) {
					movement.x -= ResourceManager.ScrollSpeed;
				} else if(xpos <= Screen.width && xpos > Screen.width - ResourceManager.ScrollWidth) {
					movement.x += ResourceManager.ScrollSpeed;
				}

				//vertical camera movement
				if(ypos >= 0 && ypos < ResourceManager.ScrollWidth) {
					movement.z -= ResourceManager.ScrollSpeed;
				} else if(ypos <= Screen.height && ypos > Screen.height - ResourceManager.ScrollWidth) {
					movement.z += ResourceManager.ScrollSpeed;
				}

code to add movement in the appropriate axis when the mouse is inside the region that we defined for movement - in this case the first 15 pixels in from each edge of the screen.

Now, as I mentioned before, our camera is sitting inside a 3-dimensional world. This means that it is more than likely that our camera has been rotated in some interesting fashion. So if we simply add our movement vector to the position of the camera it will appear to do weird things ... (feel free to comment this next bit out at some point and see what I mean). What we want is for our camera to move forwards in the direction that it is pointing. Thankfully, Unity has a method attached to the camera that will do this for us.

				//make sure movement is in the direction the camera is pointing
				//but ignore the vertical tilt of the camera to get sensible scrolling
				movement = Camera.mainCamera.transform.TransformDirection(movement);
				movement.y = 0;

That does the trick quite nicely. Note that we are changing the vertical movement back to 0 to ensure that the camera pans around nicely but does not have weird up and down movement at the same time. To add vertical movement we can use the scroll wheel on the mouse.

				//away from ground movement
				movement.y -= ResourceManager.ScrollSpeed * Input.GetAxis("Mouse ScrollWheel");

Now we are ready to add our desired movement to the camera. In order to do so, we must first calculate the destination of our camera, which is easy enough to do.

				//calculate desired camera position based on received input
				Vector3 origin = Camera.mainCamera.transform.position;
				Vector3 destination = origin;
				destination.x += movement.x;
				destination.y += movement.y;
				destination.z += movement.z;

Now that we know where we want the camera to go, there is one final check we wish to make. To keep things sensible we should make sure that the user cannot move the camera down through the ground, and also that they cannot move it so far up that nothing can be seen clearly anymore. Add the following limits to ResourceManager.cs

				public static float MinCameraHeight { get { return 10; } }
				public static float MaxCameraHeight { get { return 40; } }

for the minimum and maximum heights allowed for our camera. This will work fine for our world, since the ground will always be flat. However, if you add terrain at a later date you will need to rethink how this is being calculated. Now we can add the following check to MoveCamera() to keep the camera inside the limits that we have set.

				//limit away from ground movement to be between a minimum and maximum distance
				if(destination.y > ResourceManager.MaxCameraHeight) {
					destination.y = ResourceManager.MaxCameraHeight;
				} else if(destination.y < ResourceManager.MinCameraHeight) {
					destination.y = ResourceManager.MinCameraHeight;
				}

At last we are ready to add the desired movement to the camera itself, which we will only do so if the camera has actually moved. Unity nicely provides us with a static reference to the main camera which we will make use of here.

				//if a change in position is detected perform the necessary update
				if(destination != origin) {
					Camera.mainCamera.transform.position = Vector3.MoveTowards(origin, destination, Time.deltaTime * ResourceManager.ScrollSpeed);
				}

If you now go into Unity and hit play you should be able to move your camera around the map with no trouble at all.

Camera Rotation

The final thing to cover in this post is rotating the camera. We will do this when the user holds down the right mouse button. However, we will also be using the right mouse click to deselect items later on, so we need to throw a modifier key into the mix to prevent confusion / reduce mistakes. We will allow the Player to rotate the camera, but only if they are pressing the ALT key while holding down the right mouse button. This will prevent any accidental movement of the camera. Add the following code to the RotateCamera() method.

				Vector3 origin = Camera.mainCamera.transform.eulerAngles;
				Vector3 destination = origin;

				//detect rotation amount if ALT is being held and the Right mouse button is down
				if((Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) && Input.GetMouseButton(1)) {
					destination.x -= Input.GetAxis("Mouse Y") * ResourceManager.RotateAmount;
					destination.y += Input.GetAxis("Mouse X") * ResourceManager.RotateAmount;
				}

				//if a change in position is detected perform the necessary update
				if(destination != origin) {
					Camera.mainCamera.transform.eulerAngles = Vector3.MoveTowards(origin, destination, Time.deltaTime * ResourceManager.RotateSpeed);
				}

Take note that we are allowing rotation around the camera's vertical axis as well as tilting up and down to change the angle with which we are viewing the ground. It turns out that it is a lot easier to rotate the camera than it is to move it around sensibly.

Congratulations! You now have a working real-time strategy style camera that can comfortably move around your world, handling 3 dimensions comfortably. Feel free to take it for a spin and get a feel for how we will be navigating the world we are building up.

Note: I realize now (as I put things up in a nicer format for reading) that this is not quite as easy / nice to use as I might imply. However, working in changes to how I have done things now is problematic. I am going to leave things as they are at this stage. In a later post (once things are up and running) I am thinking of doing a 'Tidy Up' entry which will address some niggles - such as the camera. I will probably tie this in with talking about options that the user can play with, since that will involve mucking around with the camera a bit anyway. Until that point, the camera is going to remain as it is. Having said all that, it is still very cool to see it all working in (mostly) the way we want / expect it to.

Final Source Code

For completion this week, here is the completed code for UserInput.cs and for ResourceManager.cs. Remember, all of the code can be found on my github account. Changing to the commit for the part you are working through will give the state of the code at the end of that part (in this case it is part 2). From now on I will not post full code here, since things are going to get quite complicated quite fast.

In the next part we will look into creating the basic outline for a heads-up display to present information to the user.

UserInput.cs

				using UnityEngine;
				using System.Collections;
				using RTS;

				public class UserInput : MonoBehaviour {

					private Player player;

					// Use this for initialization
					void Start () {
						player = transform.root.GetComponent< Player >();
					}

					// Update is called once per frame
					void Update () {
						if(player && player.human) {
							MoveCamera();
							RotateCamera();
						}
					}

					private void MoveCamera() {
						float xpos = Input.mousePosition.x;
						float ypos = Input.mousePosition.y;
						Vector3 movement = new Vector3(0,0,0);

						//horizontal camera movement
						if(xpos >= 0 && xpos < ResourceManager.ScrollWidth) {
							movement.x -= ResourceManager.ScrollSpeed;
						} else if(xpos <= Screen.width && xpos > Screen.width - ResourceManager.ScrollWidth) {
							movement.x += ResourceManager.ScrollSpeed;
						}

						//vertical camera movement
						if(ypos >= 0 && ypos < ResourceManager.ScrollWidth) {
							movement.z -= ResourceManager.ScrollSpeed;
						} else if(ypos <= Screen.height && ypos > Screen.height - ResourceManager.ScrollWidth) {
							movement.z += ResourceManager.ScrollSpeed;
						}

						//make sure movement is in the direction the camera is pointing
						//but ignore the vertical tilt of the camera to get sensible scrolling
						movement = Camera.mainCamera.transform.TransformDirection(movement);
						movement.y = 0;

						//away from ground movement
						movement.y -= ResourceManager.ScrollSpeed * Input.GetAxis("Mouse ScrollWheel");

						//calculate desired camera position based on received input
						Vector3 origin = Camera.mainCamera.transform.position;
						Vector3 destination = origin;
						destination.x += movement.x;
						destination.y += movement.y;
						destination.z += movement.z;

						//limit away from ground movement to be between a minimum and maximum distance
						if(destination.y > ResourceManager.MaxCameraHeight) {
							destination.y = ResourceManager.MaxCameraHeight;
						} else if(destination.y < ResourceManager.MinCameraHeight) {
							destination.y = ResourceManager.MinCameraHeight;
						}

						//if a change in position is detected perform the necessary update
						if(destination != origin) {
							Camera.mainCamera.transform.position = Vector3.MoveTowards(origin, destination, Time.deltaTime * ResourceManager.ScrollSpeed);
						}
					}

					private void RotateCamera() {
						Vector3 origin = Camera.mainCamera.transform.eulerAngles;
						Vector3 destination = origin;

						//detect rotation amount if ALT is being held and the Right mouse button is down
						if((Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt)) && Input.GetMouseButton(1)) {
							destination.x -= Input.GetAxis("Mouse Y") * ResourceManager.RotateAmount;
							destination.y += Input.GetAxis("Mouse X") * ResourceManager.RotateAmount;
						}

						//if a change in position is detected perform the necessary update
						if(destination != origin) {
							Camera.mainCamera.transform.eulerAngles = Vector3.MoveTowards(origin, destination, Time.deltaTime * ResourceManager.RotateSpeed);
						}
					}
				}

ResourceManager.cs

				using UnityEngine;
				using System.Collections;

				namespace RTS {
					public static class ResourceManager {
						public static int ScrollWidth { get { return 15; } }
						public static float ScrollSpeed { get { return 25; } }
						public static float RotateAmount { get { return 10; } }
						public static float RotateSpeed { get { return 100; } }
						public static float MinCameraHeight { get { return 10; } }
						public static float MaxCameraHeight { get { return 40; } }
					}
				}
<
>