Tuesday 5 October 2010

Tutorial 8 : Waves (Featuring Tower Modifications)

In this tutorial we will be focusing on creating so called “waves” of units.
We will start off by fixing some nasty bugs that I overlooked in the last two tutorials.
Then we will move onto creating our Wave class. This will contain information such as how many enemies should be created this wave, and which of these created enemies are still alive. It will be responsible for creating, updating and drawing the enemies.

For the sake of simplicity I am not going to add in any “boss levels” or “fast levels” etc. for now as that would mean coming up with some pretty difficult formulas!

So, let’s begin, to start with go into “ArrowTower.cs” and find where we update our bullets. We are going to add in some code that will actually make our bullets damage the tower’s target. Just before we check if the bullet is dead add the following :

if (target != null && Vector2.Distance(bullet.Center, target.Center) < 12)
{
    target.CurrentHealth -= bullet.Damage;
    bullet.Kill();
}


All this code does is check whether the bullet has hit out target, and if it has, make sure we take some health off of the enemy and then kill the bullet. So where did the 12 come from – quite simply it is this :

(width of bullet / 2) + (width of enemy / 2) = (12 / 2) + (12 / 2)

So now the code to update our bullets should look like this :


for (int i = 0; i < bulletList.Count; i++)
{
    Bullet bullet = bulletList[i];

    bullet.SetRotation(rotation);
    bullet.Update(gameTime);
    if (!IsInRange(bullet.Center))
        bullet.Kill();

    if (Vector2.Distance(bullet.Center, target.Center) < 12)
    {
        target.CurrentHealth -= bullet.Damage;
        bullet.Kill();
    }

    if (bullet.IsDead())
    {
        bulletList.Remove(bullet);
        i--;
    }
}

Next you need to go to Tower.cs and, in the Update method, find the lines that look like this :

if (target != null)
{
    FaceTarget();

    if (!IsInRange(target.Center))
    {
        target = null;
        bulletTimer = 0;
    }
}
and replace them with these :

if (target != null)
{
    FaceTarget();

    if (!IsInRange(target.Center) || target.IsDead)
    {
        target = null;
        bulletTimer = 0;
    }
}

I’m sorry I missed these things out in the last tutorial!! I would have just modified the last tutorials to fix these bugs but I thought it might confuse some people.

And that’s our modifications to the tower class finished. If you run the project now and place a tower, it should shoot the enemy and the enemy should gradually change colour when hit.

Right, let’s move onto our waves, create a new class called “Wave.cs” and add the following fields and variables :


private int numOfEnemies; // Number of enemies to spawn
private int waveNumber; // What wave is this?
private float spawnTimer = 0; // When should we spawn an enemy
private int enemiesSpawned = 0; // How mant enemies have spawned

private bool enemyAtEnd; // Has an enemy reached the end of the path?
private bool spawningEnemies; // Are we still spawing enemies?
private Level level; // A reference of the level
private Texture2D enemyTexture; // A texture for the enemies
public List<Enemy> enemies = new List<Enemy>(); // List of enemies

public bool RoundOver
{
    get 
    { 
        return enemies.Count == 0 && enemiesSpawned == numOfEnemies; 
    }
}
public int RoundNumber
{
    get { return waveNumber; }
}

public bool EnemyAtEnd
{
    get { return enemyAtEnd; }
    set { enemyAtEnd = value; }
}
public List<Enemy> Enemies
{
    get { return enemies;}
}


A lot of these variables should explain themselves, however it may not be clear why we have added a couple of them. The reason we have a reference to the Level class is so that we can access the level’s waypoints. We must also store a copy of the enemy texture so that can be passed to the enemy when we create it.

Next we will add a constructor for the class :


public Wave(int waveNumber, int numOfEnemies,
    Level level, Texture2D enemyTexture)
{
    this.waveNumber = waveNumber;
    this.numOfEnemies = numOfEnemies;

    this.level = level;
    this.enemyTexture = enemyTexture;
}

