Tuesday 18 January 2011

Tutorial 12 : Adding a New Tower Type

Sorry for the delay, I have had a very busy at the month! In this tutorial I will show you how to add in two type of cannon, the Spike tower. This tower will be able to shoot 8 bullets in eight different directions at a time.

image

This should be pretty straight forward seeing as we have designed the Tower class to be as extensible as possible, although we will have to make several adjustments to some of the other classes.

So let’s get started, the first thing we will do is add four new textures to our project :

spike towerspike buttonspike hoverspike pressed

The “spike tower” texture needs to be added to the main Content Project, whereas the other 3 textures need to be added to a new folder called “Spike Tower” that needs to be added in the GUI folder. Confused yet? When you have done this step you should end up with something like this :

image

Hopefully your still with me after that stage! Right, we can move onto creating a new class for our tower. Add a new class into the Tower folder and call it “SpikeTower.cs”. Next, make our new class inherit from the Tower class :

public class SpikeTower : Tower

Now we are going to add some new fields and properties to the class :

// A list of directions that the tower can shoot in.
private Vector2[] directions = new Vector2[8];
// All the enimes that are in range of the tower.
private List<Enemy> targets = new List<Enemy>();

The first field will store of all the directions that the Spike Tower can shoot in (North, East, South… etc.). The next field will contain a list of all the enemies that are in range of the tower. You may be asking why not just have one target like the last tower? We need to store all these enemies because we aren’t just targeting one enemy, we are shooting bullets off in all directions and hoping one hits! We need to know all of the enemies that might be hit!

Next we need to give our tower a constructor :

/// <summary>
/// Constructs a new Spike Tower object.
/// </summary>
public SpikeTower(Texture2D texture, Texture2D bulletTexture, Vector2 position)
: base(texture, bulletTexture, position)
{
this.damage = 20; // Set the damage.
this.cost = 40; // Set the initial cost.

this.radius = 48; // Set the radius.

// Store a list of all the directions the tower can shoot.
directions = new Vector2[]
{
new Vector2(-1, -1), // North West
new Vector2( 0, -1), // North
new Vector2( 1, -1), // North East
new Vector2(-1, 0), // West
new Vector2( 1, 0), // East
new Vector2(-1, 1), // South West
new Vector2( 0, 1), // South
new Vector2( 1, 1), // South East
};
}

This is pretty much the same as the Arrow Tower constructor except here we need to initialize the directions in which the tower can shoot.

Before we can write an update method for our tower we need to make a quick change to “Bullet.cs”. So go to “Bullet.cs” and just under the existing constructor we will add a new one :

public Bullet(Texture2D texture, Vector2 position, Vector2 velocity, int speed, int damage)
: base(texture, position)
{
this.rotation = rotation;
this.damage = damage;

this.speed = speed;

this.velocity = velocity * speed;
}

Instead of taking in a rotation and then converting it into a velocity, we directly pass in a velocity to use (Well technically we pass in a direction and use that and the speed to calculate the velocity).

With that added we can go back to “SpikeTower.cs” and add our update method :

public override void Update(GameTime gameTime)
{

}

We will build this method up step by step. The first thing we will add to this method is some code to create our bullets and make sure the travel in the right direction :

bulletTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;

// Decide if it is time to shoot.
if (bulletTimer >= 1.0f && targets.Count != 0)
{
// For every direction the tower can shoot,
for (int i = 0; i < directions.Length; i++)
{
// create a new bullet that moves in that direction.
Bullet bullet = new Bullet(bulletTexture, Vector2.Subtract(center,
new Vector2(bulletTexture.Width / 2)), directions[i], 6, damage);

bulletList.Add(bullet);
}

bulletTimer = 0;
}

First we update the timer that we use to decide if it is time to shoot again. We then check if it is time to shoot, and if it is, we loop through all of the directions that our tower can shoot, and we create a bullet that travels in that direction.
The next thing we will add is some code to update our bullets :

// Loop through all the bullets.
for (int i = 0; i < bulletList.Count; i++)
{
Bullet bullet = bulletList[i];
bullet.Update(gameTime);

// Kill the bullet when it is out of range.
if (!IsInRange(bullet.Center))
{
bullet.Kill();
}

// Loop through all the possible targets
for (int t = 0; t < targets.Count; t++)
{
// If this bullet hits a target and is in range,
if (targets[t] != null && Vector2.Distance(bullet.Center, targets[t].Center) < 12)
{
// hurt the enemy.
targets[t].CurrentHealth -= bullet.Damage;
bullet.Kill();

// This bullet can't kill anyone else.
break;
}
}

// Remove the bullet if it is dead.
if (bullet.IsDead())
{
bulletList.Remove(bullet);
i--;
}
}

This code is basically the same as the update code in “ArrowTower.cs” however notice that we don’t just check if our bullets have hit just one target, we test it against all of the nearby enemies until either we hit one or we run out of enemies to test against.

