Wednesday 29 September 2010

Tutorial 6 : Tower Management

This tutorial will mainly focus on how the player will add in new towers through the addition of a new class called “Player.cs”.

The Player class will hold all the information about a specific player, such as how much money he has, what stage he is on etc. The reason we are putting all this information in it’s own class and not just in “Game1.cs” is that if we do decide to make this game multiplayer, it will make it much easier to store information about each person playing.

So let’s get started, the first thing we will do as I'm sure you have guessed is add a new class called “Player.cs”. To start with we are going to add a few fields and properties :

private int money = 50;
private int lives = 30;
 
private List<Tower> towers = new List<Tower>();
 
private MouseState mouseState; // Mouse state for the current frame
private MouseState oldState; // Mouse state for the previous frame

public int Money
{
    get { return money; }
}
public int Lives
{
    get { return lives; }
}

The first three fields are pretty self explanatory. The next two describe what is happening with the mouse, we will use them to determine if the player has clicked, and if so where etc.

Next we are going to add in one more field and a constructor for the class :


private Level level;

public Player(Level level)
{
    this.level = level;
}

We are going to pass in a reference to the level, this will come in handy when the player wants to add a new tower, as we can use the level class to make sure the tower fits on the level and that we don’t place any towers over paths.

Now we are going to add in an Update method for our Player, this is where we will handle the creation of new towers, and also the updating of existing towers.


private int cellX;
private int cellY;
 
private int tileX;
private int tileY;

public void Update(GameTime gameTime, List<Enemy> enemies)
{
    mouseState = Mouse.GetState();

    cellX = (int)(mouseState.X / 32); // Convert the position of the mouse
    cellY = (int)(mouseState.Y / 32); // from array space to level space
 
    tileX = cellX * 32; // Convert from array space to level space
    tileY = cellY * 32; // Convert from array space to level space

    oldState = mouseState; // Set the oldState so it becomes the state of the previous frame.
}

Right, I know this method looks completely pointless but I assure you it’s not!! The first and last lines are quite straight forward, we just update the mouseState so it is correct for the current frame, and update the oldState so it is correct for the previous frame. Now I’m sure your asking your self why are we dividing the mouse position by 32 only to multiply it by 32 again.

The reason if quite simple really, if you think about what happens when you divide a floating point number by another floating point number, and then cast it to and integer, you will get the integer part number of the number. So lets take a look at an example :

image

In the above example if the mouse was at position (77, 114) and we use the above equation to calculate where that is in array space we get the following :

CellX = (int) (77 / 32)

= (int) (2.40625)

= 2

Which is correct, as we can see in the image the point is in the third square along. Now we know what cell the pointer is in we can work out where that cell is in level space by multiplying it by 32 (The the widow of our tiles)

TileX = 2 * 32 = 64


Which is the level space position of the top right corner of the 3rd tile along, so hopefully now you can see why this works.

Before we handle creating new towers, first we will add in short helper method to make adding a new tower easier, and before we can do that, we need to add a new method to Level.cs. Go to Level.cs and add the following :


public int GetIndex(int cellX, int cellY)
{
    if (cellX < 0 || cellX > Width || cellY < 0 || cellY > Height)
        return 0;
 
    return map[cellY, cellX];
}

All this code does is return the index of the requested cell. We can use this index to check if we are on a path or not. Right, now we are ready to add the following method to “Player.cs” :


private bool IsCellClear()
{
    bool inBounds = cellX >= 0 && cellY >= 0 && // Make sure tower is within limits
        cellX < level.Width && cellY < level.Height; 
 
    bool spaceClear = true;
 
    foreach (Tower tower in towers) // Check that there is no tower here
    {
        spaceClear = (tower.Position != new Vector2(tileX, tileY));
 
        if (!spaceClear)
            break;
    }
 
    bool onPath = (level.GetIndex(cellX, cellY) != 1);
 
    return inBounds && spaceClear && onPath; // If both checks are true return true
}


The first thing we do here is check whether the cell the mouse is in is actually part of our level i.e. not floating off the screen somewhere. Next we loop through all of the towers the player currently owns, and make sure that there isn’t a tower already in that tile. Then we check to see if the cell has an index of 1 (the index of a path).

Now, as you may have noticed in this method we are trying to access tower.Position, but we never actually created that property in the sprite class… my bad. So lets go to “Sprite.cs” and add the following property :


public Vector2 Position
{
    get { return position; }
}

With that added, we can now go back to “Player.cs” and we just need one more field before we can add us some towers. Add the following field :


private Texture2D towerTexture;

Now we need to modify the constructor slightly to allow for a tower texture to be passed in :


public Player(Level level, Texture2D towerTexture)
{
    this.level = level;
    this.towerTexture = towerTexture;
}

