Car Racing Game Project in Js
Car Racing Game Project in Js
Submitted by
J.BHAVANA
(Reg. No:
212000320)
Bonafide Certificate
In 1895 the first true race was held, from Paris to Bordeaux, France, and back, a distance of
1,178 km. The winner made an average speed of 24.15 kph. Organized automobile racing
began in the United States with an 87-km race from Chicago to Evanston, Illinois, and back
on Thanksgiving Day in 1895.
This game consists of three major modules. First part is the Game Logic Generator which
calculate the logic of this game, such as to detect bumps to obstacle, speed control based
on keyboard input, opponents control and road generation, and this module is based on
software. The second part is the Screen Rendering Module, we adopt the Sprite Graphics
technique to decompose the display screen into 7 layers, which will be explained in derails
in later section. The last part is the Audio Module which generate the proper sound under
the control of game logic
I wasn’t really much for going to the arcades when I was younger… I didn’t need ‘em with
awesome C64 games sitting at home… but there were 3 arcade games that could always get my
money - Donkey Kong, Dragons Lair, and Outrun…
… and I really loved Outrun, the speed, the hills, the palm trees and the music - even the lowly
c64 version.
Without the polish, it’s a little ugly, but its fully functional, and I can show you how to
implement it yourself in four easy sections…
Enjoy!
A note on performance
The performance of this game is very machine/browser dependent. It works quite well in
modern browsers, especially those with GPU canvas acceleration, but a bad graphics driver can
kill it stone dead. So your mileage may vary. There are controls provided to change the
rendering resolution and the draw distance to scale to fit your machine.
The current state of mobile browser performance is pretty dismal. Dont expect this to be
playable on any mobile device.
NOTE: I havent actually spent anytime optimizing for performance yet. So it might be possible
to make it play well on older browsers, but that’s not really what this project is about.
This project happens to be implemented in javascript (because its easy for prototyping) but is
not intended to demonstrate javascript techniques or best practices. In fact, in order to keep it
simple to understand it embeds the javascript for each example directly in the HTML page
(horror!) and, even worse, uses global variables and functions (OMG!).
If I was building a real game I would have much more structure and organization to the code
Before we do any of that, lets go off and read Lou’s Pseudo 3d Page - its the main source of
information (that I could find) online about how to build a pseudo-3d racing game.
NOTE: Lou’s page doesn’t render well in google chrome - so its best viewed using Firefox or IE
Finished reading Lou’s article ? Great! We’re going to build a variation on his ‘Realistic Hills
Using 3d-Projected Segments’ approach. We will do it gradually, over the course of the next 4
articles, but we will start off here with v1, building very simple straight road geometry and
projecting it onto our HTML5 canvas element.
Some Trigonometry
Before we get down to the implementation, lets use some basic trigonometry to remind
ourselves how to project a point in a 3D world onto a 2D screen.
At its most basic, without getting into vectors and matrices, 3D projection uses a law of similar
triangles. If we were to label:
h = camera height
d = distance from camera to screen
z = distance from camera to car
y = screen y coordinate
y = h*d/z
x = w*d/z
Where w = half the width of the road (from camera to road edge)
You can see that for both x and y, what we are really doing is scaling by a factor of
d/z
Coordinate Systems
This sounds nice and simple in diagram form, but once you start coding its easy to get a little
confused because we have been a bit loose in naming our variables and its not clear which
represent 3d world coordinates and which represent 2d screen coordinates. We’ve also
assumed that the camera is at the origin of our world when in reality it will be following our
car.
Projection
Instead of hard coding a value for d, its more useful to derive it from the desired vertical field
of view. This way we can choose to ‘zoom’ the camera if needed.
Assuming we are projecting onto a normalized projection plane, with coordinates from -1 to
+1, we can calculate d as follows:
d = 1/tan(fov/2)
Setting up fov as one (of many) variables we will be able to tweak in order to fine tune the
rendering algorithm.
I mentioned in the introduction that this code does not exactly follow javascript best practices
- its a quick and dirty demo with simple global variables and functions. However, since I am
going to build 4 separate versions (straights, curves, hills and sprites) I will keep some re-
usable methods inside common.js within the following modules:
ROAD:
<!DOCTYPE html>
<html>
<head>
<title>Javascript Racer - v1 (straight)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<div id="racer">
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the
<canvas> element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
//=========================================================================
// UPDATE THE GAME WORLD
//=========================================================================
function update(dt) {
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
speed = Util.accelerate(speed, offRoadDecel, dt);
playerX = Util.limit(playerX, -2, 2); // dont ever let player go too far out of
bounds speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
//=========================================================================
// RENDER THE GAME WORLD
//=========================================================================
function render() {
var baseSegment =
findSegment(position); var maxy = height;
segment = segments[(baseSegment.index + n) %
segments.length]; segment.looped = segment.index <
baseSegment.index;
segment.fog = Util.exponentialFog(n/drawDistance, fogDensity);
Render.segment(ctx, width,
lanes,
segment.p1.screen.x,
segment.p1.screen.y,
segment.p1.screen.w,
segment.p2.screen.x,
segment.p2.screen.y,
segment.p2.screen.w,
segment.fog,
segment.color);
maxy = segment.p2.screen.y;
}
function resetRoad() {
segments = [];
for(var n = 0 ; n < 500 ; n++) {
segments.push({
index: n,
p1: { world: { z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
segments[findSegment(playerZ).index + 2].color =
COLORS.START; segments[findSegment(playerZ).index + 3].color
= COLORS.START; for(var n = 0 ; n < rumbleLength ; n++)
segments[segments.length-1-n].color = COLORS.FINISH;
function findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
//=========================================================================
// THE GAME LOOP
//=========================================================================
Game.run({
canvas: canvas, render: render, update: update, stats: stats, step: step,
images: ["background", "sprites"],
keys: [
{ keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
function reset(options) {
options = options ||
{};
canvas.width = width = Util.toInt(options.width, width);
canvas.height = height = Util.toInt(options.height,
height); lanes = Util.toInt(options.lanes, lanes);
roadWidth = Util.toInt(options.roadWidth, roadWidth);
cameraHeight = Util.toInt(options.cameraHeight, cameraHeight);
drawDistance = Util.toInt(options.drawDistance, drawDistance);
fogDensity = Util.toInt(options.fogDensity, fogDensity);
fieldOfView = Util.toInt(options.fieldOfView, fieldOfView);
segmentLength = Util.toInt(options.segmentLength, segmentLength);
rumbleLength = Util.toInt(options.rumbleLength, rumbleLength);
cameraDepth = 1 / Math.tan((fieldOfView/2) * Math.PI/180);
playerZ = (cameraHeight * cameraDepth);
resolution = height/480;
refreshTweakUI();
//=========================================================================
// TWEAK UI HANDLERS
//=========================================================================
function refreshTweakUI() {
Dom.get('lanes').selectedIndex = lanes-1;
Dom.get('currentRoadWidth').innerHTML = Dom.get('roadWidth').value = roadWidth;
Dom.get('currentCameraHeight').innerHTML = Dom.get('cameraHeight').value =
cameraHeight;
Dom.get('currentDrawDistance').innerHTML = Dom.get('drawDistance').value =
drawDistance;
Dom.get('currentFieldOfView').innerHTML = Dom.get('fieldOfView').value = fieldOfView;
//=========================================================================
</script>
</body>
<html>
<head>
<title>Javascript Racer - v2 (curves)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<td>
<select id="lanes">
<div id='instructions'>
<div id="racer">
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the
<canvas> element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
function update(dt) {
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
speed = Util.accelerate(speed, offRoadDecel, dt);
playerX = Util.limit(playerX, -2, 2); // dont ever let player go too far out of
bounds speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
//=========================================================================
// RENDER THE GAME WORLD
//=========================================================================
function render() {
var x = 0;
var dx = - (baseSegment.curve * basePercent);
var n, segment;
segment = segments[(baseSegment.index + n) %
segments.length]; segment.looped = segment.index <
baseSegment.index;
segment.fog = Util.exponentialFog(n/drawDistance, fogDensity);
x = x + dx;
dx = dx + segment.curve;
Render.segment(ctx, width,
lanes,
segment.p1.screen.x,
segment.p1.screen.y,
segment.p1.screen.w,
segment.p2.screen.x,
segment.p2.screen.y,
segment.p2.screen.w,
segment.fog,
segment.color);
maxy = segment.p2.screen.y;
}
//=========================================================================
// BUILD ROAD GEOMETRY
//=========================================================================
function addSegment(curve)
{ var n = segments.length;
segments.push({
index: n,
p1: { world: { z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { z: (n+1)*segmentLength }, camera: {}, screen: {} },
curve: curve,
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
var ROAD = {
LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
};
function addStraight(num) {
num = num || ROAD.LENGTH.MEDIUM;
addRoad(num, num, num, 0);
}
function addSCurves() {
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,
ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,
ROAD.LENGTH.MEDIUM, ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.EASY);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.MEDIUM);
function resetRoad() {
segments = [];
addStraight(ROAD.LENGTH.SHORT/4);
addSCurves();
addStraight(ROAD.LENGTH.LONG);
addCurve(ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM);
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM);
addStraight();
addSCurves();
addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.MEDIUM);
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM);
addStraight();
addSCurves();
addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.EASY);
segments[findSegment(playerZ).index + 2].color =
COLORS.START; segments[findSegment(playerZ).index + 3].color
= COLORS.START; for(var n = 0 ; n < rumbleLength ; n++)
segments[segments.length-1-n].color = COLORS.FINISH;
function findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
//=========================================================================
// THE GAME LOOP
//=========================================================================
Game.run({
canvas: canvas, render: render, update: update, stats: stats, step: step,
images: ["background", "sprites"],
keys: [
function reset(options) {
options = options ||
{};
canvas.width = width = Util.toInt(options.width, width);
canvas.height = height = Util.toInt(options.height,
height); lanes = Util.toInt(options.lanes, lanes);
roadWidth = Util.toInt(options.roadWidth, roadWidth);
cameraHeight = Util.toInt(options.cameraHeight, cameraHeight);
drawDistance = Util.toInt(options.drawDistance, drawDistance);
fogDensity = Util.toInt(options.fogDensity, fogDensity);
fieldOfView = Util.toInt(options.fieldOfView, fieldOfView);
segmentLength = Util.toInt(options.segmentLength, segmentLength);
rumbleLength = Util.toInt(options.rumbleLength, rumbleLength);
cameraDepth = 1 / Math.tan((fieldOfView/2) * Math.PI/180);
playerZ = (cameraHeight * cameraDepth);
resolution = height/480;
refreshTweakUI();
//=========================================================================
function refreshTweakUI() {
Dom.get('lanes').selectedIndex = lanes-1;
Dom.get('currentRoadWidth').innerHTML = Dom.get('roadWidth').value = roadWidth;
Dom.get('currentCameraHeight').innerHTML = Dom.get('cameraHeight').value =
cameraHeight;
//=========================================================================
</script>
</body>
<html>
<head>
<title>Javascript Racer - v3 (hills)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<td>
<select id="lanes">
<div id='instructions'>
<div id="racer">
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the
<canvas> element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
function update(dt) {
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
if (((playerX < -1) || (playerX > 1)) && (speed > offRoadLimit))
speed = Util.accelerate(speed, offRoadDecel, dt);
playerX = Util.limit(playerX, -2, 2); // dont ever let it go too far out of
bounds speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
//=========================================================================
// RENDER THE GAME WORLD
//=========================================================================
function render() {
var x = 0;
var dx = - (baseSegment.curve * basePercent);
var n, segment;
segment = segments[(baseSegment.index + n) %
segments.length]; segment.looped = segment.index <
baseSegment.index;
segment.fog = Util.exponentialFog(n/drawDistance, fogDensity);
x = x + dx;
dx = dx + segment.curve;
Render.segment(ctx, width,
lanes,
segment.p1.screen.x,
segment.p1.screen.y,
segment.p1.screen.w,
segment.p2.screen.x,
segment.p2.screen.y,
segment.p2.screen.w,
segment.fog,
segment.color);
maxy = segment.p2.screen.y;
}
//=========================================================================
// BUILD ROAD GEOMETRY
//=========================================================================
function addSegment(curve, y) {
var n = segments.length;
segments.push({
index: n,
p1: { world: { y: lastY(), z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { y: y, z: (n+1)*segmentLength }, camera: {}, screen: {}
}, curve: curve,
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
var ROAD = {
LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
HILL: { NONE: 0, LOW: 20, MEDIUM: 40, HIGH: 60 },
CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
};
function addStraight(num) {
num = num || ROAD.LENGTH.MEDIUM;
addRoad(num, num, num, 0, 0);
}
function addSCurves() {
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.EASY, ROAD.HILL.NONE);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,
ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM, ROAD.HILL.MEDIUM);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,
ROAD.LENGTH.MEDIUM, ROAD.CURVE.EASY, -ROAD.HILL.LOW);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.EASY, ROAD.HILL.MEDIUM);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.MEDIUM, -ROAD.HILL.MEDIUM);
}
function addDownhillToEnd(num) {
function resetRoad() {
segments = [];
addStraight(ROAD.LENGTH.SHORT/2);
addHill(ROAD.LENGTH.SHORT, ROAD.HILL.LOW);
addLowRollingHills();
addCurve(ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM, ROAD.HILL.LOW);
addLowRollingHills();
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM, ROAD.HILL.MEDIUM);
addStraight();
addCurve(ROAD.LENGTH.LONG, -ROAD.CURVE.MEDIUM,
ROAD.HILL.MEDIUM); addHill(ROAD.LENGTH.LONG, ROAD.HILL.HIGH);
addCurve(ROAD.LENGTH.LONG, ROAD.CURVE.MEDIUM, -ROAD.HILL.LOW);
addHill(ROAD.LENGTH.LONG, -ROAD.HILL.MEDIUM);
addStraight();
addDownhillToEnd();
segments[findSegment(playerZ).index + 2].color =
COLORS.START; segments[findSegment(playerZ).index + 3].color
= COLORS.START; for(var n = 0 ; n < rumbleLength ; n++)
segments[segments.length-1-n].color = COLORS.FINISH;
function findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
//=========================================================================
// THE GAME LOOP
//=========================================================================
Game.run({
function reset(options) {
options = options ||
{};
canvas.width = width = Util.toInt(options.width, width);
canvas.height = height = Util.toInt(options.height,
height); lanes = Util.toInt(options.lanes, lanes);
roadWidth = Util.toInt(options.roadWidth, roadWidth);
cameraHeight = Util.toInt(options.cameraHeight, cameraHeight);
drawDistance = Util.toInt(options.drawDistance, drawDistance);
fogDensity = Util.toInt(options.fogDensity, fogDensity);
fieldOfView = Util.toInt(options.fieldOfView, fieldOfView);
segmentLength = Util.toInt(options.segmentLength, segmentLength);
rumbleLength = Util.toInt(options.rumbleLength, rumbleLength);
cameraDepth = 1 / Math.tan((fieldOfView/2) * Math.PI/180);
playerZ = (cameraHeight * cameraDepth);
resolution = height/480;
refreshTweakUI();
//=========================================================================
// TWEAK UI HANDLERS
//=========================================================================
function refreshTweakUI() {
Dom.get('lanes').selectedIndex = lanes-1;
//=========================================================================
</script>
</body>
<html>
<head>
<title>Javascript Racer - v4 (final)</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="common.css" rel="stylesheet" type="text/css" />
</head>
<body>
<table id="controls">
<tr>
<td colspan="2">
<a href='v1.straight.html'>straight</a> |
<a href='v2.curves.html'>curves</a> |
<a href='v3.hills.html'>hills</a> |
<a href='v4.final.html'>final</a>
</td>
</tr>
<tr><td id="fps" colspan="2" align="right"></td></tr>
<tr>
<th><label for="resolution">Resolution :</label></th>
<td>
<select id="resolution" style="width:100%">
<option value='fine'>Fine (1280x960)</option>
<option selected value='high'>High (1024x768)</option>
<option value='medium'>Medium (640x480)</option>
<option value='low'>Low (480x360)</option>
</select>
</td>
</tr>
<tr>
<th><label for="lanes">Lanes :</label></th>
<td>
<select id="lanes">
<div id='instructions'>
<div id="racer">
<div id="hud">
<span id="speed" class="hud"><span id="speed_value" class="value">0</span>
mph</span>
<span id="current_lap_time" class="hud">Time: <span id="current_lap_time_value"
class="value">0.0</span></span>
<span id="last_lap_time" class="hud">Last Lap: <span id="last_lap_time_value"
class="value">0.0</span></span>
<span id="fast_lap_time" class="hud">Fastest Lap: <span id="fast_lap_time_value"
class="value">0.0</span></span>
</div>
<canvas id="canvas">
Sorry, this example cannot be run because your browser does not support the
<canvas> element
</canvas>
Loading...
</div>
<audio id='music'>
<source src="music/racer.ogg">
<source src="music/racer.mp3">
</audio>
<span id="mute"></span>
<script src="stats.js"></script>
<script src="common.js"></script>
<script>
var hud = {
speed: { value: null, dom: Dom.get('speed_value') },
current_lap_time: { value: null, dom: Dom.get('current_lap_time_value')
}, last_lap_time: { value: null, dom: Dom.get('last_lap_time_value') },
fast_lap_time: { value: null, dom: Dom.get('fast_lap_time_value') }
}
//=========================================================================
// UPDATE THE GAME WORLD
//=========================================================================
function update(dt) {
updateCars(dt, playerSegment,
playerW);
if (keyLeft)
playerX = playerX - dx;
else if (keyRight)
playerX = playerX + dx;
if (keyFaster)
speed = Util.accelerate(speed, accel, dt);
else if (keySlower)
speed = Util.accelerate(speed, breaking, dt);
else
speed = Util.accelerate(speed, decel, dt);
playerX = Util.limit(playerX, -3, 3); // dont ever let it go too far out of
bounds speed = Util.limit(speed, 0, maxSpeed); // or exceed maxSpeed
updateHud('speed', 5 * Math.round(speed/500));
updateHud('current_lap_time', formatTime(currentLapTime));
}
//
// optimization, dont bother steering around other cars when 'out of sight' of the
player if ((carSegment.index - playerSegment.index) > drawDistance)
return 0;
// if no cars ahead, but I have somehow ended up off road, then steer back
on if (car.offset < -0.9)
return 0.1;
else if (car.offset > 0.9)
return -0.1;
else
return 0;
}
function updateHud(key, value) { // accessing DOM can be slow, so only do it if value has
changed
if (hud[key].value !== value) {
hud[key].value = value;
Dom.set(hud[key].dom, value);
}
}
function formatTime(dt) {
var minutes = Math.floor(dt/60);
var seconds = Math.floor(dt - (minutes * 60));
var tenths = Math.floor(10 * (dt - Math.floor(dt)));
if (minutes > 0)
return minutes + "." + (seconds < 10 ? "0" : "") + seconds + "." +
tenths; else
return seconds + "." + tenths;
}
//=========================================================================
// RENDER THE GAME WORLD
//=========================================================================
function render() {
var x = 0;
var dx = - (baseSegment.curve * basePercent);
segment = segments[(baseSegment.index + n) %
segments.length]; segment.looped = segment.index <
baseSegment.index;
segment.fog = Util.exponentialFog(n/drawDistance,
fogDensity); segment.clip = maxy;
x = x + dx;
dx = dx + segment.curve;
Render.segment(ctx, width,
lanes,
segment.p1.screen.x,
segment.p1.screen.y,
segment.p1.screen.w,
segment.p2.screen.x,
segment.p2.screen.y,
segment.p2.screen.w,
segment.fog,
maxy = segment.p1.screen.y;
}
if (segment == playerSegment) {
Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
cameraDepth/playerZ,
width/2,
(height/2) - (cameraDepth/playerZ * Util.interpolate(playerSegment.p1.camera.y,
playerSegment.p2.camera.y, playerPercent) * height/2),
speed * (keyLeft ? -1 : keyRight ? 1 : 0),
playerSegment.p2.world.y - playerSegment.p1.world.y);
}
function findSegment(z) {
return segments[Math.floor(z/segmentLength) % segments.length];
}
//=========================================================================
// BUILD ROAD GEOMETRY
//=========================================================================
function addSegment(curve, y) {
var n = segments.length;
segments.push({
index: n,
p1: { world: { y: lastY(), z: n *segmentLength }, camera: {}, screen: {} },
p2: { world: { y: y, z: (n+1)*segmentLength }, camera: {}, screen: {}
}, curve: curve,
sprites: [],
cars: [],
color: Math.floor(n/rumbleLength)%2 ? COLORS.DARK : COLORS.LIGHT
});
}
var ROAD = {
LENGTH: { NONE: 0, SHORT: 25, MEDIUM: 50, LONG: 100 },
HILL: { NONE: 0, LOW: 20, MEDIUM: 40, HIGH: 60 },
CURVE: { NONE: 0, EASY: 2, MEDIUM: 4, HARD: 6 }
};
function addStraight(num) {
num = num || ROAD.LENGTH.MEDIUM;
addRoad(num, num, num, 0, 0);
}
function addSCurves() {
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.EASY, ROAD.HILL.NONE);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,
ROAD.LENGTH.MEDIUM, ROAD.CURVE.MEDIUM, ROAD.HILL.MEDIUM);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM,
ROAD.LENGTH.MEDIUM, ROAD.CURVE.EASY, -ROAD.HILL.LOW);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.EASY, ROAD.HILL.MEDIUM);
addRoad(ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, ROAD.LENGTH.MEDIUM, -
ROAD.CURVE.MEDIUM, -ROAD.HILL.MEDIUM);
}
function addBumps() {
addRoad(10, 10, 10, 0,
5);
addRoad(10, 10, 10, 0, -2);
addRoad(10, 10, 10, 0, -5);
addRoad(10, 10, 10, 0, 8);
addRoad(10, 10, 10, 0, 5);
addRoad(10, 10, 10, 0, -7);
addRoad(10, 10, 10, 0, 5);
addRoad(10, 10, 10, 0, -2);
}
function addDownhillToEnd(num) {
num = num || 200;
addRoad(num, num, num, -ROAD.CURVE.EASY, -lastY()/segmentLength);
}
function resetRoad() {
segments = [];
addStraight(ROAD.LENGTH.SHORT);
addLowRollingHills();
addSCurves();
resetSprites();
resetCars();
segments[findSegment(playerZ).index + 2].color =
COLORS.START; segments[findSegment(playerZ).index + 3].color
= COLORS.START; for(var n = 0 ; n < rumbleLength ; n++)
segments[segments.length-1-n].color = COLORS.FINISH;
function resetSprites() {
var n, i;
function resetCars() {
//=========================================================================
// THE GAME LOOP
//=========================================================================
Game.run({
canvas: canvas, render: render, update: update, stats: stats, step: step,
images: ["background", "sprites"],
keys: [
{ keys: [KEY.LEFT, KEY.A], mode: 'down', action: function() { keyLeft = true; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'down', action: function() { keyRight = true; } },
{ keys: [KEY.UP, KEY.W], mode: 'down', action: function() { keyFaster = true; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'down', action: function() { keySlower = true; } },
{ keys: [KEY.LEFT, KEY.A], mode: 'up', action: function() { keyLeft = false; } },
{ keys: [KEY.RIGHT, KEY.D], mode: 'up', action: function() { keyRight = false; } },
{ keys: [KEY.UP, KEY.W], mode: 'up', action: function() { keyFaster = false; } },
{ keys: [KEY.DOWN, KEY.S], mode: 'up', action: function() { keySlower = false; } }
],
ready: function(images) {
background =
images[0]; sprites
= images[1];
reset();
Dom.storage.fast_lap_time = Dom.storage.fast_lap_time || 180;
updateHud('fast_lap_time', formatTime(Util.toFloat(Dom.storage.fast_lap_time)));
}
function reset(options) {
options = options ||
{};
canvas.width = width = Util.toInt(options.width, width);
canvas.height = height = Util.toInt(options.height,
height); lanes = Util.toInt(options.lanes, lanes);
roadWidth = Util.toInt(options.roadWidth, roadWidth);
cameraHeight = Util.toInt(options.cameraHeight, cameraHeight);
drawDistance = Util.toInt(options.drawDistance, drawDistance);
fogDensity = Util.toInt(options.fogDensity, fogDensity);
fieldOfView = Util.toInt(options.fieldOfView, fieldOfView);
segmentLength = Util.toInt(options.segmentLength, segmentLength);
rumbleLength = Util.toInt(options.rumbleLength, rumbleLength);
cameraDepth = 1 / Math.tan((fieldOfView/2) * Math.PI/180);
playerZ = (cameraHeight * cameraDepth);
resolution = height/480;
refreshTweakUI();
//=========================================================================
// TWEAK UI HANDLERS
//=========================================================================
function refreshTweakUI() {
Dom.get('lanes').selectedIndex = lanes-1;
Dom.get('currentRoadWidth').innerHTML = Dom.get('roadWidth').value = roadWidth;
Dom.get('currentCameraHeight').innerHTML = Dom.get('cameraHeight').value =
cameraHeight;
Dom.get('currentDrawDistance').innerHTML = Dom.get('drawDistance').value =
drawDistance;
Dom.get('currentFieldOfView').innerHTML = Dom.get('fieldOfView').value = fieldOfView;
Dom.get('currentFogDensity').innerHTML = Dom.get('fogDensity').value = fogDensity;
}
//=========================================================================
</script>
</body>
</html>
Phew! That was a long last lap, but there you have it, our final version has
entered that stage where it can legitimately be called a game. It’s still very
far from a finished game, but it’s a game nonetheless.
It’s quite astounding what it takes to actually finish a game, even a simple
one. And this is not a project that I plan on polishing into a finished state. It
should really just be considered how to get started with a pseudo-3d racing
game.
The code is available on github for anyone who wants to try to turn this into a
more polished racing game. You might want to consider:
car sound fx
better synchronized music
full screen mode
HUD fx (flash on fastest lap, confetti, color coded speedometer, etc)
more accurate sprite collision
better car AI (steering, braking etc)
an actual crash when colliding at high speed
more bounce when car is off road
screen shake when off-road or collision
throw up dirt particles when off road
more dynamic camera (lower at faster speed, swoop over hills etc)
So there you have it. Another ‘weekend’ project that took a lot longer than
expected but, I think, turned out pretty well in the end. These written up
articles maybe had a little too much low level detail, but hopefully they still
made some sense.
(below) Enjoy!