domingo, 9 de junio de 2013

Understanding Steering Behaviors: Collision Avoidance

This entry is part 6 of 6 in the series Understanding Steering Behaviors

Decent NPC navigation often requires the ability to avoid obstacles. This tutorial covers the collision avoidance steering behavior, which allows characters to gracefully dodge any number of obstacles in the environment.

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

The basic idea behind collision avoidance is to generate a steering force to dodge obstacles every time one is close enough to block the passage. Even if the environment has several obstacles, this behavior will use one of them at a time to calculate the avoidance force.

Only the obstacles ahead of the character are analyzed; the closest one, said to be the most threatening, is selected for evaluation. As a result the character is able to dodge all obstacles in the area, transitioning from one to another gracefully and seamlessly.


Obstacles ahead of the character are analyzed and the closest one (most threatening) is selected.

The collision avoidance behavior is not a path finding algorithm. It will make characters move through the environment, avoiding obstacles, eventually finding a route to go through the blocks – but it does not work really well with “L” or “T” obstacles, for instance.

Tip: This collision avoidance behavior may sounds similar to the flee behavior, but there is an important difference between them. A character moving near a wall will avoid it only if it’s blocking the way, but the flee behavior will always push the character away from the wall.

Seeing Ahead

The first step to avoid obstacles in the environment is to perceive them. The only obstacles the character must worry are the ones that are in front of it and directly blocking the current route.

As previously explained, the velocity vector describes the direction of the character. It will be used to produce a new vector called ahead, which is a copy of the velocity vector, but with a different length:


The ahead vector is the character’s line of sight.

This vector is calculated as follows:

  ahead = position + normalize(velocity) * MAX_SEE_AHEAD  

The ahead vector length (adjusted with MAX_SEE_AHEAD) defines how far the character will “see”.

The greater MAX_SEE_AHEAD is, the earlier the character will start acting to dodge an obstacle, because it will perceive it as a threat even if it’s far away:


The greater the ahead length is, the earlier the character will start acting to dodge an obstacle.

Checking for Collision

In order to check for collision, every obstacle (or its bounding box) must be described as a geometric form. Using a sphere (circle in two dimensins) gives the best results, so every obstacle in the environment will be described as such.

One possible solution to check for collision is the line-sphere intersection – the line is the ahead vector and the sphere is the obstacle. That approach works, but I’m going to use a simplification of that which is easier to understand and has similar results (even better ones at times).

The ahead vector will be used to produce another vector with half of its length:


Same direction, half the length.

The ahead2 vector is calculated exactly like ahead, but its length is cut in half:

  ahead = position + normalize(velocity) * MAX_SEE_AHEAD  ahead2 = position + normalize(velocity) * MAX_SEE_AHEAD * 0.5  

We want to perform a collision check to test whether either of those two vectors are inside the obstacle sphere. That’s easily accomplished by comparing the distance between the vector’s end and the sphere’s center.

If the distance is less than or equal to the sphere radius, then the vector is inside the sphere and a collision was found:


The ahead vector is intercepting the obstacle if d < r. The ahead2 vector was omitted for clarity.