I’m not going to insult you be explaining this :P. Now we are going to add to add three new methods, one to update the wave, and one to start the wave and one to add a new enemy. Here is the first :


private void AddEnemy()
{
    Enemy enemy = new Enemy(enemyTexture,
    level.Waypoints.Peek(), 50, 1, 0.5f);
    enemy.SetWaypoints(level.Waypoints);
    enemies.Add(enemy);
    spawnTimer = 0;

    enemiesSpawned++;
}


This is most of the magic of the wave class will happen. This is the method were you can alter the stats of the enemy based on the wave number. e.g Say that you want wave 5 to be a fast stage, you would do something like this :


if (waveNumber == 5)
{
    float speed = 2.0f;


    enemy = new Enemy(enemyTexture,
        level.Waypoints.Peek(), 50, 1, speed);
    enemy.SetWaypoints(level.Waypoints);
}


The second method we will add looks like this :


public void Start()
{
    spawningEnemies = true;
}
This method just tells the wave that it is time to start churning out enemies for us to gun down. Before we can move onto the Update method, go to Enemy.cs and replace the IsDead property with this :

public bool IsDead
{
    get { return !alive; }
}
We needed to make this change because it is no longer enough to make sure that an enemy has health, we also need to make sure is not at the end of the path.
Now we can move onto the Update method :


public void Update(GameTime gameTime)
{
}
The first thing we will add to this method is the code to spawn our enemies :

if (enemiesSpawned == numOfEnemies)
    spawningEnemies = false; // We have spawned enough enemies
if (spawningEnemies)
{
    spawnTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;
    if (spawnTimer > 2)
        AddEnemy(); // Time to add a new enemey
}
First we check if we still need to spawn enemies. Then if we do, we update the time since an enemy was last created. If 2 seconds have passed since the last time we created an enemy, then we will spawn a new enemy.
Next we will add some code to update and remove dead enemies :

for (int i = 0; i < enemies.Count; i++)
{
    Enemy enemy = enemies[i];
    enemy.Update(gameTime);
    if (enemy.IsDead)
    {
        if (enemy.CurrentHealth > 0) // Enemy is at the end
        {
            enemyAtEnd = true;
        }

        enemies.Remove(enemy);
        i--;
    }
}

Here, after we Update the enemy, we check if he is “dead”. I put “dead” in speech marks because, although IsDead tells us if an enemy should be dead, it can also indicate that the enemy has reached the end of his path. This is what we check for next, if an enemy IsDead but still has health this must mean he is at the end of his path, so we therefore set enemyAtEnd to true seeing an enemy has reached the end.
All that is left to do is draw the enemies in this wave :

public void Draw(SpriteBatch spriteBatch)
{
    foreach (Enemy enemy in enemies)
        enemy.Draw(spriteBatch);
}


And there we have it, our wave class!! Now let’s make a few changes to Game1.cs so our changes will show up. At the top of Game1.cs replace the line were we define our enemy with the following :


//Enemy enemy1;
Wave wave;

Next find where we initialize our enemy and replace it with this :

//enemy1 = new Enemy(enemyTexture, Vector2.Zero, 100, 10, 0.5f);
//enemy1.SetWaypoints(level.Waypoints);
wave = new Wave(0, 10, level, enemyTexture);
wave.Start();

After that find where we update our enemy and replace it with this :

//enemy1.Update(gameTime);

//List<Enemy> enemies = new List<Enemy>();
//enemies.Add(enemy1);
wave.Update(gameTime);
player.Update(gameTime, wave.Enemies);

Finally find where we draw the enemy and replace it with this :

//enemy.Draw(spriteBatch);
wave.Draw(spriteBatch);

Now you should see about ten black dots appear at the start of your path and march to the end!! Happy hunting.


