Thursday, 23 December 2010

Tutorial 11 : Creating the GUI (Part 2)

In this tutorial we will be adding a generic button class to our project, and then we will be using this class to add buttons to our toolbar. We will keep this button class a general as possible so it can be re-used throughout the project.

The first thing we are going to do is add a new class in the GUI folder called “Button.cs” and make it inherit the Sprite class. Before we add anything to this class though, we are going to create a new enum just above it :

/// <summary>
/// Describes the state of the button.
/// </summary>
public enum ButtonStatus
{
Normal,
MouseOver,
Pressed,
}

/// <summary>
/// Stores the appearance and functionality of a button.
/// </summary>
public class Button : Sprite
{

}

Note : The reason the enum is called ButtonStatus is to avoid confusion with the XNA enum called ButtonState.

This enum gives us a way to define what the button should like in certain conditions. There are 3 different states the button can be in :

arrow buttonarrow pressedarrow hover

Normal : This is the default state of the button – the button is in this state when the mouse is not hovering over the button and the button has not been pressed.

MouseOver : The button is in the state when the player has the mouse hovering over the button and is not holding down the left mouse button.

Pressed : The button is in the state when the player has the left button held down while hovering over the button.

Right, let’s makes a start on the actual button class! The first thing we are going to add is some fields :

// Store the MouseState of the last frame.
private MouseState previousState;

// The the different state textures.
private Texture2D hoverTexture;
private Texture2D pressedTexture;

// A rectangle that covers the button.
private Rectangle bounds;

// Store the current state of the button.
private ButtonStatus state = ButtonStatus.Normal;

These fields should explain themselves, so I'm just going to move onto the constructor :

/// <summary>
/// Constructs a new button.
/// </summary>
/// <param name="texture">The normal texture for the button.</param>
/// <param name="hoverTexture">The texture drawn when the mouse is over the button.</param>
/// <param name="pressedTexture">The texture drawn when the button has been pressed.</param>
/// <param name="position">The position where the button will be drawn.</param>
public Button(Texture2D texture, Texture2D hoverTexture, Texture2D pressedTexture, Vector2 position)
: base(texture, position)
{
this.hoverTexture = hoverTexture;
this.pressedTexture = pressedTexture;

this.bounds = new Rectangle((int)position.X, (int)position.Y,
texture.Width, texture.Height);
}

This constructor takes in three different textures, each one controls the appearance of the different button states. We also initialize the bounds rectangle to fit around the button based on the buttons position and the sizes of the texture.

Next we are going to add in a method to update the state of the button :

/// <summary>
/// Updates the buttons state.
/// </summary>
/// <param name="gameTime">The current game time.</param>
public override void Update(GameTime gameTime)
{

}

The first thing we do in this method is get the current position of the mouse and check whether it is over the button :

// Determine if the mouse if over the button.
MouseState mouseState = Mouse.GetState();

int mouseX = mouseState.X;
int mouseY = mouseState.Y;

bool isMouseOver = bounds.Contains(mouseX, mouseY);

We then use this information to either change the button state to MouseOver or to change it back to Normal :

// Update the button state.
if (isMouseOver && state != ButtonStatus.Pressed)
{
state = ButtonStatus.MouseOver;
}
else if (isMouseOver == false && state != ButtonStatus.Pressed)
{
state = ButtonStatus.Normal;
}

Lastly we check whether the player pressed the mouse while it was over the button, and then if he let it up again while the mouse was still over the button :

// Check if the player holds down the button.
if (mouseState.LeftButton == ButtonState.Pressed &&
previousState.LeftButton == ButtonState.Released)
{
if (isMouseOver == true)
{
// Update the button state.
state = ButtonStatus.Pressed;
}
}

// Check if the player releases the button.
if (mouseState.LeftButton == ButtonState.Released &&
previousState.LeftButton == ButtonState.Pressed)
{
if (isMouseOver == true)
{
// update the button state.
state = ButtonStatus.MouseOver;
}

else if (state == ButtonStatus.Pressed)
{
state = ButtonStatus.Normal;
}
}

previousState = mouseState;

The next method we are going to add is one that is responsible for drawing the button :

/// <summary>
/// Draws the button.
/// </summary>
/// <param name="spriteBatch">A SpriteBatch that has been started</param>
public override void Draw(SpriteBatch spriteBatch)
{
switch (state)
{
case ButtonStatus.Normal:
spriteBatch.Draw(texture, bounds, Color.White);
break;
case ButtonStatus.MouseOver:
spriteBatch.Draw(hoverTexture, bounds, Color.White);
break;
case ButtonStatus.Pressed:
spriteBatch.Draw(pressedTexture, bounds, Color.White);
break;
default:
spriteBatch.Draw(texture, bounds, Color.White);
break;
}
}

