##### Tutorial Details
• Difficulty: Intermediate
• Platform: Flash (Flash Player 10+)
• Language: AS3
• Software Used: Flash Professional CS3+
• Estimated Completion Time: 50 minutes

Path following is a simple concept to grasp: the object moves from point A to point B to point C, and so on. But what if we want our object to follow the path of the player, like ghosts in racing games? In this tutorial, I’ll show you how to achieve this with waypoints in AS3.

## Final Result Preview

Click the SWF, then use the arrow keys to move around. Press space to switch to the ghost, which will follow the path you’ve created.

## The Logic Behind Path Following

Let’s suppose the player moves 4 units left and 2 units down from our point of origin. For our ghost to end up in the same location it will have to also move 4 units left and 2 units down from the same point of origin. Now let’s say our player is moving at a speed of 2; for the path following to remain accurate our ghost will also have a speed rate of 2.

What if our player decides to take a pause before continuing on? The obvious solution is for the ghost to keep track of the player’s exact position every tick – but this will involve storing a lot of data. Instead, what we’ll do is simply store data every time the player presses different keys – so if the player moves right for ten seconds, we’ll store the same amount of data as if the player moved right for half a second.

For this technique to work our ghost must abide by the following rules:

• The ghost and player have the same point of origin.
• The ghost must follow the exact same path as the player.
• The ghost should move at the same speed as the player.
• The ghost has to store the current time each time the player’s motion changes.

## Step 1: Setting Up

Start by creating a new Flash file (ActionScript 3.0). Set the width to 480, the height to 320 and frames per second to 30. Leave the background color as white and save the file as CreatingGhosts.fla; lastly set its class to CreatingGhosts.

Before we move into the classes we need to create a pair of MovieClips. Start by drawing two separate 20px squares without a stroke. Convert the first fill to a MovieClip, setting its registration to the center, naming it player and exporting it for ActionScript with the class name Player. Now repeat the same process, except replace the name with ghost and the class with Ghost. Remove these MovieClips from the stage.

Create your document class with the following code:

package{
import flash.display.*;
import flash.events.*;

public class CreatingGhosts extends MovieClip{
public var player:Player = new Player();
public function CreatingGhosts(){
}
}
}


Self explanatory; our next step will be to set up the Player class:

package{
import flash.display.*;
import flash.events.*;
import flash.geom.Point;
import flash.ui.Keyboard;
import flash.utils.Timer;
import flash.utils.getTimer;

public class Player extends MovieClip{
public var startPos:Point;
public var startTime:int;
public var speed:Number = 2;
public var currentLife:int;
public var keyPressLeft:Boolean = false;
public var keyPressRight:Boolean = false;
public var keyPressUp:Boolean = false;
public var keyPressDown:Boolean = false;
public function Player(){

}
}
}


The first three variables are used to help meet the rules; startPos is our point of origin, startTime is the time when the Player was added to the stage and speed is our our rate of movement. currentLife is an addition used to check how many times the player has died, accordingly each path is stored and obtainable through that value. The last four variables are used to check key presses.

It’s time to create the Ghost class:

package{
import flash.display.*;
import flash.events.*;
import flash.geom.Point;
import flash.utils.getTimer;
import flash.utils.Timer;

public class Ghost extends MovieClip{
static public var waypoints:Array = new Array();
static public var times:Array = new Array();
public var i:int = 0;
public var startTime:int;
public var speed:Number = 2;
public var selectedLife:int;
public function Ghost(){

}
}
}


The two static variables, waypoints and times, will be used to store arrays; the first will store coordinates of the player’s positions whenever the player changes motion, and the second will store the times at which each change occurred. The other variables match those from the Player class.

## Step 2: Initializing the Player

Within the Player’s constructor add the following line:

addEventListener(Event.ADDED_TO_STAGE, init);


Next create the init() function:

public function init(e:Event){

}


First, we need to obtain the startTime and push a new time array to the Ghost’s times array. (This is a little confusing; the ghost has multiple time arrays to allow it to deal with multiple lives in the future.)

startTime = flash.utils.getTimer();
Ghost.times.push(new Array);
currentLife = Ghost.times.length - 1;
Ghost.times[currentLife].push(flash.utils.getTimer() - startTime);


startTime is set to the current time (a value in milliseconds); we add a new child array to the Ghost’s times array; our currentLife is set to the index of this new array; and we push the time that has elapsed during this function to the first element of this new array.

Now we set up the starting position:

