Advertisement

Understanding Steering Behaviors: Path Following

by
This post is part of a series called Understanding Steering Behaviors.
Understanding Steering Behaviors: Collision Avoidance
Understanding Steering Behaviors: Leader Following

Path following is a frequent problem in game development. This tutorial covers the path following steering behavior, which allows characters to follow a predefined path made of points and lines.

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

A path following behavior can be implemented in several ways. The original Reynolds implementation uses a path made of lines, where characters follow them strictly, almost like a train on rails.

Depending on the situation, such precision may not be required. A character can move along a path following lines, but using them as a reference, rather than as rails.

The implementation of path following behavior in this tutorial is a simplification of the original one proposed by Reynolds. It still produces good results, but it does not rely on heavy math calculations such as vector projections.


Defining a Path

A path can be defined as a set of points (nodes) connected by lines. Even though curves can also be used to describe a path, points and lines are easier to handle and produce almost the same results.

If you need to use curves, they can be reduced to a set of connected points:


Curves and lines.

The class Path will be used to describe the route. Basically, the class has a vector of points and a few methods to manage that list:

public class Path
{
	private var nodes :Vector.<Vector3D>;

	public function Path() {
		this.nodes = new Vector.<Vector3D>();
	}

	public function addNode(node :Vector3D) :void {
		nodes.push(node);
	}

	public function getNodes() :Vector.<Vector3D> {
		return nodes;
	}
}

Every point in the path is a Vector3D representing a position in the space, the same way the character's position property works.


Moving From Node to Node

In order to navigate thought the path, the character will move from node to node until it reaches the end of the route.

Every point in the path can be seen as a target, so the seek behavior can be used:

Seek one point after another.

The character will seek the current point until it is reached, then the next point in the path becomes the current one and so on. As previously described in the collision avoidance tutorial, every behavior's forces are re-calculated every game update, so the transition from one node to another is seamless and smooth.

The character's class will need two additional properties to instrument the navigation process: the current node (the one the character is seeking) and a reference to the path being followed. The class will look like the following:

public class Boid
{
	public var path	:Path;
	public var currentNode	:int;
	(...)

	private function pathFollowing() :Vector3D {
		var target :Vector3D = null;

		if (path != null) {
			var nodes :Vector.<Vector3D> = path.getNodes();

			target = nodes[currentNode];

			if (distance(position, target) <= 10) {
				currentNode += 1;

				if (currentNode >= nodes.length) {
					currentNode = nodes.length - 1;
 				}
 			}
 		}

		return null;
 	}

 	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));
 	}
 	(...)
}

The pathFollowing() method is the one responsible for generating the path following force. Currently it produces no force, but it does select the targets properly.

The path != null test checks whether the character is following any path. If that's the case the currentNode property is used to look up the current target (the one the character must seek) in the list of points.

If the distance between the current target and the character's position is less than 10, it means the character has reached the current node. If that happens, currentNode is incremented by one, meaning the character will seek the next point in the path. The process is repeated until the path runs out of points.


Calculating and Adding Forces

The force used to push the character towards each node in the path is the seek force. The pathFollowing() method already chooses the appropriate node, so now it needs to return a force that will push the character towards that node:

private function pathFollowing() :Vector3D {
  var target :Vector3D = null;

  if (path != null) {
    var nodes :Vector.<Vector3D> = path.getNodes();

    target = nodes[currentNode];

    if (distance(position, target) <= 10) {
        currentNode += 1;

        if (currentNode >= nodes.length) {
            currentNode = nodes.length - 1;
        }
    }
  }

  return target != null ? seek(target) : new Vector3D();
}

After the path following force is calculated, it must be added to the character's velocity vector as usual:

steering = nothing(); // the null vector, meaning "zero force magnitude"
steering = steering + pathFollowing();

steering = truncate (steering, max_force)
steering = steering / mass

velocity = truncate (velocity + steering, max_speed)
position = position + velocity

The path following steering force is extremely similar to the pursuit behavior, where the character constantly adjusts its direction to catch the target. The difference lies in how the character seeks an immovable target, which is ignored in favor of another one as soon as the character gets too close.

The result is the following:

Path following in action. Click to show forces.

Smoothing the Movement

The current implementation requires all characters to "touch" the current point in the path in order to select the next target. As a consequence, a character might perform undesired movement patterns, such as moving in circles around a point until it is reached.

In nature, every movement tends to obey the principle of least effort. For instance, a person will not walk in the middle of a corridor all the time; if there is a turn, the person will walk closely to the walls while turning in order to shorten the distance.

That pattern can be recreated by adding a radius to the path. The radius is applied to the points and it can be seen as the route "width". It will control how distant a character can move from the points along the way:

Influence of radius on path following.

If the distance between the character and the point is less than or equal to the radius, the point is considered reached. As a consequence, all characters will move using the lines and points as guides:

Path following with radius. Click the "Force" button to show forces. Click the "+" and "-" buttons to dynamically adjust the radius size.

The greater the radius, the wider the route and the bigger the distance the characters will keep from the points when turning. The value of the radius can be tweaked to produce different following patterns.


Going Back and Forth

Sometimes it is useful for a character to keep moving after it reaches the end of the path. In a patrol pattern, for instance, the character should return to the beginning of the route after it reaches the end, following the very same points.

This can be achieved by adding the pathDir property to the character's class; this is an integer that controls the direction in which the character is moving along the path. If pathDir is 1, it means the character is moving towards the end of the path; -1 denotes a movement towards the start.

The pathFollowing() method can be changed to:

private function pathFollowing() :Vector3D {
    var target :Vector3D = null;

    if (path != null) {
        var nodes :Vector.<Vector3D> = path.getNodes();

        target = nodes[currentNode];

        if (distance(position, target) <= path.radius) {
            currentNode += pathDir;

            if (currentNode >= nodes.length || currentNode < 0) {
                pathDir *= -1;
                currentNode += pathDir;
            }
        }
    }

    return target != null ? seek(target) : new Vector3D();
}

Unlike the older version, the value of pathDir is now added to the property currentNode (instead of simply adding 1). This allows the character to select the next point in the path based on the current direction.

After that, a test checks whether the character has reached the end of the route. If that's the case, pathDir is multiplied by -1, which inverts its value, making the character invert the movement direction as well.

The result is a back-and-forth movement pattern:

Path following with radius and back-and-forth pattern. Click the "Force" button to show forces. Click the "+" and "-" buttons to dynamically adjust the radius size.

Conclusion

The path following behavior allows any character to move along a predefined path. The route is guided by points and it can be adjusted to be wider or narrower, producing movement patterns that feel more natural.

The implementation covered in this tutorial is a simplification of the original path following behavior proposed by Reynolds, but it still produces convincing and appealing results.

Advertisement