Our Spike Tower is nearly finished however there is one more property that needs to be added, however first we need to go back to “Tower.cs” and add the following property :

public virtual bool HasTarget
{
// Check if the tower has a target.
get { return target != null; }
}

I will explain the significance of this property later. While we are still in “Tower.cs” there is one more change that needs to be made, find the GetClosestEnemy method and make it a virtual method, so change this :

public void GetClosestEnemy(List<Enemy> enemies)

to this :

public virtual void GetClosestEnemy(List<Enemy> enemies)

Now then, go back to “SpikeTower.cs” and add the following property :

public override bool HasTarget
{
// The tower will never have just one target.
get { return false; }
}

Again I will explain this in more detail later. Before we can move onto to adding a button for this tower, there is a few more changes to be made (yes I know I said we only had one more property to add…). We need to override the GetClosestEnemy method, this is because our now Spike Tower handles enemies differently than the normal method, so let’s override it and add in some new logic :

public override void GetClosestEnemy(List<Enemy> enemies)
{
// Do a fresh search for targets.
targets.Clear();

// Loop over all the enemies.
foreach (Enemy enemy in enemies)
{
// Check wether this enemy is in shooting distance.
if (IsInRange(enemy.Center))
{
// Make it a target.
targets.Add(enemy);
}
}
}

As you can probably see, instead of trying to find the closest enemy, we are just finding all of the enemies that are in range of the tower (see above for the explanation why).

Before we can add a new button for this tower, we need to make a small method to the Players Update method, so go ahead and find the Update method in “Player.cs” and more specifically this line :

if (tower.Target == null)

and replace it with the following :

// Make sure the tower has no targets.
if (tower.HasTarget == false)

This is where the property we added earlier comes into play. Normally this property will just return whether or not a tower has a single target, but in the case of the Spike Tower, it will always return false. This is because the Spike Tower never really has a single target, it can have many, and it always needs to check whether new enemies have come into range or old enemies are no longer in range.

While we are in the player class, go to the top of the class and find the towerTexture field, and change it to :

// The textures used to draw our tower.
private Texture2D[] towerTextures;

It is no longer possible to only store one texture to use for all of the Towers, we will needs a separate texture for each tower. We need to modify the constructor to accommodate this change :

/// <summary>
/// Construct a new player.
/// </summary>
public Player(Level level, Texture2D[] towerTextures, Texture2D bulletTexture)
{
this.level = level;

this.towerTextures = towerTextures;
this.bulletTexture = bulletTexture;
}

Now that we have access to different textures to use for different types of tower, we can go ahead and find the AddTower method and change it so it can handle the SpikeTower. You need to change the switch statement so it looks like this :

switch (newTowerType)
{
case "Arrow Tower":
{
towerToAdd = new ArrowTower(towerTextures[0],
bulletTexture, new Vector2(tileX, tileY));
break;
}
case "Spike Tower":
{
towerToAdd = new SpikeTower(towerTextures[1],
bulletTexture, new Vector2(tileX, tileY));
break;
}
}

As you can see the code is pretty much the same, the major difference is that the arrow tower uses the first texture stored in our texture array, and the spike tower uses the second texture stored in the array. We must remember this order when we pass our texture array to the player.
Now all that’s left to do is to add a new button for our tower, so go back to “Game1.cs” and at the top add the following field :

Button spikeButton;

Next, find the LoadContent method. We need to adjust the code that initializes the player so that we pass in a texture array instead of a single texture :

Texture2D[] towerTextures = new Texture2D[]
{
Content.Load<Texture2D>("arrow tower"),
Content.Load<Texture2D>("spike tower")
};

player = new Player(level, towerTextures, bulletTexture);

Now go down to where we load in the textures for the arrow tower and just under, load in the textures for the spike tower :

// The "Normal" texture for the spike button.
Texture2D spikeNormal = Content.Load<Texture2D>("GUI\\Spike Tower\\spike button");
// The "MouseOver" texture for the spike button.
Texture2D spikeHover = Content.Load<Texture2D>("GUI\\Spike Tower\\spike hover");
// The "Pressed" texture for the spike button.
Texture2D spikePressed = Content.Load<Texture2D>("GUI\\Spike Tower\\spike pressed");

And just under that initialize our new button and assign a click event to it :

// Initialize the spike button.
spikeButton = new Button(spikeNormal, spikeHover,
spikePressed, new Vector2(32, level.Height * 32));

spikeButton.Clicked += new EventHandler(spikeButton_Clicked);

Now, just under the LoadContent method, we need to add a new method that will be called when the spike button is clicked :

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

You can see an explanation of this in the previous tutorial. All that is left to do now is Update and draw our buttons!

Fnd the Update method and just after where we update the arrowButton, update the spikeButton :

//Update the spike button.
spikeButton.Update(gameTime);

And then go down to the Draw method and just after where we draw the arrowButton, draw the spikeButton :

spikeButton.Draw(spriteBatch);