And that’s all the fields we will add for now. Next, in the Update method add the following just before where we set oldState :


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


This little snippet of code is what actually adds a new tower to the game. The first two lines check whether the player had the left mouse button pressed down the last frame and if it is now released this frame i.e did the player just click. Next we check whether or not the cell that the player just clicked in is clear, and if it is, we add in a new tower at the location of the cell.

We are almost finished with the Player class, but there is one big thing that we are missing, have you spotted it yet? At no point are our towers updated, we will handle this now, just under the code we just added, add the following :


foreach (Tower tower in towers)
{
    if (tower.Target == null)
    {
        tower.GetClosestEnemy(enemies);
    }
 
    tower.Update(gameTime);
}


All this little snippet does is loop through all of the players towers, and updates them. It also checks whether the tower has a target or not, and if it doesn’t, it finds one for it.

So, we now have a new class that handles when new towers are created, and updates all of those towers, but what's the point of that when at the moment we can’t draw them!! Add the following method to the end of the Player class :


public void Draw(SpriteBatch spriteBatch)
{
    foreach (Tower tower in towers)
    {
        tower.Draw(spriteBatch);
    }
}


And now we’re done, all we have left to do is set up a new player class in Game1 and we are good to go. So, let’s go to “Game1.cs” and replace the line where create define a new tower with the following :


//Tower tower;
Player player;


Next, find where we initialized the tower and replace it with the following :


Texture2D towerTexture = Content.Load<Texture2D>("arrow tower");
//tower = new Tower(towerTexture, Vector2.Zero);
player = new Player(level, towerTexture);

Next replace the update method with this one :


protected override void Update(GameTime gameTime)
{
    enemy1.Update(gameTime);

    List<Enemy> enemies = new List<Enemy>();
    enemies.Add(enemy1);

    player.Update(gameTime, enemies);
 
    base.Update(gameTime);
}


Finally, make sure your draw method looks like this :


protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin(); 

    level.Draw(spriteBatch);
    enemy1.Draw(spriteBatch);
    player.Draw(spriteBatch);

    spriteBatch.End();

    base.Draw(gameTime);
}

And there we have it, a fully functioning player class that enables us to create and update new towers. If you run the project now, and click on empty cells you will see that new towers get created there, and they will all track our enemy.

