Sunday, 26 September 2010

Tutorial 4 : Waypoints

In this tutorial, I will show you a method on how we can get our newly created enemy’s to follow the path that we have drawn out on our level.
The first big hurdle we face is, how will an enemy where the path is, when should he turn, when does he know when he is at the end of the path? This is where waypoints come in, when we are designing our levels, we can specify a set of points along a path (normally on the corners of paths), then when we load in our levels, we will tell our enemy’s that we want them to move between these points until they reach the end of the path.
image
As shown in the image above, we will specify a start and an end for the enemy, as well as intermediate points.
We will store this path data in the level class. For the sake of simplicity we will hard code in the way points now, however later on I will show you a way of loading them in from an xml file.
Go to “Level.cs” and add the following field :
private Queue<Vector2> waypoints = new Queue<Vector2>();
They reason we are using a Queue to store our waypoints, is so that when we add our points, we will be able to access the first waypoint we added first, and the last added waypoint last. This behaviour is different from a Stack<Vector2> because in a stack you access the first added waypoint last, and the last added waypoint first.
For more information on queues see the MDSN Documentation.

Now go to the Level() constructor and add the following :

public Level()
{
    waypoints.Enqueue(new Vector2(2, 0) * 32);
    waypoints.Enqueue(new Vector2(2, 1) * 32);
    waypoints.Enqueue(new Vector2(3, 1) * 32);
waypoints.Enqueue(new Vector2(3, 2) * 32);
    waypoints.Enqueue(new Vector2(4, 2) * 32);
    waypoints.Enqueue(new Vector2(4, 4) * 32);
    waypoints.Enqueue(new Vector2(3, 4) * 32);
    waypoints.Enqueue(new Vector2(3, 5) * 32);
    waypoints.Enqueue(new Vector2(2, 5) * 32);
    waypoints.Enqueue(new Vector2(2, 7) * 32);
    waypoints.Enqueue(new Vector2(7, 7) * 32);
}

All we are doing here is hard coding in our path, notice how we multiply the Vector2 by 32 so that we transform the value from array space into our level space. Then simply after the level properties add in a new property for the waypoints :


public Queue<Vector2> Waypoints
{
    get { return waypoints; }
}

Great, so we now have a queue of points that will guide our enemies. So lets add in a way to pass that list onto our enemies.

Go to “Enemy.cs” and just above the fields, add the following :


private Queue<Vector2> waypoints = new Queue<Vector2>();


Now we will add in a method that receives our queue of waypoints, then copy’s them into the enemy’s waypoint queue, add the following just under the Enemy() constructor :


public void SetWaypoints(Queue<Vector2> waypoints)
{
    foreach (Vector2 waypoint in waypoints)
        this.waypoints.Enqueue(waypoint);
 
    this.position = this.waypoints.Dequeue();
}


The reason why we don’t just make the two queues equal each other, is because when we pass our queue in to the method, we are only passing in a reference to our queue, so if we modified this queue in our enemy class, then the queue would also be modified in the level class, which is not what we want.

Next, we are going to add in a helper method to check whether our enemy has reached it’s next waypoint, just under the properties add this :


public float DistanceToDestination
{
    get { return Vector2.Distance(position, waypoints.Peek()); }
}

This just calculates how for we are from the waypoint at the front of the queue. Now in the Enemy Update() method add the following just after base.Update(gameTime) :


if (waypoints.Count > 0)
{
    if (DistanceToDestination < speed)
    {
        position = waypoints.Peek();
        waypoints.Dequeue();
    }

    else
    {
        Vector2 direction = waypoints.Peek() - position;
        direction.Normalize();
 
        velocity = Vector2.Multiply(direction, speed);
 
        position += velocity;
    }
}
 
else
    alive = false;


Right, lets look at this code step by step, first we simply check if their are any more waypoints to head towards, if not we will just kill the enemy.

