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

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

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

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.

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?

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.

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: