Creating an RTS in Unity: Part VII

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

Now that we have a basic Unit and Building in our world, along with the ability to select them and display that fact to the user, let’s improve our HUD a little further. One of the things that is useful in games (actually, on a computer in general) is having the mouse cursor change when what the player can do changes. For example, hovering over a building you can select would show a different cursor than hovering over the ground when a unit that can move is selected. This time we will add in the ability to change the cursor based on a sense of cursor state. The HUD itself will be responsible for actually drawing the correct cursor. But it will be up to a number of things in our game to determine what the actual state of the cursor needs to be.

Custom Cursor

The first thing we want to be able to do is to draw a custom cursor. As part of this we will introduce the concept of cursor state – so that drawing is based on that state and so that changing that state does not need to worry about the draw code. Before we begin drawing we should create a place to store all the cursors that we are going to use. Inside the HUD folder create a new folder called Cursors. We may as well put all of our cursors into folders now, so that when we want to use them we can just grab the ones we want. Inside the new Cursors folder create folders with the following names: Attack, Harvest, Move, Pan, Select. As you can see, we are going to have a number of different states that we will display to the user with our cursor. Many of these actions will not be added until later, but it is still useful to have the cursors here.

  1. Attack Cursors
    • Attack Cursor 1Attack Cursor 1
    • Attack Cursor 2Attack Cursor 2
  2. Harvset Cursors
    • Harvest Cursor 1Harvest Cursor 1
    • Harvest Cursor 2Harvset Cursor 2
  3. Move Cursors
    • Move Cursor 1Move Cursor 1
    • Move Cursor 2Move Cursor 2
  4. Pan Cursors
    • Pan Up CursorPan Up Cursor
    • Pan Down CursorPan Down Cursor
    • Pan Left CursorPan Left Cursor
    • Pan Right CursorPan Right Cursor
  5. Select Cursor
    • Select Cursor 1Select Cursor

The images above are the cursors that I am using for this project. Feel free to use them as well. Each of these needs to go into the appropriate folder. (Apologies for slightly weird formatting or display there, it is WordPress being annoying … If you click on each image they should open in a new tab, you can save them from there.)

Now that we have some images to work with, we need a way to be able to access these within our HUD. Add the following variables to the top of HUD.cs

public Texture2D activeCursor;
public Texture2D selectCursor, leftCursor, rightCursor, UpCursor, downCursor;
public Texture2D[] moveCursors, attackCursors, harvestCursors;

and then drag the appropriate images onto them in Unity. Just make sure that when you are adding images to the array variables that you add them in the right order (1, 2, 3, …) otherwise the animation sequence that we end up using will look really weird. Now it is time to get our custom cursor drawing. Add the following method to HUD.cs.

private void DrawMouseCursor() {
	if(!MouseInBounds()) {
		Screen.showCursor = true;
	} else {<
		Screen.showCursor = false;
		GUI.skin = mouseCursorSkin;
		GUI.BeginGroup(new Rect(0,0,Screen.width,Screen.height));
		UpdateCursorAnimation();
		Rect cursorPosition = GetCursorDrawPosition();
		GUI.Label(cursorPosition, activeCursor);
		GUI.EndGroup();
	}
}

If the mouse is over the HUD area of the screen we will use the system mouse cursor (although we could easily change this at a later date). If not we need to cancel the system cursor and draw our own in its place. To make sure that things always behave we will define a skin for our cursor (a new default skin will do fine) and define the group drawing area to be the entire screen. We then update any animation our cursor might have to make sure we have the correct frame set in activeCursor, find where it needs to be drawn on screen, and then draw it there using a Label.

As usual, there are some steps we need to take to make this method work. First, create a new skin (leave all the settings as default) and call it MouseCursorSkin. Now create a reference to another GUISkin at the top of HUD called mouseCursorSkin and attach the new skin to your HUD in Unity. Finally, we need to create the two methods which do most of the work for us. Let’s start with UpdateCursorAnimation.

private void UpdateCursorAnimation() {
	//sequence animation for cursor (based on more than one image for the cursor)
	//change once per second, loops through array of images
	if(activeCursorState == CursorState.Move) {
		currentFrame = (int)Time.time % moveCursors.Length;
		activeCursor = moveCursors[currentFrame];
	} else if(activeCursorState == CursorState.Attack) {
		currentFrame = (int)Time.time % attackCursors.Length;
		activeCursor = attackCursors[currentFrame];
	} else if(activeCursorState == CursorState.Harvest) {
		currentFrame = (int)Time.time % harvestCursors.Length;
		activeCursor = harvestCursors[currentFrame];
	}
}

This method is relying on two global variables, so we need to declare them at the top of HUD.

private CursorState activeCursorState;
private int currentFrame = 0;

CursorState is actually an Enum. These are useful for declaring things like a collection of states (as opposed to using an array of strings that we then have to access in annoying ways). As you can see above, checking to see whether we are in a certain state is quick and easy. Inside the RTS folder create a new C# script called Enums and replace the entire file with the code below.

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

If we ever want to handle more cursor states in our game all we need to do is add extra entries to this enum and then we can perform the relevant checks elsewhere in our code. Remember, if we want to access this Enum from a class we need to add

using RTS;

above the class definition.

Now to evaluate that method and see what it is actually doing. Each branch of the if statement is handling a different cursor which has more than one image for it. If there is only one image we do not need to handle an animation. The logic for each cursor is the same, all that is changing is the array of images being referenced. The first line determines the current frame. This does so by making sneaky use of some math theory, which I will go into very briefly here. The % operator in C# performs a division on two integers (whole numbers) and then returns the remainder. For example: 7 / 2 gives us 3 lots of two with one left over (3 * 2 + 1 = 7). So 7 % 2 will return the one left over. This is very useful when we want to access a particular entry in an array based on some changing element – in this case time. Unity provides us the current elapsed game time (in seconds) through Time.time. By casting this to an int we round the current time down to the nearest second (so 116.23 seconds becomes 116 seconds). We then divide this value by the number of images we have for our cursor (given by the length of the array for that cursor’s images) and retrieve the remainder. This is guaranteed to be a valid index in our array. The index position will advance by one every second, and it will automatically wrap back to the start of the array (since a match with the length of the array gives a remainder of 0). This one simple line of code is extremely powerful, and we will use this underlying concept in other places as we progress. Once we have the index value we can set the active cursor to the appropriate image for the current cursor state. (For those interested in learning more look up Modular arithmetic)

There is still one more method for us to create: GetCursorDrawPosition.

private Rect GetCursorDrawPosition() {
	//set base position for custom cursor image
	float leftPos = Input.mousePosition.x;
	float topPos = Screen.height - Input.mousePosition.y; //screen draw coordinates are inverted
	//adjust position base on the type of cursor being shown
	if(activeCursorState == CursorState.PanRight) leftPos = Screen.width - activeCursor.width;
	else if(activeCursorState == CursorState.PanDown) topPos = Screen.height - activeCursor.height;
	else if(activeCursorState == CursorState.Move || activeCursorState == CursorState.Select || activeCursorState == CursorState.Harvest) {
		topPos -= activeCursor.height / 2;
		leftPos -= activeCursor.width / 2;
	}
	return new Rect(leftPos, topPos, activeCursor.width, activeCursor.height);
}

The basic position we want to start with for any cursor is the position on screen where the mouse cursor would be drawn. Remember that Unity has the draw coordinate starting in the bottom left corner, so we need to take the screen coordinates for the mouse (which start from the top left corner) and invert these to make sure that the cursor will be drawn in the correct position.

We now want to tweak the position of the mouse based on its current state. There are a number of reasons why we might want to do this. The most common reason is that a large number of the cursors we will use actually have the point where we think the cursor is as the centre of the image, not the top left corner (which is where we will be starting the drawing of our image from). In all of these cases we need to shift the draw position up and to the left by half the width and height of our image. The other special cases here (at the moment) are for when we are panning right or down. Remember that this happens when the mouse is positioned on the far right or bottom of the screen. If we were to draw the cursor from the top left of our image in either of those situations, we would be drawing the cursor off-screen, which is no use at all. So we need to make sure that the draw position is shifted back on screen appropriately in theses scenarios.

