1. Code
  2. Coding Fundamentals
  3. Game Development

Create a Racing Game Without a 3D Engine

Scroll to top

This tutorial will give you an alternative to 3D for racing games in ActionScript 3. No external framework is required for this old-school style example.


Final Result Preview

Let's take a look at the final result we will be working towards:


Step 1: Set up the FLA Document

Create a new Flash document set for ActionScript 3.0. I'll be using dimensions of 480x320px, a frame rate of 30 FPS, and a light blue background. Save the file with a name of your choice.

Create a .FLA documentCreate a .FLA documentCreate a .FLA document

Step 2: Create a Document Class

Besides the FLA, we also need to create a document class. Create a new Actionscript file, and add this code:

1
2
package 
3
{
4
	import flash.display.MovieClip;
5
    
6
	public class Main extends MovieClip
7
	{
8
		public function Main()
9
		{
10
			
11
		}
12
	}
13
}

Save this file in the same directory as our FLA. Name it Main.as.


Step 3: Link the Main Class with the FLA

In order to compile the code from the Main class, we need to link it with the FLA. On the Properties panel of the FLA, next to Class, enter the name of the document class, in this case, Main.

Link the Main class with the FLA

Then, save the changes on the FLA.


Step 4: Draw a Road Line

We need to start with a line to represent one segment of the road. Press R to select the Rectangle tool. In this example I'm going to make a gray rectangle for the road itself, two small red rectangles at each edge of the gray one, and green rectangles to fill the rest of the line. The green ones must be even wider than the stage, I'm making them 1500 pixels wide. The width of the road may vary to your needs, I'll be using one of 245px wide. It's not necessary for them to be very high, since we'll be using several instances to draw the whole road on the screen. I'll be making them 10px high.

Draw some rectangles next to each other for a road lineDraw some rectangles next to each other for a road lineDraw some rectangles next to each other for a road line

Step 5: Create a MovieClip for the Road Lines

Once you have drawn all the rectangles, select them all (Ctrl + A) and press F8 to create a Movie Clip out of those rectangles you just made. Name it "Road", make sure the the Registration Point is at the center, and select the "Export for ActionScript" checkbox.

Create a MovieClip out of the rectangles you drew.Create a MovieClip out of the rectangles you drew.Create a MovieClip out of the rectangles you drew.

You'll end up with a Road MovieClip in the Library.

Road MovieClip in the Library.Road MovieClip in the Library.Road MovieClip in the Library.

It's up to you if you want to draw each rectangle on different layers. I'm just going to put the gray one on a second layer. If you have any Road instance on the Stage, delete it. We'll be adding the Road MovieClip by code later.


Step 6: Set up the Play Area

Let's get back to the Main class. We're going to use that Road MovieClip to generate the illusion of a racing track.

We're going to determine the depth of the visible road, as well as dimensions of the play area. Also, in our class, all the Road instances that we add to the stage will be accessed from an Array. We'll use another Array (zMap) to determine each line's depth.

In this example, I'll be setting a depth of 150 road lines in a 480x320 play area (it's not necessary to be the same size of the stage, but since that's all there's going to be shown, I'll use those numbers).

1
2
//Depth of the visible road

3
private const roadLines:int = 150;
4
//Dimensions of the play area.

5
private const resX:int = 480;
6
private const resY:int = 320;
7
//Line of the player's car.

8
private const noScaleLine:int = 8;
9
//All the road lines will be accessed from an Array.

10
private var zMap:Array = [];
11
private var lines:Array = [];
12
private var halfWidth:Number;
13
private var lineDepth:int;
14
private const widthStep:Number = 1;

Step 7: Display the Road by Code

We'll be using all the previous variables and constants inside the Main function. We'll be scaling each line according to their corresponding depth.

