Imagine a game scene where a room is crowded with AI-controlled entities. For some reason, they must leave the room and pass through a doorway. Instead of making them walk over each other in a chaotic flow, teach them how to politely leave while standing in line. This tutorial presents the queue steering behavior with different approaches to make a crowd move while forming rows of entities.
Note: Although this tutorial is written using AS3 and Flash, you should be able to use the same techniques and concepts in almost any game development environment. You must have a basic understanding of math vectors.
Introduction
Queuing, in the context of this tutorial, is the process of standing in line, forming a row of characters that are patiently waiting to arrive somewhere. As the first in the line moves, the rest follow, creating a pattern that looks like a train pulling wagons. When waiting, a character should never leave the line.
In order to illustrate the queue behavior and show the different implementations, a demo featuring a "queuing scene" is the best way to go. A good example is a room crowded with AI-controlled entities, all trying to leave the room and pass through the doorway:
This scene was made using two previously described behaviors: seek and collision avoidance.
The doorway is made of two rectangular obstacles positioned side by side with a gap between them (the doorway). The characters seek a point located behind that. When there, the characters are re-positioned at the bottom of the screen.
Right now, without the queue behavior, the scene looks like a horde of savages stepping on each other's heads to arrive at the destination. When we're done, the crowd will smoothly leave the place, forming rows.
Seeing Ahead
The first ability a character must obtain to stand in line is to find out whether there is someone ahead of them. Based on that information, it can decide whether to continue or to stop moving.
Despite the existence of more sophisticated ways to check neighbors ahead, I'll use a simplified method based on the distance between a point and a character. This approach was used in the collision avoidance behavior to check for obstacles ahead:
A point called ahead
is projected in front of the character. If the distance between that point and a neighbor character is less than or equal to MAX_QUEUE_RADIUS
, it means there is someone ahead and the character must stop moving.
The ahead
point is calculated as follows (pseudo-code):
// Both qa and ahead are math vectors qa = normalize(velocity) * MAX_QUEUE_AHEAD; ahead = qa + position;
The velocity, which also gives the character's direction, is normalized and scaled by MAX_QUEUE_AHEAD
to produce a new vector called qa
. When qa
is added to the position
vector, the result is a point ahead of the character, and a distance of MAX_QUEUE_AHEAD
units away from it.
All of this can be wrapped in the getNeighborAhead()
method:
private function getNeighborAhead() :Boid { var i:int; var ret :Boid = null; var qa :Vector3D = velocity.clone(); qa.normalize(); qa.scaleBy(MAX_QUEUE_AHEAD); ahead = position.clone().add(qa); for (i = 0; i < Game.instance.boids.length; i++) { var neighbor :Boid = Game.instance.boids[i]; var d :Number = distance(ahead, neighbor.position); if (neighbour != this && d <= MAX_QUEUE_RADIUS) { ret = neighbor; break; } } return ret; }
The method checks the distance between the ahead
point and all other characters, returning the first character whose distance is less or equal to MAX_QUEUE_AHEAD
. If no character is found, the method returns null
.
Creating the Queuing Method
As with all other behaviors, the queuing force is calculated by a method named queue()
:
private function queue() :Vector3D { var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { // TODO: take action because neighbor is ahead } return new Vector3D(0, 0); }
The result of getNeighborAhead()
in stored in the variable neighbor
. If neighbor != null
it means that there is someone ahead; otherwise, the path is clear.
The queue()
, like all other behavior methods, must return a force which is the steering force related to the method itself. queue()
will return a force with no magnitude for now, so it will produce no effects.
The update()
method of all characters in the doorway scene, until now, is (pseudo-code):
public function update():void { var doorway :Vector3D = getDoorwayPosition(); steering = seek(doorway); // seek the doorway steering = steering + collisionAvoidance(); // avoid obstacles steering = steering + queue(); // queue along the way steering = truncate (steering, MAX_FORCE); steering = steering / mass; velocity = truncate (velocity + steering , MAX_SPEED); position = position + velocity;
Since queue()
returns a null force, the characters will continue to move without forming rows. It's time to make them take some action when a neighbor is detected right ahead.
Some Words About Stopping Movement
Steering behaviors are based on forces that constantly change, so the whole system becomes very dynamic. Depending on the implementation, the more forces that are involved, the harder it becomes to pinpoint and cancel a specific force vector.
The implementation used in this steering behavior series adds all forces together. As a consequence, to cancel a force, it must be re-calculated, inverted and added to the current steering force vector again.
That's pretty much what happens in the arrival behavior, where the velocity is canceled to make the character stop moving. But what happens when more forces are acting together, such as collision avoidance, flee, and more?
The following sections present two ideas for making a character stop moving. The first one uses a "hard stop" approach that acts directly on the velocity vector, ignoring all other steering forces. The second one uses a force vector, named brake
, to gracefully cancel all other steering forces, eventually making the character stop moving.
Stopping Movement: "Hard Stop"
Several steering forces are based on the character's velocity vector. If that vector changes, all other forces will be affected when they are recalculated. The "hard stop" idea is quite simple: if there is a character ahead, we "shrink" the velocity vector:
private function queue() :Vector3D { var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { velocity.scaleBy(0.3); } return new Vector3D(0, 0); }
In the code above, the velocity
vector is scaled to 30%
of its current magnitude (length) while a character is ahead. As a consequence, the movement is drastically reduced, but it will eventually come back to its normal magnitude when the character that is blocking the way moves.
That's easier to understand by analyzing how movement is calculated every update:
velocity = truncate (velocity + steering , MAX_SPEED); position = position + velocity;
If the velocity
force keeps shrinking, so does the steering
force, because it is based on the velocity
force. It creates a vicious cycle that will end up with an extremely low value for velocity
. That's when the character stops moving.
When the shrinking process ends, every game update will increase the velocity
vector a little, affecting the steering
force too. Eventually several updates after will bring both velocity
and steering
vector back to their normal magnitudes.
The "hard stop" approach produces the following result:
Even though this result is quite convincing, it feels like a "robotic" outcome. A real crowd usually has no empty spaces between their members.
Stopping Movement: Braking Force
The second approach for stopping movement tries to create a less "robotic" result by canceling all active steering forces using a brake
force:
private function queue() :Vector3D { var v :Vector3D = velocity.clone(); var brake :Vector3D = new Vector3D(); var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { brake.x = -steering.x * 0.8; brake.y = -steering.y * 0.8; v.scaleBy( -1); brake = brake.add(v); } return brake; }
Instead of creating the brake
force by re-calculating and inverting each one of the active steering forces, brake
is calculated based on the current steering
vector, which holds all steering forces added to the moment:
The brake
force receives both its x
and y
components from the steering
force, but inverted and with a scale of 0.8
. It means that brake
has 80% of the magnitude of steering
and points in the opposite direction.
Tip: Using the steering
force directly is dangerous. If queue()
is the first behavior to be applied to a character, the steering
force will be "empty". As a consequence, queue()
must be invoked after all other steering methods, so that it can access the complete and final steering
force.
The brake
force also needs to cancel the character's velocity. That's is done by adding -velocity
to the brake
force. After that, the method queue()
can return the final brake
force.
The result of using the brake force is the following:
Mitigating Characters' Overlap
The braking approach produces a more natural result compared to the "robotic" old one, because all characters are trying to fill the empty spaces. However, it introduces a new problem: characters are overlapping.
In order to fix that, the brake approach can be enhanced with a slightly modified version of the "hard stop" approach:
private function queue() :Vector3D { var v :Vector3D = velocity.clone(); var brake :Vector3D = new Vector3D(); var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { brake.x = -steering.x * 0.8; brake.y = -steering.y * 0.8; v.scaleBy( -1); brake = brake.add(v); if (distance(position, neighbor.position) <= MAX_QUEUE_RADIUS) { velocity.scaleBy(0.3); } } return brake; }
A new test is used to check nearby neighbors. This time instead of using the ahead
point to measure the distance, the new test checks the distance between the characters position
vector:
This new test checks whether there are any nearby characters within the MAX_QUEUE_RADIUS
radius, but now it is centered at the position
vector. If someone is in range, it means the surrounding area is becoming too crowded and characters are probably starting to overlap.
The overlapping is mitigated by scaling the velocity
vector to 30% of its current magnitude every update. Just like in the "hard stop" approach, shrinking the velocity
vector drastically reduces the movement.
The result seems less "robotic", but it's not ideal, since the characters are still overlapping at the doorway:
Adding Separation
Even though the characters are trying to reach the doorway in a convincing way, filling all empty spaces when the path becomes narrow, they are getting too close to each other at the doorway.
This can be solved by adding a separation force:
private function queue() :Vector3D { var v :Vector3D = velocity.clone(); var brake :Vector3D = new Vector3D(); var neighbor :Boid = getNeighborAhead(); if (neighbor != null) { brake.x = -steering.x * 0.8; brake.y = -steering.y * 0.8; v.scaleBy( -1); brake = brake.add(v); brake = brake.add(separation()); if (distance(position, neighbor.position) <= MAX_QUEUE_RADIUS) { velocity.scaleBy(0.3); } } return brake; }
Previously used in the leader following behavior, the separation force added to the brake
force will make characters stop moving at the same time they try to stay away from each other.
The result is a convincing crowd trying to reach the doorway:
Conclusion
The queue behavior allows characters to stand in line and patiently wait to arrive at the destination. Once in line, a character will not try to "cheat" by jumping positions; it will move only when the character right in front of it moves.
The doorway scene used in this tutorial presented how versatile and tweakable this behavior can be. A few changes produce different results, which can be fine adjusted to a wide variety of situations. The behavior can also be combined with others, such as collision avoidance.
I hope you liked this new behavior and start using it to add moving crowds to your game!