We draw a different texture depending on what state the button is in.

And that’s the button class finished for now, we will come back to it later to add in events.

Before we add a button to our toolbar we will need to add some new images to our project. Download the images I uploaded at the start of the tutorial and then add them into a new folder called GUI in the Content Project. I know these aren't the greatest images in the world but they’re the best my art skills can create!

Now we are ready to add a button to our toolbar, go back to “Game1.cs” and add a field for a button :

Button arrowButton;

Next we need to load in our button textures and pass them to our button, add the following to LoadContent :

// The "Normal" texture for the arrow button.
Texture2D arrowNormal = Content.Load<Texture2D>("GUI\\Arrow Tower\\arrow button");
// The "MouseOver" texture for the arrow button.
Texture2D arrowHover = Content.Load<Texture2D>("GUI\\Arrow Tower\\arrow hover");
// The "Pressed" texture for the arrow button.
Texture2D arrowPressed = Content.Load<Texture2D>("GUI\\Arrow Tower\\arrow pressed");

// Initialize the arrow button.
arrowButton = new Button(arrowNormal, arrowHover,
arrowPressed, new Vector2(0, level.Height * 32));

All that’s left is to update and draw the button; in the Update method update the button :

//Update the arrow button.
arrowButton.Update(gameTime);

And in the Draw method, just after you draw the toolbar, draw the button :

// Draw the tool bar first,
toolBar.Draw(spriteBatch, player);
// and then our buttons.
arrowButton.Draw(spriteBatch);

Hit the F5 key and you will see our new button being drawn in the bottom left corner of our game window, and you should find it is fully interactive with your mouse!

So now that we have the button drawn and animated there’s just one thing missing… the ability to detect when the player has clicked the button, let’s go back to “Button.cs” and solve this now.

Add a new event to the class just under where we define the fields :

// Gets fired when the button is pressed.
public event EventHandler Clicked;

Note : For more information on events see http://msdn.microsoft.com/en-us/library/aa645739(v=vs.71).aspx

All that’s left to do is fire this event when the user clicks so find the Update method and find the code that deals with when the player releases the button. This is where we fire our event :

// Check if the player releases the button.
if (mouseState.LeftButton == ButtonState.Released &&
previousState.LeftButton == ButtonState.Pressed)
{
if (isMouseOver == true)
{
// update the button state.
state = ButtonStatus.MouseOver;

if (Clicked != null)
{
// Fire the clicked event.
Clicked(this, EventArgs.Empty);
}
}

else if (state == ButtonStatus.Pressed)
{
state = ButtonStatus.Normal;
}
}

We first check if the event is null, or in other words if anything attached itself to the event, and, if it has, we fire the event.

And that’s our button class finished for good! All that we have left to do is to attach our button to a method that when called, will allow the player to create a new arrow tower.

To do this we need to go back to “Game1.cs” and find where we initialized the button in the LoadContent method. Just under that we will hook up our event to a new method :

arrowButton.Clicked += new EventHandler(arrowButton_Clicked);

Now we need to add a new method called arrowButton_Clicked that will be called by the event :

private void arrowButton_Clicked(object sender, EventArgs e)
{
player.NewTowerType = "Arrow Tower";
}

When this method is called it tells the Player class that the player wants to build a new Arrow Tower, the only problem with this code is, the Player class doesn’t have a property called NewTowerType, let’s go to “Player.cs” now and add one :

// The type of tower to add.
private string newTowerType;

public string NewTowerType
{
set { newTowerType = value; }
}

Now that we know what tower we want to build, we need a way to act on this information and create a new tower for the player, lets add a new method to handle this :

/// <summary>
/// Adds a tower to the player's collection.
/// </summary>
public void AddTower()
{
Tower towerToAdd = null;

switch (newTowerType)
{
case "Arrow Tower":
{
towerToAdd = new ArrowTower(towerTexture,
bulletTexture, new Vector2(tileX, tileY));
break;
}
}

// Only add the tower if there is a space and if the player can afford it.
if (IsCellClear() == true && towerToAdd.Cost <= money)
{
towers.Add(towerToAdd);
money -= towerToAdd.Cost;

// Reset the newTowerType field.
newTowerType = string.Empty;
}
}

What this method does is it checks what type of tower the player wants, and initializes one based on that information. It then checks if there is a space for that tower and also if the player can afford it, and if they can, we add the new tower to the game, charge the player for it, and reset the newTowerType field.

All that’s left to now is to go to the Update method and find the code that used to create tower’s for the player :

if (mouseState.LeftButton == ButtonState.Released
    && oldState.LeftButton == ButtonState.Pressed)
{
if (IsCellClear())
{
Tower tower = new Tower(towerTexture, new Vector2(tileX, tileY));
towers.Add(tower);
}
}

And replace it with our new method of creating towers :

if (mouseState.LeftButton == ButtonState.Released
    && oldState.LeftButton == ButtonState.Pressed)
{
if (string.IsNullOrEmpty(newTowerType) == false)
{
AddTower();
}
}

We’re finished! Hit the F5 key and try clicking on a clear space, you should find that no tower is created, however if you click the tower button first and then click a space you should find that a new tower is created and you should also see that the player’s money goes down.

Here is the source code for this tutorial.

Note : As I see it, there is only one more tutorial left in this series which will contain a small bug fix and a small addition to the GUI. However if you have any suggestions for any more tutorials in the series feel free to tell me then and I will see what I can do.

13 comments:

  1. Hi, thanks for this excellent explanation.
    I was wondering if it maybe wouldn't be easier to make it so the button remains in the "pressed" state after clicking, then looping through all buttons to see which one is pressed and adding a tower of the appropriate type, before setting the button back to "normal". What do you reckon the drawbacks would be ?

    ReplyDelete
  2. Hey,

    That sounds like a perfectly good idea, how I would do this is add an event to the player class that fires when the player creates a new tower. It would probably need to be a custom event so you could pass the tower type to subscribing events.

    Then in Game1 you would subscribe to the event and when the event is fired use the tower type that was passed to the event to update the button state.

    Hope that helps :).

    ReplyDelete
  3. Hey,

    best first of all i had to say it is the best tutorial i have been read about tower defence.. =)

    great!

    But i had an question..
    How to add a few more towers or how can we handle the money with a few towers?

    And sorry for my bad english.. =)

    ReplyDelete
  4. Thanks! :) I will add a tutorial on how to do this! It should be up by the end of the week.

    ReplyDelete
  5. yeah great!

    Thank you very much =)

    I will follow ;)

    ReplyDelete
  6. where is the tutorial? =(

    ReplyDelete
  7. I just posted it, sorry for the long wait :(

    ReplyDelete
  8. Up till this tut everything worked pretty much as it should. I might have missed something in the previous tutorials though.

    I never added player lives or gold afaik.

    Now, i can still put down towers without clicking the button but they are not shooting any more. Clicking the button does not make the towers shoot either.

    Downloaded the source, but it's getting harder to find where i went wrong.

    ReplyDelete
  9. Fixed it, i added some code in the wrong class. It's getting a bit confusing because now and then you don't say in which class to add the code, although most of the time it's pretty obvious.

    However, i really don't emember assigning 50 gold and 30 lives to the corresponding fields.

    I just don't want to use your source to continue the tutorial for learning purpose.

    ReplyDelete
  10. Ooohh! And why is the enum of the different button states outside the class? Never seen that before.

    And also, is it possible to make the method arrowButton_Clicked in another class then Game1.cs? I could make the player class public or make it in the method itself public and put it inside the player class? I probably should know this but my brain starting to overload trying to understand how everything works together :D.

    ReplyDelete
  11. Hey Madmenyo,

    If you are unsure or it isn't clear which code goes where, feel free to leave a comment and I will try to alter the article to make it more clear :).

    As for the player fields, if you look back to Tutorial 6, the first two lines of code assign these values.

    Putting the enum outside of the class is just personal preference. It is just a habit that I have gotten into so feel free to change it so it works better for you; everything should still work fine!

    And finally, the arrowButton_Clicked can be put into any class as long as Game1.cs can still access it! For example, if you wanted to have that method in the player class, you would move the method to Player.cs and make it public. Then in Game1.cs in the LoadContent method simply change this :

    arrowButton.Clicked += new EventHandler(arrowButton_Clicked);

    to this :

    arrowButton.Clicked += new EventHandler(player.arrowButton_Clicked);

    Let me know if that helps!

    ReplyDelete
  12. Hey, I like your tuts very much! I am trying to convert this game for WP7, so that you can play it on touchscreen.
    My question is, do I have any advantage from handling the buttons inside the toolbar class?
    Means you add a list of buttons inside toolbar and construct them on toolbars constructor. Also updating and drawing would be inside the toolbar class.

    Arctic

    ReplyDelete
  13. Hey Arctic,

    I think it would make the code a lot cleaner especially if you are adding new buttons to it. I would add a method like AddButton() to the toolbar class as well to make it easy to add buttons. When a new button is added you could just set it's position to be the position of the last button in the list + the width of the last button + some offset!

    Let me know if you need any more help!

    ReplyDelete