1
2
public function Main()
3
{
4
    //Populate the zMap with the depth of the road lines

5
    for (var i:int = 0; i < roadLines; i++)
6
    {
7
        zMap.push(1 / (i - resY / 2));
8
    }
9
    //We want the line at the bottom to be in front of the rest,

10
    //so we'll add every line at the same position, bottom first.

11
    lineDepth = numChildren;
12
    for (i = 0; i < roadLines; i++)
13
    {
14
        var line = new Road();
15
        lines.push(line);
16
        addChildAt(line, lineDepth);
17
        line.x = resX / 2;
18
        line.y = resY - i;
19
    }
20
    //Scaling the road lines according to their position

21
    halfWidth = resX / 2;
22
    for (i = 0; i < roadLines; i++)
23
    {
24
        lines[i].scaleX = halfWidth / 60 - 1.2;
25
        halfWidth -= widthStep;
26
    }
27
}

If you Publish (Ctrl + Enter) the document at this point you'll get a view of a straight road.

A straight road drawn by a series of the Road MovieClips we created previously.A straight road drawn by a series of the Road MovieClips we created previously.A straight road drawn by a series of the Road MovieClips we created previously.

You can play around with the scaling calculations to get different results. You might want a wider road or a longer view distance.


Step 8: Make a Second Road Graphic

Right now the road looks so flat that you wouldn't be able to tell if we're moving forward. We need at least two different styles of segment to distinguish how fast or how slow we're moving.

Go to the Library panel and double-click the Road MovieClip to get back to the rectangles you drew. Now press F6 to insert a new Keyframe (if you have more than one layer you may want to insert a new Keyframe on every layer). Now, based on the first frame, you can change the colors of the rectangles or modify their design in some way. I'll be changing their color and adding some lane lines to the second frame.

Change the colors or add lane lines on the second frame.Change the colors or add lane lines on the second frame.Change the colors or add lane lines on the second frame.

Step 9: Keep the Player Line from Scaling

We're going to define a new variable in the Main class to maintain consistency on the player's line (assuming there will be a car in the game, we're going to keep the scaling to 1 on that line)

1
2
private var playerZ:Number;

Next, we'll modify the Main function.


Step 10: Add Alternating Lines to the Road

This variable will be used in the Main function. Now the Road lines will be segmented, some will be displaying the second frame and the rest will be showing the first frame, enhancing the illusion of a racing track.

1
2
public function Main()
3
{
4
    for (var i:int = 0; i < roadLines; i++)
5
    {
6
        zMap.push(1 / (i - resY / 2));
7
    }
8
9
    playerZ = 100 / zMap[noScaleLine];
10
    for (i = 0; i < roadLines; i++)
11
    {
12
        zMap[i] *= playerZ;
13
    }
14
15
    lineDepth = numChildren;
16
    for (i = 0; i < roadLines; i++)
17
    {
18
        var line = new Road();
19
        lines.push(line);
20
        addChildAt(line, lineDepth);
21
        line.x = resX / 2;
22
        line.y = resY - i;
23
    }
24
    
25
    halfWidth = resX / 2;
26
    for (i = 0; i < roadLines; i++)
27
    {
28
        if (zMap[i] % 100 > 50)
29
            lines[i].gotoAndStop(1);
30
        else
31
            lines[i].gotoAndStop(2);
32
        lines[i].scaleX = halfWidth / 60 - 1.2;
33
        halfWidth -= widthStep;
34
    }
35
}

It might not be necessary to multiply by 100 to get the segments correctly, but these are the numbers I'll be using in this example, you're free to modify the numbers to your taste (and if you screw something up, you have this as a reference).

Add alternate lines to the road.Add alternate lines to the road.Add alternate lines to the road.

Step 11: Set up a Speed and an Offset

Let's start making things move. We're going to set a variable for speed. This will indicate the depth we'll advance by frame. I'm going to start at a speed of 20, you may use any number you want.

We also need an indicator for the road segments, which will change according to the speed.

1
2
private var speed:int = 20;
3
private var texOffset:int = 100;

Step 12: Start Moving Forward

Before we can do anything with those variables, we need to import a new Event to this class. We could use either a Timer or an EnterFrame. In this example I'll be using the EnterFrame Event.

1
2
import flash.events.Event;

Next, we're going to cut the last conditional in the Main() function and move it to a new function we're creating. This new function will be triggered by the EnterFrame Event, so we'll get continuous movement on the road. Let's call it race().

1
2
public function Main()
3
{
4
    for (var i:int = 0; i < roadLines; i++)
5
    {
6
        zMap.push(1 / (i - resY / 2));
7
    }
8
9
    playerZ = 100 / zMap[noScaleLine];
10
    for (i = 0; i < roadLines; i++)
11
    {
12
        zMap[i] *= playerZ;
13
    }
14
15
    lineDepth = numChildren;
16
    for (i = 0; i < roadLines; i++)
17
    {
18
        var line = new Road();
19
        lines.push(line);
20
        addChildAt(line, lineDepth);
21
        line.x = resX / 2;
22
        line.y = resY - i;
23
    }
24
    
25
    halfWidth = resX / 2;
26
    for (i = 0; i < roadLines; i++)
27
    {
28
        lines[i].scaleX = halfWidth / 60 - 1.2;
29
        halfWidth -= widthStep;
30
    }
31
    
32
    addEventListener(Event.ENTER_FRAME, race);
33
}

Step 13: Define a Race Function

Now let's bring back the conditional that was cut off to the new function so we get movement. The texOffset will point the position of the road to keep an accurate illusion of movement.

1
2
private function race(event:Event):void
3
{
4
    for (var i:int = 0; i < roadLines; i++)
5
    {
6
        if ((zMap[i] + texOffset) % 100 > 50)
7
            lines[i].gotoAndStop(1);
8
        else
9
            lines[i].gotoAndStop(2);
10
    }
11
    texOffset = texOffset + speed;
12
    while (texOffset >= 100)
13
    {
14
        texOffset -= 100;
15
    }
16
}

If you Publish this now, you should be getting an animated road.


Step 14: Steering

Perpetually straight roads are boring and there are thousands of ways to make a perspective going only forward. Now let's add some new variables to take care of the curves while in motion.

In this example I'll be alternating curves to the right with straight sections. The road ahead will be stored in the nextStretch variable. Also, we'll be moving the lines' x position at the curves.

1
2
private var rx:Number; //Each line's x position

3
private var dx:Number; //Curve amount per segment

4
private var ddx:Number = 0.02; //Curve amount per line

5
private var segmentY:int = roadLines;
6
private var nextStretch = "Straight";

Step 15: Add Curves to the Road

The rx variable will store the x position of each line, so we'll want it to start at the center and take the curves from there. Also, ddx controls the sharpness of the curves. In this example I'll have it at 0.02; you might want to vary its value between curves. This is how the new race() function will look:

1
2
private function race(event:Event):void
3
{
4
    rx = resX / 2;
5
    dx = 0;
6
    for (var i:int = 0; i < roadLines; i++)
7
    {
8
        if ((zMap[i] + texOffset) % 100 > 50)
9
            lines[i].gotoAndStop(1);
10
        else
11
            lines[i].gotoAndStop(2);
12
        lines[i].x = rx;
13
        
14
        if (nextStretch == "Straight")
15
        {
16
            if (i >= segmentY)
17
                dx += ddx;
18
            else
19
                dx -= ddx / 64; //Reverts smoothly from a curve to a straight part.

20
        }
21
        else if (nextStretch == "Curved")
22
        {
23
            if (i <= segmentY)
24
                dx += ddx;
25
            else
26
                dx -= ddx / 64;
27
        }
28
        rx += dx;
29
    }
30
    texOffset = texOffset + speed;
31
    while (texOffset >= 100)
32
    {
33
        texOffset -= 100;
34
    }
35
    segmentY -= 1;
36
    while (segmentY < 0)
37
    {
38
        segmentY += roadLines;
39
        if (nextStretch == "Curved")
40
            nextStretch = "Straight";
41
        else
42
            nextStretch = "Curved";
43
    }
44
}

This time we won't be touching the Main function. If you Publish it now you should be getting something like this:

Curves.Curves.Curves.

You might want to change the Curve value for Left and Right, and change the steering values. At this point you should already be able to add a car to the scene and control the speed manually.


Step 16: Hills, Slopes

Remember the rectangles for the road are more than 1 pixel high? That might help us stretch the road view in case we want hills in our game.

There's a method for making hills that's very similar to making curves. There might be lots of different methods, but this is the one I'll be using here. For simplicity, I'll be recycling as much of the code we already have and just add a few lines for this new effect. As usual, if you don't like the results you may modify the values at will.

We just made variables for the x positions of the road lines, now let's make those for the y positions as well.

1
2
private var ry:Number;
3
private var dy:Number;
4
private var ddy:Number = 0.01; //A little less steep than the curves.

Step 17: Downhill, Uphill

For simplicity, in this example I'm going to use the same straight segments for both a straight an uphill effect, and the curves for both a curve and a downhill effect.

1
2
private function race(event:Event):void
3
{
4
    rx = resX / 2;
5
    ry = resY;
6
    dx = 0;
7
    dy = 0;
8
    for (var i:int = 0; i < roadLines; i++)
9
    {
10
        if ((zMap[i] + texOffset) % 100 > 50)
11
            lines[i].gotoAndStop(1);
12
        else
13
            lines[i].gotoAndStop(2);
14
        lines[i].x = rx;
15
        lines[i].y = ry;
16
        
17
        if (nextStretch == "Straight")
18
        {
19
            if (i >= segmentY)
20
            {
21
                dx += ddx;
22
                dy -= ddy;
23
            }
24
            else
25
            {
26
                dx -= ddx / 64;
27
                dy += ddy;
28
            }
29
        }
30
        else if (nextStretch == "Curved")
31
        {
32
            if (i <= segmentY)
33
            {
34
                dx += ddx;
35
                dy -= ddy;
36
            }
37
            else
38
            {
39
                dx -= ddx / 64;
40
                dy += ddy;
41
            }
42
        }
43
        rx += dx;
44
        ry += dy - 1;
45
    }
46
    texOffset = texOffset + speed;
47
    while (texOffset >= 100)
48
    {
49
        texOffset -= 100;
50
    }
51
    segmentY -= 1;
52
    while (segmentY < 0)
53
    {
54
        segmentY += roadLines;
55
        if (nextStretch == "Curved")
56
            nextStretch = "Straight";
57
        else
58
            nextStretch = "Curved";
59
    }
60
}

In your game you should separate the curves from the hills and make two different algorithms, but this example shows how similar they can be.

Hills.Hills.Hills.

Step 18: Enhance the Aesthetics of the Road

Old-school games couldn't take advantage of Flash, but we can. Something as simple as adding a gradient to the road lines will make a nice difference. If you want to, you may use any filters and textures you like, but in this example I'm just adding some simple gradients, so let's get back to the Road MovieClip.

On frame 1, select the gray rectangle, then go to the Color panel and choose Linear Gradient from the drop-down menu, then choose Reflect color as Flow, so the gradient will continue back and forth from the first to the last color. I'm not telling you to choose the same colors as I do, but I'll be using #666666 and #999999 here. If you need to rotate the gradient, press F to switch to the Gradient Transform Tool, that will let you move, rotate, and resize your gradient. In this case I'm moving the gradient to a quarter of the rectangle, and resizing it to half the size of the rectangle, so the center will be lighter and the edges will be darker. I use a similar size for the green part, so it'll change from dark green (#006600) to light green (#009900) continuously.

Add gradients for a nicer texture.Add gradients for a nicer texture.Add gradients for a nicer texture.

Now go to frame 2 and make new gradients with different colors. For the gray rectangle, I kept the lighter color and only changed the darker color to #777777. On the green part, I changed the size of the gradient to try to avoid a checkerboard look, and the change of colors was very subtle (#007700 and #008800).

A subtle change of colors from frame 1 to frame 2.A subtle change of colors from frame 1 to frame 2.A subtle change of colors from frame 1 to frame 2.

Maybe now you'll want to add a nice background at the horizon, or some graphic for the sky.


Conclusion

Whether you're short on resources for 3D frameworks or you just want to go old-school, now you have a simple example of how to make an illusion of depth for a racing game. Now it's up to you if it'll be a motorcycle Grand Prix, or a street race on a highway full of traffic, or maybe something unrelated to racing.

I hope you've found this tutorial useful. Thanks for reading!

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.