If either of the two ahead vectors are inside the obstacle sphere, then that obstacle is blocking the way. The Euclidean distance between two points can be used:

  private function distance(a :Object, b :Object) :Number {          return Math.sqrt((a.x - b.x) * (a.x - b.x)  + (a.y - b.y) * (a.y - b.y));  }    private function lineIntersectsCircle(ahead :Vector3D, ahead2 :Vector3D, obstacle :Circle) :Boolean {          // the property "center" of the obstacle is a Vector3D.          return distance(obstacle.center, ahead) <= obstacle.radius || distance(obstacle.center, ahead2) <= obstacle.radius;  }  

If more than one obstacle is blocking the way, then the closest one (the “most threatening”) is selected for calculation:


The closest obstacle (most threatening) is selected for calculation.

Calculating the Avoidance Force

The avoidance force must push the character away from the obstacle, allowing it to dodge the sphere. It can be done using a vector formed by using the center of the sphere (which is a position vector) and the ahead vector. We calculate this avoidance force as follows:

  avoidance_force = ahead - obstacle_center  avoidance_force = normalize(avoidance_force) * MAX_AVOID_FORCE  

After avoidance_force is calculated it is normalized and scaled by MAX_AVOID_FORCE, which is a number used to define the avoidance_force length. The greater MAX_AVOID_FORCE is, the stronger is the avoidance force pushing the character away from the obstacle.


Avoidance force calculation. The dashed orange line shows the path the character will take to avoid the obstacle. Tip: The position of any entity can be described as a vector, so they can be used in calculations with other vectors and forces.

Avoiding the Obstacle

The final implementation for the collisionAvoidance() method, which returns the avoidance force, is:

  private function collisionAvoidance() :Vector3D {          ahead = ...; // calculate the ahead vector          ahead2 = ...; // calculate the ahead2 vector            var mostThreatening :Obstacle = findMostThreateningObstacle();          var avoidance :Vector3D = new Vector3D(0, 0, 0);            if (mostThreatening != null) {                  avoidance.x = ahead.x - mostThreatening.center.x;                  avoidance.y = ahead.y - mostThreatening.center.y;                    avoidance.normalize();                  avoidance.scaleBy(MAX_AVOID_FORCE);          } else {                  avoidance.scaleBy(0); // nullify the avoidance force          }            return avoidance;  }    private function findMostThreateningObstacle() :Obstacle {          var mostThreatening :Obstacle = null;            for (var i:int = 0; i < Game.instance.obstacles.length; i++) {                  var obstacle :Obstacle = Game.instance.obstacles[i];                  var collision :Boolean = lineIntersecsCircle(ahead, ahead2, obstacle);                    // "position" is the character's current position                  if (collision && (mostThreatening == null || distance(position, obstacle) < distance(position, mostThreatening))) {                          mostThreatening = obstacle;                  }          }          return mostThreatening;  }  

The avoidance force must be added to the character’s velocity vector. As previously explained, all steering forces can be combined into one, producing a force that represents all active behavior acting on the character.

Depending on the avoidance force angle and direction it will not interrupt other steering forces, such as seek or wander. The avoidance force is added to the player velocity as usual:

  steering = nothing(); // the null vector, meaning "zero force magnitude"  steering = steering + seek(); // assuming the character is seeking something  steering = steering + collisionAvoidance();    steering = truncate (steering, max_force)  steering = steering / mass    velocity = truncate (velocity + steering, max_speed)  position = position + velocity  

Since all steering behaviors are re-calculated every game update, the avoidance force will remain active as long as the obstacle is blocking the way.

As soon as the obstacle is not intercepting the ahead vector line, the avoidance force will become null (no effect) or it will be re-calculated to avoid the new threatening obstacle. The result is a character that is able to avoid obstacles:


Move the mouse cursor. Click to show forces.

Improving Collision Detection

The current implementation has two problems, both related to the collision detection. The first one happens when the ahead vectors are outside the obstacle sphere, but the character is too close to (or inside) the obstacle.

If that happens, the character will touch (or enter) the obstacle, skipping the avoidance process because no collision was detected:


Sometimes the ahead vectors are outside the obstacle, but the character is inside.

This problem can be fixed by adding a third vector to the collision check: the character’s position vector. The use of three vectors greatly improves the collision detection.

The second problem happens when the character is close to the obstacle, steering away from it. Sometimes the maneuvering will cause a collision, even though the character is just rotating to face another direction:


Maneuvering might cause a collision, even though the character is just rotating.

That problem can be fixed by scaling the ahead vectors according to the character’s current velocity. The code to calculate the ahead vector, for instance, is changed to:

  dynamic_length = length(velocity) / MAX_VELOCITY  ahead = position + normalize(velocity) * dynamic_length  

The variable dynamic_length will range from 0 to 1. When the character is moving at full speed, dynamic_length is 1; when the character is slowing down or accelerating, dynamic_length is 0 or greater (e.g. 0.5).

As a consequence, if the character is just maneuvering without moving, dynamic_length tends to zero, producing a null ahead vector, which has no collisions.

Below is the result with these improvements:


Move the mouse cursor. Click to show forces.

Demo: It’s Zombie Time!

In order to demonstrate the collision avoidance behavior in action, I think a horde of zombies is the perfect fit. Below is a demo showing several zombies (with different velocities) seeking the mouse cursor. Art by SpicyPixel and Clint Bellanger, from OpenGameArt.


Move the mouse cursor. Click to show forces.

Conclusion

The collision avoidance behavior allows any character to dodge obstacles in the environment. Since all steering forces are re-calculated every game update, the characters seamlessly interact with different obstacles, always analyzing the most threatening one (the closest).

Even though this behavior is not a path finding algorithm, the results achieved are quite convincing for crowded maps.



No hay comentarios:

Publicar un comentario