388 lines
18 KiB
HTML
388 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<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">
|
|
<option>1</option>
|
|
<option>2</option>
|
|
<option selected>3</option>
|
|
<option>4</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="roadWidth">Road Width (<span id="currentRoadWidth"></span>) :</label></th>
|
|
<td><input id="roadWidth" type='range' min='500' max='3000' title="integer (500-3000)"></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="cameraHeight">CameraHeight (<span id="currentCameraHeight"></span>) :</label></th>
|
|
<td><input id="cameraHeight" type='range' min='500' max='5000' title="integer (500-5000)"></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="drawDistance">Draw Distance (<span id="currentDrawDistance"></span>) :</label></th>
|
|
<td><input id="drawDistance" type='range' min='100' max='500' title="integer (100-500)"></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="fieldOfView">Field of View (<span id="currentFieldOfView"></span>) :</label></th>
|
|
<td><input id="fieldOfView" type='range' min='80' max='140' title="integer (80-140)"></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="fogDensity">Fog Density (<span id="currentFogDensity"></span>) :</label></th>
|
|
<td><input id="fogDensity" type='range' min='0' max='50' title="integer (0-50)"></td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div id='instructions'>
|
|
<p>Use the <b>arrow keys</b> to drive the car.</p>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
var fps = 60; // how many 'update' frames per second
|
|
var step = 1/fps; // how long is each frame (in seconds)
|
|
var width = 1024; // logical canvas width
|
|
var height = 768; // logical canvas height
|
|
var centrifugal = 0.3; // centrifugal force multiplier when going around curves
|
|
var offRoadDecel = 0.99; // speed multiplier when off road (e.g. you lose 2% speed each update frame)
|
|
var skySpeed = 0.001; // background sky layer scroll speed when going around curve (or up hill)
|
|
var hillSpeed = 0.002; // background hill layer scroll speed when going around curve (or up hill)
|
|
var treeSpeed = 0.003; // background tree layer scroll speed when going around curve (or up hill)
|
|
var skyOffset = 0; // current sky scroll offset
|
|
var hillOffset = 0; // current hill scroll offset
|
|
var treeOffset = 0; // current tree scroll offset
|
|
var segments = []; // array of road segments
|
|
var stats = Game.stats('fps'); // mr.doobs FPS counter
|
|
var canvas = Dom.get('canvas'); // our canvas...
|
|
var ctx = canvas.getContext('2d'); // ...and its drawing context
|
|
var background = null; // our background image (loaded below)
|
|
var sprites = null; // our spritesheet (loaded below)
|
|
var resolution = null; // scaling factor to provide resolution independence (computed)
|
|
var roadWidth = 2000; // actually half the roads width, easier math if the road spans from -roadWidth to +roadWidth
|
|
var segmentLength = 200; // length of a single segment
|
|
var rumbleLength = 3; // number of segments per red/white rumble strip
|
|
var trackLength = null; // z length of entire track (computed)
|
|
var lanes = 3; // number of lanes
|
|
var fieldOfView = 100; // angle (degrees) for field of view
|
|
var cameraHeight = 1000; // z height of camera
|
|
var cameraDepth = null; // z distance camera is from screen (computed)
|
|
var drawDistance = 300; // number of segments to draw
|
|
var playerX = 0; // player x offset from center of road (-1 to 1 to stay independent of roadWidth)
|
|
var playerZ = null; // player relative z distance from camera (computed)
|
|
var fogDensity = 5; // exponential fog density
|
|
var position = 0; // current camera Z position (add playerZ to get player's absolute Z position)
|
|
var speed = 0; // current speed
|
|
var maxSpeed = segmentLength/step; // top speed (ensure we can't move more than 1 segment in a single frame to make collision detection easier)
|
|
var accel = maxSpeed/5; // acceleration rate - tuned until it 'felt' right
|
|
var breaking = -maxSpeed; // deceleration rate when braking
|
|
var decel = -maxSpeed/5; // 'natural' deceleration rate when neither accelerating, nor braking
|
|
var offRoadDecel = -maxSpeed/2; // off road deceleration is somewhere in between
|
|
var offRoadLimit = maxSpeed/4; // limit when off road deceleration no longer applies (e.g. you can always go at least this speed even when off road)
|
|
|
|
var keyLeft = false;
|
|
var keyRight = false;
|
|
var keyFaster = false;
|
|
var keySlower = false;
|
|
|
|
//=========================================================================
|
|
// UPDATE THE GAME WORLD
|
|
//=========================================================================
|
|
|
|
function update(dt) {
|
|
|
|
var playerSegment = findSegment(position+playerZ);
|
|
var speedPercent = speed/maxSpeed;
|
|
var dx = dt * 2 * speedPercent; // at top speed, should be able to cross from left to right (-1 to +1) in 1 second
|
|
|
|
position = Util.increase(position, dt * speed, trackLength);
|
|
|
|
skyOffset = Util.increase(skyOffset, skySpeed * playerSegment.curve * speedPercent, 1);
|
|
hillOffset = Util.increase(hillOffset, hillSpeed * playerSegment.curve * speedPercent, 1);
|
|
treeOffset = Util.increase(treeOffset, treeSpeed * playerSegment.curve * speedPercent, 1);
|
|
|
|
if (keyLeft)
|
|
playerX = playerX - dx;
|
|
else if (keyRight)
|
|
playerX = playerX + dx;
|
|
|
|
playerX = playerX - (dx * speedPercent * playerSegment.curve * centrifugal);
|
|
|
|
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 basePercent = Util.percentRemaining(position, segmentLength);
|
|
var maxy = height;
|
|
|
|
var x = 0;
|
|
var dx = - (baseSegment.curve * basePercent);
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
Render.background(ctx, background, width, height, BACKGROUND.SKY, skyOffset);
|
|
Render.background(ctx, background, width, height, BACKGROUND.HILLS, hillOffset);
|
|
Render.background(ctx, background, width, height, BACKGROUND.TREES, treeOffset);
|
|
|
|
var n, segment;
|
|
|
|
for(n = 0 ; n < drawDistance ; n++) {
|
|
|
|
segment = segments[(baseSegment.index + n) % segments.length];
|
|
segment.looped = segment.index < baseSegment.index;
|
|
segment.fog = Util.exponentialFog(n/drawDistance, fogDensity);
|
|
|
|
Util.project(segment.p1, (playerX * roadWidth) - x, cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
|
|
Util.project(segment.p2, (playerX * roadWidth) - x - dx, cameraHeight, position - (segment.looped ? trackLength : 0), cameraDepth, width, height, roadWidth);
|
|
|
|
x = x + dx;
|
|
dx = dx + segment.curve;
|
|
|
|
if ((segment.p1.camera.z <= cameraDepth) || // behind us
|
|
(segment.p2.screen.y >= maxy)) // clip by (already rendered) segment
|
|
continue;
|
|
|
|
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;
|
|
}
|
|
|
|
Render.player(ctx, width, height, resolution, roadWidth, sprites, speed/maxSpeed,
|
|
cameraDepth/playerZ,
|
|
width/2,
|
|
height,
|
|
speed * (keyLeft ? -1 : keyRight ? 1 : 0),
|
|
0);
|
|
}
|
|
|
|
//=========================================================================
|
|
// 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
|
|
});
|
|
}
|
|
|
|
function addRoad(enter, hold, leave, curve) {
|
|
var n;
|
|
for(n = 0 ; n < enter ; n++)
|
|
addSegment(Util.easeIn(0, curve, n/enter));
|
|
for(n = 0 ; n < hold ; n++)
|
|
addSegment(curve);
|
|
for(n = 0 ; n < leave ; n++)
|
|
addSegment(Util.easeInOut(curve, 0, n/leave));
|
|
}
|
|
|
|
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 addCurve(num, curve) {
|
|
num = num || ROAD.LENGTH.MEDIUM;
|
|
curve = curve || ROAD.CURVE.MEDIUM;
|
|
addRoad(num, num, num, curve);
|
|
}
|
|
|
|
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;
|
|
|
|
trackLength = segments.length * segmentLength;
|
|
}
|
|
|
|
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; } },
|
|
{ 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();
|
|
}
|
|
});
|
|
|
|
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();
|
|
|
|
if ((segments.length==0) || (options.segmentLength) || (options.rumbleLength))
|
|
resetRoad(); // only rebuild road when necessary
|
|
}
|
|
|
|
//=========================================================================
|
|
// TWEAK UI HANDLERS
|
|
//=========================================================================
|
|
|
|
Dom.on('resolution', 'change', function(ev) {
|
|
var w, h, ratio;
|
|
switch(ev.target.options[ev.target.selectedIndex].value) {
|
|
case 'fine': w = 1280; h = 960; ratio=w/width; break;
|
|
case 'high': w = 1024; h = 768; ratio=w/width; break;
|
|
case 'medium': w = 640; h = 480; ratio=w/width; break;
|
|
case 'low': w = 480; h = 360; ratio=w/width; break;
|
|
}
|
|
reset({ width: w, height: h })
|
|
Dom.blur(ev);
|
|
});
|
|
|
|
Dom.on('lanes', 'change', function(ev) { Dom.blur(ev); reset({ lanes: ev.target.options[ev.target.selectedIndex].value }); });
|
|
Dom.on('roadWidth', 'change', function(ev) { Dom.blur(ev); reset({ roadWidth: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
|
|
Dom.on('cameraHeight', 'change', function(ev) { Dom.blur(ev); reset({ cameraHeight: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
|
|
Dom.on('drawDistance', 'change', function(ev) { Dom.blur(ev); reset({ drawDistance: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
|
|
Dom.on('fieldOfView', 'change', function(ev) { Dom.blur(ev); reset({ fieldOfView: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
|
|
Dom.on('fogDensity', 'change', function(ev) { Dom.blur(ev); reset({ fogDensity: Util.limit(Util.toInt(ev.target.value), Util.toInt(ev.target.getAttribute('min')), Util.toInt(ev.target.getAttribute('max'))) }); });
|
|
|
|
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>
|
|
|