This post is part of a series called
Understanding Steering Behaviors.
http://gamedevelopment.tutsplus.com/tutorials/understanding-steering-behaviors-queue--gamedev-14365
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:
Boids leaving the room and passing through the doorway without the queue behavior. Click to show forces.
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:
Test for neighbors using the ahead point.
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):
1
2
3
|
// 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 byMAX_QUEUE_AHEAD
to produce a new vector called qa
. When qa
is added to theposition
vector, the result is a point ahead of the character, and a distance ofMAX_QUEUE_AHEAD
units away from it.
All of this can be wrapped in the getNeighborAhead()
method:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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 namedqueue()
:
1
2
3
4
5
6
7
8
9
|
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):
01
02
03
04
05
06
07
08
09
10
11
12
|
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:
1
2
3
4
5
6
7
8
9
|
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:
1
2
|
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:
Queue behavior with "hard stop" approach. Click to show forces.
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
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:
Representation of brake force.
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 ofsteering
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:
Queue behavior using the brake force approach. Click to show forces.
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
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 charactersposition
vector:
Check nearby neighbors within the MAX_QUEUE_RADIUS radius centered at the position instead of the ahead point.
This new test checks whether there are any nearby characters within theMAX_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:
Queue behavior with "hard stop" and brake force combined. Click to show forces.
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:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
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 thebrake
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:
Queue behavior with "hard stop", brake force and separation combined. Click to show forces.
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 ascollision avoidance.
I hope you liked this new behavior and start using it to add moving crowds to your game!