36 comments:

  1. Nice tut. Keep up the good work.

    ReplyDelete
  2. Does the player class need to include:

    using Microsoft.Xna.Framework.Input;

    Also, when I click on the window border of my program, it gives me an error that says:

    An unhandled exception of type 'System.IndexOutOfRangeException' occurred in Tower Defense.exe

    Additional information: Index was outside the bounds of the array.

    Is there a way to avoid this?

    ReplyDelete
  3. hm when i click on empty cell nothing happens ;(
    don't know where is the problem

    ReplyDelete
  4. Hey Sean, I'm not sure, if you delete it does it give you an error? ;)

    I think this is a mistake on my part, if you go to the GetIndex method and change the if statement to look like this : if (cellX < 0 || cellX > Width - 1 || cellY < 0 || cellY > Height - 1)
    return 0;
    That should fix your problem :)

    ReplyDelete
    Replies
    1. I got that error too. I was just cheap though and did a try/catch solution...

      Delete
  5. Hi Anonymous, have you tried comparing you source code against the code I provided?

    There must have been an error when you where writing some of your code :) If that doesn't help just leave another comment and I will try and offer some more advice!

    ReplyDelete
  6. If I don't include Microsoft.Xna.Framework.Input; in the Player class the MouseState code does not work. It doesn't recognize the MouseState object type or Mouse.GetState() methods.

    ReplyDelete
  7. ok i found where was an error, now working

    ReplyDelete
  8. @Sean then you do need to include it :P

    ReplyDelete
  9. Hi, when i do this it says that all the Enemy, Tower, Level, etc. all the ones with capital letters arent there in the player.cs?

    ReplyDelete
  10. Ok hi, I was the one who posted that thing about the capital letter things, and it got fixed. Now, i can build the towers, but it doesn't attack the enemy? help please?

    ReplyDelete
  11. Hi, when you say that the towers don't attack, do the towers just not move or do they not shoot bullets?

    At this stage in the series all the towers should do is rotate to track enemies. In the next tutorial we move on to adding bullets!

    Let me know if that helps!

    ReplyDelete
  12. ok thanks, that helps part of it. but they also aren't rotating. Im gonna log in with my google account after this, and hopefully we can understand each other better.
    -Chicken21200

    ReplyDelete
  13. Well at least that's one problem solved! At the end of the last tutorial did your towers rotate?

    If not I would go back over that tutorial to make sure you didn't miss anything! If that isn't the case there may be something missing from your update method in the TowerManager class.

    Let me know if this helps!

    ReplyDelete
  14. Is it correct that i don't see a mouse cursor in the game window yet?

    ReplyDelete
  15. You should be able to see it by now, but it is easy to make it appear, just add the following line to the Game1 constructor :

    IsMouseVisible = true;

    ReplyDelete
  16. Thanks, i needed that in there. Tower shoots now and i understand most of whats going on.

    ReplyDelete
  17. what if you're going to use these codes for win7 mobile? which i need to use the touch screen rather than a mouse... do i need to change some part of the codes?

    ReplyDelete
  18. Hey,

    You won't need to change any of the mouse code that I know of, it should just work!

    This link should confirm that :

    http://msdn.microsoft.com/en-us/library/bb197572.aspx

    ReplyDelete
  19. Hey,

    Your tutorials are great but i seem to be having an error.
    In the foreach loop in isCellClear method.

    It tells me it doesn't know "Position".

    'WindowsGame4.Tower' does not contain a definition for 'Position' and no extension method 'Position' accepting a first argument of type 'WindowsGame4.Tower' could be found (are you missing a using directive or assembly reference?)

    ReplyDelete
  20. Hey, would you be able to post your copy of just the loop that is throwing the error?

    ReplyDelete
  21. FireFly, getting same prob here as Anon. mar 14, 2012 03:15am

    copy of my loop:
    ---------------
    private bool IsCellClear()
    {
    bool inBounds = cellX >= 0 && cellY >= 0 && cellX < level.Width && cellY < level.Height;

    bool spaceClear = true;

    foreach (Tower tower in towers) // check that there is no tower here
    {
    spaceClear = (tower.position != new Vector2(tileX, tileY));

    if (!spaceClear)
    break;
    }
    bool onPath = (level.GetIndex(cellX, cellY) != 1);

    return inBounds && spaceClear && onPath; //if both checks are true return true
    }
    ----------------

    error says that the position is not accessible due to it being protected.
    I'm looking up my code. I'll let you know where I stumbled if you dont' come up with any suggestions.

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  22. Never mind last comment. I kept reading your tut and quickly noticed you addressed it.

    Silly me... heheh

    ReplyDelete
  23. Does anyone have an idea how I would code this to work for xbox 360, trying to show my son how to program games and I've converted everything in your tutorial to xbox 360, but this part and since this is my first game, still not all that familiar with gamepad positioning as I would think you would move a sprit around instead of a "cursor". Any help is appreciated.

    ReplyDelete
  24. Hi,

    I have created a version of the project for you which *should* work on the xbox that will hopefully give you an idea of what needs to be done to get things working. However if you have any more questions or problems let me know!

    http://www.mediafire.com/file/tp9473ace2qrn2e/TowerDefenseTutorial_6_Xbox.zip

    ReplyDelete
  25. I'm currently trying it out, I will give you an update as soon I can, thank you for your help, at first glance looks like I wasn't too far off, having to adapt some code as I'm using it inside a game state manager so some code is actually called from the gameplayscreen.cs vice game1.cs. Thank you again for your help!

    ReplyDelete
  26. I'm currently trying it out, I will give you an update as soon I can, thank you for your help, at first glance looks like I wasn't too far off, having to adapt some code as I'm using it inside a game state manager so some code is actually called from the gameplayscreen.cs vice game1.cs. Thank you again for your help!

    ReplyDelete
  27. Is it possible to have towers set at the beginning of the game?
    I'm working with my group on a project for school and we're currently using your tutorials, which are a great help! But we want to make different type(color) monsters which are only vulnerable to the same color tower.

    ReplyDelete
  28. Hi FireFly your tutorials are great. One thing I cant figure out is when im clicking on an empty free space my tower is being placed at pixels 0,0.

    I have already got rid of this line.(tower = new Tower(towerTexture, Vector2.Zero);) just so you know its not that.

    Does anybody have any ideas.

    Thanks, Liam Harley

    ReplyDelete
  29. Hmm that is strange, have you tried setting a breakpoint inside Player.Update() to make sure it's being called?

    If it is make sure cellX and cellY are being set to the values that you would expect.

    If thats the case then try pointing a break point on the line where you create the tower (Tower tower = new ...) and make sure cellX and cellY still have the right values.

    Good luck!

    ReplyDelete
  30. hmm is there supposed to be a cursor present because when ever i run the game there isn't one and when i click i get this error An unhandled exception of type 'System.StackOverflowException' occurred in WindowsGame1.exe
    on the line
    private bool IsCellClear()
    { <--- this line
    bool inBounds = cellX >= 0 && cellY >= 0 && // Make sure tower is within limits
    cellX < level.Width && cellY < level.Height;

    ReplyDelete
    Replies
    1. and is there a way to make it full screen?

      Delete