The next statement checks whether we are “near enough” to the next waypoint to just say that we have arrived, this give us a little tolerance just in case our enemy goes a little bit past the way point. If the enemy is close enough, then we just set the enemy position to the position of the waypoint and then remove that waypoint so we can move onto the next.

In the second statement (else) we calculate the direction that we need to travel in to get from our current position to the next waypoint, and then simply increase our velocity in that direction based on the speed our entity can travel.

And there we have it, a base class for all our enemy’s that will follow a path. So let’s see it in action! Go into “Game1.cs” and find where the LoadContent() method. Replace the line where we create our enemy with the following :


enemy1 = new Enemy(enemyTexture, Vector2.Zero, 100, 10, 0.5f);
enemy1.SetWaypoints(level.Waypoints);


Here we pass in the queue of waypoints we created in our level class to the enemy class. If you run the project now, you will probably still see our enemy change colour and die before he gets to the end of the path, to fix this, just find the Update() method and remove this line :


enemy1.CurrentHealth -= 1;


Now when you run the project, you should see a little black dot appear at the start of the path, then follow the path to the end, and then disappear.

15 comments:

  1. Great series! Thanks for your effort!

    ReplyDelete
  2. I added in (as the last line in my SetWaypoints function of Enemy):

    _position = _waypoints.Peek();

    This way, the enemy's starting location is at the first waypoint, as opposed to the 'default' 0,0 vector.

    This tutorial is awesome so far, having a blast. Thanks for all your hard work.

    ReplyDelete
  3. I'm glad your enjoying my tutorials! :)

    That would work, another way you could set the enemies position to be at the first way point is to change this line :

    enemy1 = new Enemy(enemyTexture, Vector2.Zero, 100, 10, 0.5f);

    to this :

    enemy1 = new Enemy(enemyTexture, level.Waypoints.Peek(), 100, 10, 0.5f);

    and then enemies are created at the right position! (I haven't tested this I just assume it will work ;) )

    ReplyDelete
  4. Hi!
    First of all: Thanks for your very helpful tutorial. It really makes fun to play with the code and learn thereby XNA :).

    I try to use sprites which are rescaled at runtime to fit path width but I get unexpected behaviour which I don't understand.

    I changed the appropriate lines in constructor and Update method in Sprite.cs:
    Constructor:
    center = new Vector2(position.X + GameConstants.StandardDimension / 2, position.Y +GameConstants.StandardDimension / 2);
    origin = new Vector2(GameConstants.StandardDimension / 2, GameConstants.StandardDimension / 2);

    Update method:
    this.center = new Vector2(position.X + GameConstants.StandardDimension / 2, position.Y + GameConstants.StandardDimension / 2);

    GameConstant is public static class, StandardDimension is public static int.

    My Draw method in Sprite.cs now looks like:
    public virtual void Draw(SpriteBatch spriteBatch, Color color, Rectangle animationRectangle){
    float scaleFactor = Convert.ToSingle(GameConstants.StandardDimension) / Convert.ToSingle(this.texture.Height);
    spriteBatch.Draw(texture, center, animationRectangle, color, rotation, origin, scaleFactor, SpriteEffects.None, 0);
    }

    My texture is a spritesheet with some sprites in a row for animation of the enemy. animationRectangle is a rectangle which specifies the section of the spritesheet to displayed (the rectangle is moved along the spritesheet in the Update method of Enemy class. All works like expected if the real height of sprite is equal to GameConstants.StandardDimension (so the images are not rescaled).

    The unexpected behaviour (at least for me) is that the sprite is not positioned on the path whenever the scalefactor is not equal to 1.0!
    They are always shifted a little bit to the right (17 pixel for GameConstants.StandardDimension = 48 but this shift changes whenever I change GamesConstants.StandardDimension or the real size of sprite (so the scalefactor is changed...)). I really don't know why and I hope someone can help me.

    Thanks in advance :).

    ReplyDelete
  5. Got it :). Works now like expected. I didn't know that origin depends on real texture size and not the rectangle specified in Draw method of spriteBatch (destRect in my code).

    For those who might be interested in drawing sprites which are rescaled at runtime. Hear are my now working changes:

    GameConstants is public static class, StandardDimension is public static int.

    Changed lines in constructor method of Sprite.cs:
    origin = new Vector2((float)texture.Width / 2, (float)texture.Height / 2);
    center = new Vector2(position.X + (float)GameConstants.StandardDimension / 2, position.Y + (float)GameConstants.StandardDimension / 2);

    Changed lines in Update method:
    this.center = new Vector2(position.X + (float)GameConstants.StandardDimension / 2, position.Y + (float)GameConstants.StandardDimension / 2);

    My Draw method in Sprite.cs now looks like:
    public virtual void Draw(SpriteBatch spriteBatch, Color color){
    Rectangle destRect = new Rectangle((int)(center.X), (int)(center.Y), GameConstants.StandardDimension, GameConstants.StandardDimension);
    spriteBatch.Draw(texture, destRect, null, color, rotation, origin, SpriteEffects.None, 0);
    }

    You should also change all other lines which use hard coded texture size. For example:
    In Level class:
    Constructor:
    ...
    waypoints.Enqueue(new Vector2(2, 0) * GameConstants.StandardDimension);
    ...

    Draw method:
    ...
    batch.Draw(texture, new Rectangle(x * GameConstants.StandardDimension, y * GameConstants.StandardDimension, GameConstants.StandardDimension, GameConstants.StandardDimension), Color.White);
    ...

    and so on...

    ReplyDelete
  6. Excellent tutorials!!!! thank you very much.. ive learned a lot with you tutorials

    ReplyDelete
  7. how i implement pathfinding in branching way?

    ReplyDelete
  8. Very nice tutorials, Thank you very much.Just a little question.

    I have made my own path out of your example, just for testing. The sprite stops at the start position when path is done. Now I try to figure out how I can make an endless loop with this path. Some hint or idea for how I can solve that?

    Modified path:

    waypoints.Enqueue(new Vector2(0, 0) * 32);
    waypoints.Enqueue(new Vector2(7, 0) * 32);
    waypoints.Enqueue(new Vector2(7, 7) * 32);
    waypoints.Enqueue(new Vector2(0, 7) * 32);
    waypoints.Enqueue(new Vector2(0, 0) * 32);
    waypoints.Enqueue(new Vector2(14, 0) * 32);
    waypoints.Enqueue(new Vector2(14, 10) * 32);
    waypoints.Enqueue(new Vector2(0, 10) * 32);
    waypoints.Enqueue(new Vector2(0, 0) * 32);

    ReplyDelete
  9. You could most likely solve this by capturing the waypoint in memory before you dequeue and just add it back to the queue again. Essentially each time you remove an object you'd be adding it back so your queue remains the same size.

    ReplyDelete
    Replies
    1. Thank u, I will try that.

      Delete
  10. I can't get the enemy to start moving and I can't figure out why, I've redone the coding several times over to make sure that its in the right place and believe the waypoints are registered correctly due to the fact that:

    enemy1 = new Enemy(enemyTexture, level.Waypoints.Peek(), 100, 10, 0.5f);

    puts the enemy on the right starting path, he just won't move.

    Any help would be great.

    ReplyDelete
  11. im getting an error that the Queue is empty at

    this.position = this.waypoints.Dequeue();

    im fairly knew to XNA so im not quite sure where it isnt working

    ReplyDelete
    Replies
    1. got it. i just had to move the Level constructor to the top of Level.cs. sorry for the inconvenience and thank you for an incredible tutorial!

      Delete
  12. In these code:
    position = waypoints.Peek();
    waypoints.Dequeue();
    Why you just don't use
    position = waypoints.Dequeue();
    ???

    ReplyDelete
  13. Because I was young and foolish when I wrote this code ;)

    ReplyDelete