25 comments:

  1. how can i create more waves?
    thanks :-)

    ReplyDelete
  2. sorted it thanks :-)

    ReplyDelete
  3. Check out my latest tutorial :)

    ReplyDelete
  4. i thingk, in the last section you are missing:
    player.Update(gameTime, wave.Enemies);
    best regards,
    hannes

    ReplyDelete
  5. Thanks for pointing that out - Fixed

    ReplyDelete
  6. Made a quick change to the Tower.CS that was bugging me. Towers were facing dead enemies after they 'disappeared'. (Note, I'm just starting this tutorial, so I'm not sure if this will conflict with the WAVES code at all.)

    if (target != null) //Target?
    {
    if (!target.IsDead) //Is it Alive?
    FaceTarget(); //Face it

    if (!IsInRange(target.Center)|| target.IsDead)
    {
    target = null;
    bulletTimer = 0;
    }

    }

    ReplyDelete
  7. Huh I'm not sure why the tower is still following the dead enemy, I just downloaded the source at the end of the tutorial and this doesn't happen to me so maybe you missed something?

    But anyway that's a good fix and I can't see it affecting anything later on!

    ReplyDelete
  8. I also experienced the whole "towers targetting dead mobs" problem and while Somber's fix does fix the visual problem, the mob is still following the waypoint. I fixed that by opening up Enemy.cs and in the update changing:

    if (waypoints.Count > 0)

    To:

    if (waypoints.Count > 0 && alive)

    Other than that, great job! :)

    ReplyDelete
  9. im trying to find a way to make GetClosestEnemy to select the closest enemy to the goal that is in range. instead of just the closest. any ideas?

    ReplyDelete
    Replies
    1. in case anyone else needs it, here is my solution:

      public void GetTarget(List enemies,Vector2 goal)
      {
      target = null;
      float smallestRange = 100000;
      List targets = new List();
      foreach (Enemy enemy in enemies)
      {
      if (Vector2.Distance(center, enemy.Center) < radius)
      {
      targets.Add(enemy);
      }
      }
      foreach (Enemy enemy in targets)
      {
      if (Vector2.Distance(center, goal) < smallestRange)
      {
      smallestRange = Vector2.Distance(center, goal);
      target = enemy;
      }
      }
      }

      Delete
    2. You could also check this out : http://forums.create.msdn.com/forums/t/90241.aspx

      Although it should be the same as yours.

      Delete
  10. Hello FireFly!

    I have one question.. i want to create more different types of enemies (example: enemy1 and enemy2). Enemy1 will have speed 0.5f, enemy2 will have only 50 health...
    I think that I've tried everything, but I've always failed. All help will be very usefull.
    Thanx!

    ReplyDelete
  11. Then you will wish to change the AddEnemy() method in Wave.cs. The enemy constructor takes both speed and health as input values.

    For example if you wanted the enemies of wave 4 to be faster you could do something like:

    Enemy enemy = new Enemy(enemyTexture,
    level.Waypoints.Peek(), 50, 1, 0.5f);

    if (waveNumber == 4)
    {
    enemy = new Enemy(enemyTexture,
    level.Waypoints.Peek(), 50, 1, 2.5f);
    }

    I hope that helps!

    ReplyDelete
  12. Thax for quick response, but i think that we don't think same.
    Well.. I would like to have two different types of enemys in one wave.
    Something like this:
    Enemy enemy = new Enemy(enemyTexture, level.Waypoints.Peek(), 50, 1, 0.5f);
    Enemy enemyTWO= new Enemy(enemyTextureTWO, level.Waypoints.Peek(), 30, 1, 2.5);


    I hope that this is more understandable.
    And by the way, great work ;)
    Thanx

    ReplyDelete
  13. Oh ok I understand. Then what I would do is store a new list of enemies in the Wave class and call it something like enemiesToSpawn. In the wave constructor fill this list with all of the different enemies you would like to spawn:

    for(int i = 0; i < numberOfFastEnemiesToSpawn)
    {
    enemiesToSpawn.Add(new Enemy(enemyTextureFast, level.Waypoints.Peek(), 30, 1, 2.5));
    }

    for(int i = 0; i < numberOfBossEnemiesToSpawn)
    {
    enemiesToSpawn.Add(new Enemy(enemyTextureBoss, level.Waypoints.Peek(), 300, 10, 0.2));
    }

    Then instead of creating a new enemy in the AddEnemy method, you would change it to something like:

    Enemy enemy = enemiesToSpawn[0];
    enemiesToSpawn.RemoveAt(0);

    If you do it this way you will need to change this in the Update() method:

    if (enemiesSpawned == numOfEnemies)
    spawningEnemies = false;

    To something like:

    if (enemiesToSpawn.Count <= 0)
    spawningEnemies = false;

    I hope that helps!

    ReplyDelete
  14. well.. after 2 days of programming, i still couldn't figure it out what should I do.

    ReplyDelete
  15. I've got it working with the fast monsters, but now after the 2nd wave the game stops and waves won't continue...

    ReplyDelete
  16. After editing the RoundOver property from:

    public bool RoundOver
    {
    get
    {
    return monsters.Count == 0 && monstersSpawned == numOfMonsters ;
    }
    }

    to:

    public bool RoundOver
    {
    get
    {
    return monsters.Count == 0 && monstersToSpawn.Count == 0 ;
    }
    }

    My waves will now go past number 2 (The first wave in which I added my fast monsters)

    ReplyDelete
  17. Ah cool i'm glad you fixed that, but I'm not sure what you are now struggling with, did you add the two different types of monster to the list?

    ReplyDelete
  18. how do i get infinty waves

    ReplyDelete
  19. Hello FireFly, FIrst of all,thank's for your tutorial,its a good starting point to work on,now i have a question,what's about animating the enemy's?i mean, i have tryed to use instead of using a single image,would be nice to make the enemie's moving,IE like an animated car and so on,i have splitted your Sprite class away from the enemy class,and worked a bit over it, right now i can import and make the sprite work,but the problem comes with the enemy spawn, the first enemy its ok,it's anymated and everything, but as soon as the second enemy spawns,the first one disappear,under debugging i can see the enemy count, and its ok, but on the screen, i can see only one enemy,do you have any hint?
    thank's again for your time,and sorry for the long post :)

    ReplyDelete
  20. first of all great tutorial!!

    But I have a problem and i just can't find out why.. I'm getting an error all the time.. the round starts, I build a few towers and ERROR.. just when a shoot is going to hit an enemy.. in this line:

    if (Vector2.Distance(bullet.Center, target.Center) < 12)
    {
    target.CurrentHealth -= bullet.Damage;
    bullet.Kill();
    }

    it writes NullReference Exception, but I don't know what is null and what the 12 stands for.. I am a beginner at programming sth with C# so it's just a little mistake.. but i hope u can help me!! ^^

    ReplyDelete
  21. When you get the error and visual studio pops up, try hovering your mouse over each individual field name, e.g hover the mouse over where it says bullet and it should tell you if it's null.

    Does this error still occur once you have completed the tutorial? Have you compared your code to the sample code?

    What the first line says is, if the distance between the bullet and the enemy is less than some distance, a collision happens.

    In this case I set that distance to twelve as the radius of the enemy is 8 and the radius of the bullet is 4. (4 + 8 = 12)

    Let me know if that helps!

    ReplyDelete
  22. I'm sorry I haven't completed the tutorial.. and I changed the size of the map, so mine is much bigger, but this doesn't really effect this line, does it?

    it just tells me that i = 0

    i dont know if this really helps..

    ReplyDelete
  23. everything works great except for in Wave.cs in the Draw method
    enemy.Draw(spriteBatch)
    has an error: No overload for method 'Draw' takes 1 arguments
    its inheriting the Sprite.Draw method and wants a spriteBatch and a color. im not sure if i did something wrong or if there was something left out.
    any help would be great!

    ReplyDelete