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 :

privateint money = 50;

privateint 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

publicint Money

{

get { return money; }

}

publicint 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.

privateint cellX;

privateint cellY;

privateint tileX;

privateint tileY;

publicvoid 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 :

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 :

publicint 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” :

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 :

publicvoid 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 :

protectedoverridevoid 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 :

protectedoverridevoid 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.

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 :)

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?)

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.

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!

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!

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!

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.

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.exeon 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;