startPos = new Point(stage.stageWidth/2, stage.stageHeight/2);
this.x = startPos.x;
this.y = startPos.y;
Ghost.waypoints.push(new Array);
Ghost.waypoints[currentLife].push(startPos);


Our point of origin is set to the center of the stage; we reposition our Player to the origin; a new array is added to the waypoints array in the Ghost class; and the first position is pushed to that array.

So, at the moment, Ghost.times[0][0] contains the number of milliseconds since the SWF was set up (practically zero), and Ghost.waypoints[0][0] contains a Point set to the center of the stage.

Our aim is to code this so that if, after one second, the player presses a key, then Ghost.times[0][1] will be set to 1000, and Ghost.waypoints[0][1] will be another Point, again set to the center (because the player will not have moved yet). When the player lets go of that key (or presses another), Ghost.times[0][2] will be set to the current time, and Ghost.waypoints[0][2] will be a Point that matches the player’s position at that time.

Now, here are the three event listeners:

addEventListener(Event.ENTER_FRAME, enterFrame);


## Step 3: Key Events

For now let’s ignore the enterFrame and focus on the key presses.

public function keyDown(e:KeyboardEvent){
if (e.keyCode == Keyboard.LEFT && keyPressLeft == false){
updateWaypoints();
keyPressLeft = true;
}else if (e.keyCode == Keyboard.RIGHT  && keyPressRight == false){
updateWaypoints();
keyPressRight = true;
}

if (e.keyCode == Keyboard.UP  && keyPressUp == false){
updateWaypoints();
keyPressUp = true;
}else if (e.keyCode == Keyboard.DOWN  && keyPressDown == false){
updateWaypoints();
keyPressDown = true;
}

if (e.keyCode == Keyboard.SPACE){
destroy();
}
}


Just a few simple if-statements to prevent bugs in key presses, and two new functions that are being called. updateWaypoints() will be called every time new points and times are to be pushed to the ghost arrays, and destroy() is used to remove the Player and add the Ghost to the stage. But before we go to those functions let’s finish off the key press functions.

public function keyUp(e:KeyboardEvent){
if (e.keyCode == Keyboard.LEFT  && keyPressLeft == true){
updateWaypoints();
keyPressLeft = false;
}else if (e.keyCode == Keyboard.RIGHT  && keyPressRight == true){
updateWaypoints();
keyPressRight = false;
}

if (e.keyCode == Keyboard.UP  && keyPressUp == true){
updateWaypoints();
keyPressUp = false;
}else if (e.keyCode == Keyboard.DOWN  && keyPressDown == true){
updateWaypoints();
keyPressDown = false;
}
}


This time we do the opposite: the variables are set to false when the key is released and the waypoints are updated.

I will elaborate in more detail on what is happening between those functions. Each time you press a key the waypoints and times are updated, so if you press another to cause a change a point and its corresponding time are added to the ghost arrays.

But what happens if the player decides to randomly release a key and cause change again? Well we account for that by updating the waypoints and times again. If this was not done the Ghost would not be able to account for 90 degree turns; instead it would move on an angle towards the next point.

## Step 4: Updating and Destroying

Our updateWaypoints() function is fairly simple, seeing as it consists of code that we have already written:

public function updateWaypoints(){
Ghost.times[currentLife].push(flash.utils.getTimer() - startTime);
Ghost.waypoints[currentLife].push(new Point(this.x, this.y));
}


The destroy() function is just as simple! Waypoints are updated, a Ghost is added, event listeners are stopped and our Player is removed:

public function destroy(){
updateWaypoints();
var ghost:Ghost = new Ghost();
removeEventListener(Event.ENTER_FRAME, enterFrame);
stage.removeEventListener(KeyboardEvent.KEY_DOWN, keyDown);
stage.removeEventListener(KeyboardEvent.KEY_UP, keyUp);
parent.removeChild(this);
}


## Step 5: The Player’s enterFrame

Begin by creating the function:

public function enterFrame(e:Event){

}


For the purposes of this tutorial we will add some simple collision with borders, to show how the waypoints are updated on this change:

if((this.x-(this.width/2)) > 0){

}
if((this.x+(this.width/2)) < stage.stageWidth){

}
if((this.y-(this.height/2)) > 0){

}
if((this.y+(this.height/2)) < stage.stageHeight){

}


Now are player should only move in the specified direction while it isn’t touching a border. Inside the first if-statement add the following code for moving left:

if(keyPressLeft == true){
if((this.x-(this.width/2)) <= 0){
updateWaypoints();
this.x = this.width/2;
}else{
this.x -= speed;
}
}