It turns out that there is one more thing we need to do before we can even show a custom cursor on screen at the moment. We need to set what the default cursor will be for our HUD. But before we do that, let’s create a method that will allow us to easily change our cursor state.

public void SetCursorState(CursorState newState) {
	activeCursorState = newState;
	switch(newState) {
	case CursorState.Select:
		activeCursor = selectCursor;
		break;
	case CursorState.Attack:
		currentFrame = (int)Time.time % attackCursors.Length;
		activeCursor = attackCursors[currentFrame];
		break;
	case CursorState.Harvest:
		currentFrame = (int)Time.time % harvestCursors.Length;
		activeCursor = harvestCursors[currentFrame];
		break;
	case CursorState.Move:
		currentFrame = (int)Time.time % moveCursors.Length;
		activeCursor = moveCursors[currentFrame];
		break;
	case CursorState.PanLeft:
		activeCursor = leftCursor;
	break;
	case CursorState.PanRight:
		activeCursor = rightCursor;
		break;
	case CursorState.PanUp:
		activeCursor = upCursor;
		break;
	case CursorState.PanDown:
		activeCursor = downCursor;
		break;
	default: break;
	}
}

We set the active cursor state to the new state specified and then we update the cursor accordingly. You will notice that if a cursor has multiple images we are using the same method as before for selecting which frame of the animation to show. We do it this way so that next update the animation will flow seamlessly, rather than jumping from the first frame to whatever UpdateCursorAnimation() decides the frame should be.

Okay, all of our framing is in place, now we just need to call it from the right places. In HUD, add the following line to the end of Start()

SetCursorState(CursorState.Select);

and this line to the end of OnGUI() (inside the check for whether the player is human).

DrawMouseCursor();

If you run this from inside Unity you should see that our Select cursor is now being drawn while the mouse is inside the playing area. (Sometimes in this demo mode Unity does not hide the system mouse cursor when it should, selecting an object should fix this. Either way, you should still be able to see the Select cursor being drawn)

Changing Cursor State

It turns out that most of the framework for changing our cursor state is in place already. All that is left is to implement the logic of when this should change throughout our codebase. For now, let’s change the cursor when we are panning the map around. This is a good simple way to inform the user what is happening. To make this happen we need to add some code to MoveCamera() inside our UserInput script. Update the code which handles vertical and horizontal movement to the following

bool mouseScroll = false;

//horizontal camera movement
if(xpos >= 0 && xpos < ResourceManager.ScrollWidth) {
	movement.x -= ResourceManager.ScrollSpeed;
	player.hud.SetCursorState(CursorState.PanLeft);
	mouseScroll = true;
} else if(xpos <= Screen.width && xpos > Screen.width - ResourceManager.ScrollWidth) {
	movement.x += ResourceManager.ScrollSpeed;
	player.hud.SetCursorState(CursorState.PanRight);
	mouseScroll = true;
}

//vertical camera movement
if(ypos >= 0 && ypos < ResourceManager.ScrollWidth) {
	movement.z -= ResourceManager.ScrollSpeed;
	player.hud.SetCursorState(CursorState.PanDown);
	mouseScroll = true;
} else if(ypos <= Screen.height && ypos > Screen.height - ResourceManager.ScrollWidth) {
	movement.z += ResourceManager.ScrollSpeed;
	player.hud.SetCursorState(CursorState.PanUp);
	mouseScroll = true;
}

and then add the following check at the bottom of the method

if(!mouseScroll) {
	player.hud.SetCursorState(CursorState.Select);
}

to make sure that we do not get stuck with a Pan cursor. If you run this now you will see that the cursor is changing, but we cannot see it when we are panning up or right since our mouse is over the HUD. Let’s fix that, since it will leave players highly confused as to what is going on. At the top of DrawMouseCursor() we need a better check to see if the mouse is in the HUD or not. This should do, so add it to the start of the method

bool mouseOverHud = !MouseInBounds() && activeCursorState != CursorState.PanRight && activeCursorState != CursorState.PanUp;