And where finished! Who knew it would take so much to just add in a new tower Winking smile. I’m still undecided on whether to write another tutorial about creating a custom tower class, or just write a “finishing off” tutorial. Let me know what you think!

24 comments:

  1. What about the level editor ?
    Would be very nice !

    BTW, thanks for the excellent tutorial series, it really kicks ass man !

    ReplyDelete
  2. Instead of using 3 different images, wouldn't be easier when drawing (Draw method) use Color.Green, Color.Red, for example ?

    Thanks

    ReplyDelete
  3. lol now the other tower is not shooting?? oO

    thats not profitable xD

    plz fix that mistake =)

    ReplyDelete
  4. i think that lines are wrong..

    if (tower.Target == null)


    and replace it with the following :

    // Make sure the tower has no targets.
    if (tower.HasTarget == false)

    Because the arrow towers will never shoot, if this code is addet

    ReplyDelete
  5. Hey Dwan, yes it probably would :) However what if in your final game you decided that when the button is pressed it should do something other then change colour?

    This way you have more control over the buttons appearance, but if you don't need this extra control, simply do what you suggested above :)

    ReplyDelete
  6. Hey Leonarado, thanks for the kind words!

    I think that a level editor is probably unnecessary for a Tower Defence because of how small the levels are, however if you really want to make one you could check out Nick Gravelyn's tutorials.

    ReplyDelete
  7. You are right Anonymous the HasTarget property should look like this :

    public virtual bool HasTarget
    {
    // Check if the tower has a target.
    get { return target != null; }
    }

    Thanks for pointing out the bug! :)

    ReplyDelete
  8. A custom tower class would be nice !

    ReplyDelete
  9. Hey FireFly no problem =)

    I had to say your tutorials are very nice!

    And yes an custom tower class would be really nice =)

    ReplyDelete
  10. Hi cool tutorial. I really like it :-)

    - A Button to upgrade the tower would be nice. So he does more damage or has a longer shooting range.
    - The player sould get some money when he kills an enemy or survives a round.
    - Multiplayer would be nice too, but i think its nothing easy to do.

    Best Regards
    Alex

    ReplyDelete
  11. Your finding target code is very inefficient.
    Your bullet should be the one keeping track of when it should expire, not the tower.

    ReplyDelete
  12. So you think for every bullet that is fired, the bullet should store some kind of reference to the tower that fired it so that the bullet knows when it is out of the range of the tower? Should the bullet also be aware of all the enemy's as well so it knows when it hits one...?

    I think my method of just letting the tower handle its own bullets would be more efficient than that.

    Of course there are better ways to handle the bullets more efficiently, i.e using some kind of resource pool.

    Maybe I misunderstood you?

    ReplyDelete
  13. It doesn't need a reference to the tower or to the enemies, you can simply just say once it has travelled x distance, it has expired.

    ReplyDelete
  14. But the tower will still have to remove the dead bullet from it's list of bullets, and for your idea to work each bullet would need to keep track of the towers range, I just don't see the benefit of the bullet keeping track of when it should expire.

    ReplyDelete
  15. After adding this new tower... everything works fine except now I can place towers on top of each other.

    ReplyDelete
  16. nevermind, the problem was my code not yours. my bad.

    ReplyDelete
  17. I get this error:
    An unhandled exception of type 'System.NullReferenceException' occurred in TowerTest.exe

    Additional information: Object reference not set to an instance of an object.

    for this line of code in the ArrowTower class:
    if (Vector2.Distance(bullet.Center, target.Center) < 12)

    ReplyDelete
  18. I'm commented here^^^ because the error only started after I made the spike tower

    ReplyDelete
  19. Hey Anonymous,

    Are you getting this error at the end of the tutorial or are you still working through it?

    If you haven't finished the tutorial yet the chances are that it may get fixed later in the tutorial!

    If not just leave another comment and I will try and help some more!

    ReplyDelete
  20. Sorry for taking so long to reply I was away on vacation. I have worked through the tutorial and didn't have a problem till I tested the game and added a few arrow towers. Also I was wondering, if you aren't too busy, could you write a tutorial about making a tower the does splash damage or help point me in the right direction of how I should go about doing it? I've tried figuring it out by myself but I've only ended up screwing other things up lol

    ReplyDelete
  21. sorry I meant when I added a few spike towers, not arrow towers.

    ReplyDelete
  22. I'm sure you're busy but I was wondering if I could get some help with what I posted above? Thanks.

    ReplyDelete
  23. I think you are getting the error because the code you posted above should like like this :

    if (target != null && Vector2.Distance(bullet.Center, target.Center) < 12)

    Let me know if this helps!

    ReplyDelete
  24. Hi firefly, im having this problem which whenever i place my spike tower on the map it show null reference from the line if (IsCellClear() == true && towerToAdd.Cost <= money) i have gone through and through with this tutorial and the source code and have absolutely no idea what the problem im facing, would you kindly help me? =D

    ReplyDelete