First we check if the left key is currently down, then we check to see if the Player’s position is greater than or equal to 0; if so we update our waypoints and reposition the player to the edge of the left side; if not we continue to move the player left.

The exact same thing is done for the other three sides:

if(keyPressRight == true){
if((this.x+(this.width/2)) >= stage.stageWidth){
updateWaypoints();
this.x = (stage.stageWidth - (this.width/2));
}else{
this.x += speed;
}
}

if(keyPressUp == true){
if((this.y-(this.height/2)) <= 0){
updateWaypoints();
this.y = this.height/2;
}else{
this.y -= speed;
}
}

if(keyPressDown == true){
if((this.y+(this.height/2)) >= stage.stageHeight){
updateWaypoints();
this.y = (stage.stageHeight - (this.height/2));
}else{
this.y += speed;
}
}


And with that we are finished with the Player Class!

## Step 6: Initializing the Ghost

Add the following line inside the Ghost’s constructor:

addEventListener(Event.ADDED_TO_STAGE, init);


Like before create the init() function:

public function init(e:Event){
selectedLife = times.length - 1;
this.x = waypoints[selectedLife][0].x;
this.y = waypoints[selectedLife][0].y;
startTime = flash.utils.getTimer();
}


We start by selecting the path we want to use (by default it will choose the last array); we then position the ghost to the origin and set our Ghost’s start time. Then an event listener for the enterFrame is created.

## Step 7: The Ghost’s enterFrame

Naturally we create our enterFrame function:

public function enterFrame(e:Event){

}


Now we have to loop through our time array. We do this through the variable i; we check if it is less than the length of the array and we also check if the time elapsed is greater than or equal to the current time in the array:

while (i < times[selectedLife].length - 1 && flash.utils.getTimer() - startTime >= times[selectedLife][i]) {
i++;
}


The next thing to do is to move the Ghost if the time elapsed is less than the current time from the array:

if (flash.utils.getTimer() - startTime < times[selectedLife][i]) {
updatePosition();
}


## Step 8: Updating the Ghost’s Position

We’ll start this step off by creating the updatePosition() function:

public function updatePosition(){

}


Next add two variables, to represent the difference and the distance between the old and the new position:

var diff:Point = waypoints[selectedLife][i].subtract(new Point(this.x, this.y));
var dist = diff.length;


We subtract the points from each other to find the distance. Now, we must move the ghost:

if (dist <= speed){
this.x = waypoints[selectedLife][i].x;
this.y = waypoints[selectedLife][i].y;
}else{
diff.normalize(1);
this.x += diff.x * speed;
this.y += diff.y * speed;
}


First we check whether the distance is less than the speed (i.e. the distance the ghost moves each tick); if so we move the Ghost directly to the point. However, if the distance is less then we normalize the difference (“means making its magnitude be equal to 1, while still preserving the direction and sense of the vector” – Euclidean Vectors in Flash), and we increase the Ghost’s position along the direction of the point.

## Step 9: A Side Note

Something to note about this method is that it uses a lot of CPU resources to continuously load times and points, and at times can produce some lag even though the logic is correct. We found two ways of countering this, though!

The first is setting your SWF to be uncompressed in the Publish Settings; this will result in a longer load time at start up however the performance will be smoother. The second is more preferable if you plan on compiling your project as an exe for offline use: simply increase the frame rate to something around 60.

## Conclusion:

Thank you for taking the time to read this tutorial! If you have any questions or comment please leave them below. And if you want an extra challenge try setting up the Ghost class to follow the paths in reverse, or in slow motion.

• Margalus

Great tutorial, i like the concept and how it was developed!

• marc

I enjoyed reading this as well. Do you have any ideas as to how we can workaround condition # 1? Specifically, that the ghost and player have the same point of origin.

Nice spending a worthwhile hour with you…

• Tyler Seitz

If I understand what what you’re saying you would like the ghost to have a different point of origin.

For this to happen you shouldn’t initialize everything the same within the Ghost’s init() function, except for the x & y coordinates and the enterFrame() function . Set the coordinates to your desired position.

Then within the enterFrame() have a condition for when i is equivalent to zero and not at the first way point, the ghost will make its way there while all the values remain unset. Once it reaches the first waypoint initialize the time, selected life and go about the moving the same way.

Good luck, I hope this helps!

• Patrik

Hello.
Thanks for the article. I’m wondering about the effects of using a uncompressed swf-file.

“The first is setting your SWF to be uncompressed in the Publish Settings; this will result in a longer load time at start up however the performance will be smoother.”

If possible, please explain what you mean with a smoother performance and how it works.

Thanks alot.
Patrik