and then replace the

if(!MouseInBounds())

check with

if(mouseOverHud)

to determine whether to use the system cursor or not.

I think that about does it for today. We now have custom cursors at our disposal and an easy way to change those at will. As our units and buildings gain more abilities (or we add more specialized versions of them into our game), we will be able to easily change the cursor whenever we want to. The full code for the end of this stage is up on github under the commit for Part 7.

Advertisements

12 thoughts on “Creating an RTS in Unity: Part VII

  1. Eldoud says:

    How can I do to display bigger cursors with the same images ?
    If I define two new variables (in GetCursorDrawPosition()) :
    float width = activeCursor.width*5;
    float height = activeCursor.height*5;
    and then set the return like this :
    return new Rect(leftPos, topPos, width, height);
    I only have a cursor which is about twice the size of the previous one (instead of 5 tiimes biiger).

    • I’m not sure sorry. I do know that by default Unity likes to work with textures with dimensions that are a power of 2, and I think that maybe it forces them to be like this. You would need to look up on that a bit more I think. I would say that it is something to do with how Unity is drawing / resizing a texture though.

  2. anon says:

    Bugs/Problems:

    In DrawMouseCursor: } else {<

    Should remove the <

    In the case/switch:

    activeCursor = moveCursors.Length
    should be: activeCursor = moveCursors[currentFrame]

    In the inputhandler scrolling-left:
    if(xPos <= 0 && xPos = 0 && xPos < ResourceManager.scrollWidth)

    Minor adjustments I did:

    Changed to
    topPos = Screen.height – (activeCursor.height/2);

    This makes it so that the cursor pointer is exactly at the edge, and not slightly floating besides it.

    changed:
    if(activeCursorState == CursorState.PanRight) leftPos = Screen.width – activeCursor.width;
    to:
    if(activeCursorState == CursorState.PanRight) leftPos = Input.mousePosition.x – activeCursor.width;

    Makes it so it doesn't get glued to the edge of the screen (kinda jarring). Same thing with panDown. If you're doing like me and removing the test in input to test if the cursor is within the window (to allow easy scrolling in windowed mode), then you'll have to put that test in here as well to glue the drawing of the cursor if your mouse is outside.

  3. Jack says:

    Hello, there are some issues that I’m getting when I compile my code, there are all like this: “The name `mouseOverHud’ does not exist in the current context”. and a few more of this kind but with ‘mouseCursorState’ and ‘activeCursorState’. I can’t understand whats causing this problem, and I have already added the public GUISkin mouseCursorSkin; in my class. Please help me, I will be very very glad.

    • Errors like that mean that you are trying to access a variable which has not been declared yet. It is possible that you have not read far enough through the tutorial when you go to compile your code. In general things should work fine when I mention the ability to run the game and see something happen. Until that point, though, there are no guarantees that things will even compile.

      • Jack says:

        Wow! Thanks for such a fast reply, I thought I would not get even an answer, but unfortunately, I still can’t make my code functional. I redid everything, I read the whole tutorial many times, I copied and pasted the pieces of code you gave, I did everything you said line by line, but I wasn’t lucky. Anyway, these are some breathtaking tutorials and I want to thank you very much for the effort you put in every single one (in fact, this is the first issue i get). If you could review your tutorial and check everything it would be awesome. I’ll post my code later so you could see it in detail if you are not busy. Thanks!

        • I do try to make an effort to reply to comments. I know that it can be helpful to ask questions. I have tidied up parts of the tutorial when I put it onto a dedicated website. See http://www.stormtek.geek.nz/rts_tutorial for the whole tutorial in one place.

          I do mention in this part that you need to declare private CursorState activeCursorState; at the top of HUD.cs. There is no mention anywhere in this post to mouseCursorState, so maybe that is meant to be activeCursorState? And I do mention that you need to add bool mouseOverHud = …; to the top of DrawMouseCursor()

          Hope that helps. I would seriously recommend working through the rest of the tutorial on my website, rather than through the blog here, since it is a whole lot nicer to use.

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