The JavaScript code below implements a simple 3D aerial combat (“dogfight”) game. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.
Use the W, A, S, D keys to change direction, and the and the up and down arrow keys to accelerate and decelerate. The object is to catch the green ship and avoid the red ship.
<html> <body> <script type="text/javascript"> // main function main() { var meshForMovers = new Mesh ( // vertices [ new Coords(-5, 5, 0), new Coords(5, 5, 0), new Coords(2, 0, 0), new Coords(-2, 0, 0), new Coords(0, 0, 20), ], // faces [ new MeshFace([ 0, 1, 4 ]) , new MeshFace([ 1, 2, 4 ]), new MeshFace([ 2, 3, 4 ]), new MeshFace([ 3, 0, 4 ]), new MeshFace([ 0, 1, 2, 3 ]), ] ); var venueSizeInPixels = new Coords(1, 1, 1).multiplyScalar(1000000); var moverForPlayer = new Mover ( "Player", 1, // integrity 1.2, // speedMax 0.1, // accelPerTick 0.05, // turnPerTick new Location ( new Coords(-100, 0, 0), Orientation.fromForwardAndDown ( new Coords(1, 0, 0), new Coords(0, 0, 1) ) ), new Activity_UserInputAccept(), "Gray", // color meshForMovers ); Globals.Instance.initialize ( 20, // framesPerSecond new Display(new Coords(300, 300, 1)), new Venue ( "Venue0", venueSizeInPixels, // movers [ moverForPlayer, new Mover ( "Prey", 1, // integrity 1, // speedMax 0.1, // accelPerTick 0.05, // turnPerTick new Location ( new Coords(100, 0, 0), Orientation.fromForwardAndDown ( new Coords(1, 0, 0), new Coords(0, 0, 1) ) ), new Activity_MoveRandomly(), "Green", // color meshForMovers ), new Mover ( "Predator", 1, // integrity 1, // speedMax 0.1, // accelPerTick 0.05, // turnPerTick new Location ( new Coords(0, 0, 0), Orientation.fromForwardAndDown ( new Coords(1, 0, 0), new Coords(0, 0, 1) ) ), new Activity_MoveTowardTargetPos ( moverForPlayer.loc.pos ), "Red", // color meshForMovers ), ] ) ); } // classes function Action() { // abstract class } { // instances Action.Instances = new Action_Instances(); function Action_Instances() { this.Accelerate = new Action_Accelerate(1); this.Decelerate = new Action_Accelerate(-1); this.PitchDown = new Action_TurnAxisTowardOtherInDirection(0, 2, 1); this.PitchUp = new Action_TurnAxisTowardOtherInDirection(0, 2, -1); this.RollLeft = new Action_TurnAxisTowardOtherInDirection(2, 1, 1); this.RollRight = new Action_TurnAxisTowardOtherInDirection(2, 1, -1); this.YawLeft = new Action_TurnAxisTowardOtherInDirection(0, 1, -1); this.YawRight = new Action_TurnAxisTowardOtherInDirection(0, 1, 1); } } function Action_Accelerate(sign) { this.sign = sign; } { Action_Accelerate.prototype.performForActor = function(actor) { var actorLoc = actor.loc; actorLoc.accel.overwriteWith ( actorLoc.orientation.forward ).multiplyScalar ( this.sign * actor.accelPerTick ); } } function Action_TurnAxisTowardOtherInDirection(indexOfAxisToBeTurned, indexOfAxisToTurnToward, sign) { this.indexOfAxisToBeTurned = indexOfAxisToBeTurned; this.indexOfAxisToTurnToward = indexOfAxisToTurnToward; this.sign = sign; this.temp = new Coords(); } { Action_TurnAxisTowardOtherInDirection.prototype.performForActor = function(actor) { var actorLoc = actor.loc; var actorOrientation = actorLoc.orientation; var axes = actorOrientation.axes; var axisToBeTurned = axes[this.indexOfAxisToBeTurned]; var axisToTurnToward = axes[this.indexOfAxisToTurnToward]; axisToBeTurned.add ( this.temp.overwriteWith ( axisToTurnToward ).multiplyScalar ( this.sign * actor.turnPerTick ) ).normalize(); actorOrientation.orthogonalizeAxes().normalizeAxes(); } } function Activity_DoNothing() { // do nothing } { Activity_DoNothing.prototype.performForActor = function(actor) { // do nothing } } function Activity_MoveRandomly() { this.ticksToHoldCourse = 0; this.targetPos = new Coords(); this.directionToTarget = new Coords(); this.ticksToHoldCourseMax = 200; } { Activity_MoveRandomly.prototype.performForActor = function(actor) { var actorLoc = actor.loc; var actorOrientation = actorLoc.orientation; this.ticksToHoldCourse--; if (this.ticksToHoldCourse <= 0) { var venue = Globals.Instance.venue; this.targetPos.randomize().multiply ( venue.sizeInPixels ).subtract ( venue.sizeInPixelsHalf ); this.ticksToHoldCourse = Math.floor ( Math.random() * this.ticksToHoldCourseMax ); } // hack - Instant turning. actorOrientation.forward.overwriteWith ( this.targetPos ).subtract ( actorLoc.pos ).normalize(); actorOrientation.orthogonalizeAxes(); Action.Instances.Accelerate.performForActor(actor); } } function Activity_MoveTowardTargetPos(targetPos) { this.targetPos = targetPos; } { Activity_MoveTowardTargetPos.prototype.performForActor = function(actor) { var actorLoc = actor.loc; var actorOrientation = actorLoc.orientation; // hack - Instant turning. actorOrientation.forward.overwriteWith ( this.targetPos ).subtract ( actorLoc.pos ).normalize(); actorOrientation.orthogonalizeAxes(); Action.Instances.Accelerate.performForActor(actor); } } function Activity_UserInputAccept() { // do nothing } { Activity_UserInputAccept.prototype.performForActor = function(actor) { var keyCodesPressed = Globals.Instance.inputHelper.keyCodesPressed; var actions = Action.Instances; for (var i = 0; i < keyCodesPressed.length; i++) { var keyCodePressed = keyCodesPressed[i]; var actionToPerform = null; if (keyCodePressed == "_38") // up arrow { actionToPerform = actions.Accelerate; } if (keyCodePressed == "_40") // down arrow { actionToPerform = actions.Decelerate; } else if (keyCodePressed == "_65") // a { actionToPerform = actions.YawLeft; } else if (keyCodePressed == "_68") // d { actionToPerform = actions.YawRight; } else if (keyCodePressed == "_69") // e { actionToPerform = actions.RollRight; } else if (keyCodePressed == "_81") // q { actionToPerform = actions.RollLeft; } else if (keyCodePressed == "_83") // s { actionToPerform = actions.PitchUp; } else if (keyCodePressed == "_87") // w { actionToPerform = actions.PitchDown; } if (actionToPerform != null) { actionToPerform.performForActor(actor); } } } } function Camera(focalLength, loc) { this.focalLength = focalLength; this.loc = loc; } function CompassPoint(name, direction) { this.name = name; this.direction = direction; this.loc = new Location(new Coords()); } { CompassPoint.prototype.updateForVenueTimerTick = function(venue) { this.loc.pos.overwriteWith ( this.direction ).multiplyScalar ( 100 ).add ( venue.moverForPlayer.loc.pos ); } } function Constants() { // static class } { Constants.RadiansPerCycle = Math.PI * 2.0; } function Coords(x, y, z) { this.x = x; this.y = y; this.z = z; } { Coords.prototype.add = function(other) { this.x += other.x; this.y += other.y; this.z += other.z; return this; } Coords.prototype.clear = function() { this.x = 0; this.y = 0; this.z = 0; return this; } Coords.prototype.clone = function() { return new Coords(this.x, this.y, this.z); } Coords.prototype.crossProduct = function(other) { return this.overwriteWithXYZ ( this.y * other.z - this.z * other.y, this.z * other.x - this.x * other.z, this.x * other.y - this.y * other.x ); } Coords.prototype.divide = function(other) { this.x /= other.x; this.y /= other.y; this.z /= other.z; return this; } Coords.prototype.divideScalar = function(scalar) { this.x /= scalar; this.y /= scalar; this.z /= scalar; return this; } Coords.prototype.dotProduct = function(other) { return this.x * other.x + this.y * other.y + this.z * other.z; } Coords.prototype.isInRangeMax = function(max) { returnValue = ( this.x >= 0 && this.x <= max.x && this.y >= 0 && this.y <= max.y && this.z >= 0 && this.z <= max.z ); return returnValue; } Coords.prototype.isInRangeMaxXY = function(max) { returnValue = ( this.x >= 0 && this.x <= max.x && this.y >= 0 && this.y <= max.y ); return returnValue; } Coords.prototype.magnitude = function() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } Coords.prototype.multiply = function(other) { this.x *= other.x; this.y *= other.y; this.z *= other.z; return this; } Coords.prototype.multiplyScalar = function(scalar) { this.x *= scalar; this.y *= scalar; this.z *= scalar; return this; } Coords.prototype.normalize = function() { return this.divideScalar(this.magnitude()); } Coords.prototype.overwriteWith = function(other) { this.x = other.x; this.y = other.y; this.z = other.z; return this; } Coords.prototype.overwriteWithXYZ = function(x, y, z) { this.x = x; this.y = y; this.z = z; return this; } Coords.prototype.randomize = function() { this.x = Math.random(); this.y = Math.random(); this.z = Math.random(); return this; } Coords.prototype.subtract = function(other) { this.x -= other.x; this.y -= other.y; this.z -= other.z; return this; } Coords.prototype.toString = function() { return "(" + this.x + "," + this.y + "," + this.z + ")" } Coords.prototype.trimToMagnitudeMax = function(magnitudeMax) { var magnitude = this.magnitude(); if (magnitude > magnitudeMax) { this.divideScalar ( magnitude ).multiplyScalar ( magnitudeMax ); } return this; } Coords.prototype.trimToRangeMinMax = function(min, max) { if (this.x < min.x) { this.x = min.x; } else if (this.x > max.x) { this.x = max.x; } if (this.y < min.y) { this.y = min.y; } else if (this.y > max.y) { this.y = max.y; } if (this.z < min.z) { this.z = min.z; } else if (this.z > max.z) { this.z = max.z; } return this; } } function Display(sizeInPixels) { this.sizeInPixels = sizeInPixels; this.sizeInPixelsHalf = sizeInPixels.clone().divideScalar(2); // temporary variables this.drawPos = new Coords(); this.transformOrient = new Transform_Orient(); } { Display.prototype.clear = function() { this.graphics.fillStyle = "Black"; this.graphics.fillRect ( 0, 0, this.sizeInPixels.x, this.sizeInPixels.y ); this.graphics.strokeStyle = "Gray"; this.graphics.strokeRect ( 0, 0, this.sizeInPixels.x, this.sizeInPixels.y ); } Display.prototype.drawCompassPointForCamera = function(compassPoint, camera) { this.graphics.strokeStyle = "Gray"; this.graphics.fillStyle = "Gray"; var drawPos = this.drawPos; this.transformWorldPosToViewPos ( drawPos.overwriteWith(compassPoint.loc.pos), camera ); if (drawPos.isInRangeMaxXY(this.sizeInPixels) == true) { this.drawTextAtLocationForCamera ( compassPoint.name, compassPoint.loc, camera ); } } Display.prototype.drawMeshAtLocationForCamera = function(mesh, loc, camera) { var drawPos = this.drawPos; var vertices = mesh.vertices; var faces = mesh.faces; this.transformOrient.orientation = loc.orientation; var meshPos = loc.pos; for (var f = 0; f < faces.length; f++) { var face = faces[f]; this.graphics.beginPath(); for (var vi = 0; vi < face.vertexIndices.length; vi++) { var vertexIndex = face.vertexIndices[vi]; var vertex = vertices[vertexIndex]; drawPos.overwriteWith ( vertex ); this.transformOrient.applyToCoords ( drawPos ); drawPos.add ( meshPos ); this.transformWorldPosToViewPos ( drawPos, camera ); if (drawPos.z < 0) { break; } else if (vi == 0) { this.graphics.moveTo(drawPos.x, drawPos.y); } else { this.graphics.lineTo(drawPos.x, drawPos.y); } } this.graphics.closePath(); this.graphics.stroke(); } } Display.prototype.drawMoverForCamera = function(mover, camera) { this.graphics.strokeStyle = mover.color; var drawPos = this.drawPos; this.transformWorldPosToViewPos ( drawPos.overwriteWith(mover.loc.pos), camera ); var moverDistanceFromPlayer = Math.round(mover.displacementFromPlayer.magnitude()); var moverLabel = mover.name + "\n" + moverDistanceFromPlayer; if (drawPos.z > 0 && drawPos.isInRangeMaxXY(this.sizeInPixels) == true) { this.drawTextAtPos(moverLabel, drawPos); this.drawMeshAtLocationForCamera(mover.mesh, mover.loc, camera); } else { drawPos.z = 0; drawPos.subtract ( this.sizeInPixelsHalf ).trimToMagnitudeMax ( this.sizeInPixelsHalf.x ).add ( this.sizeInPixelsHalf ); this.drawTextAtPos(moverLabel, drawPos); this.graphics.beginPath(); this.graphics.arc ( drawPos.x, drawPos.y, 5, // radius 0, Constants.RadiansPerCycle ); this.graphics.stroke(); } } Display.prototype.drawTextAtLocationForCamera = function(textToDraw, loc, camera) { var drawPos = this.drawPos; drawPos.overwriteWith ( loc.pos ); this.transformWorldPosToViewPos ( drawPos, camera ); if (drawPos.z > 0) { this.drawTextAtPos(textToDraw, drawPos); } } Display.prototype.drawTextAtPos = function(textToDraw, drawPos) { var textToDrawAsLines = textToDraw.split("\n"); for (var i = 0; i < textToDrawAsLines.length; i++) { var lineToDraw = textToDrawAsLines[i]; this.graphics.fillText(lineToDraw, drawPos.x, drawPos.y); this.graphics.strokeText(lineToDraw, drawPos.x, drawPos.y); drawPos.y += 10; // hack } } Display.prototype.drawVenue = function(venue) { this.clear(); var camera = venue.camera; var movers = venue.movers; var compassPoints = venue.compassPoints; for (var i = 1; i < movers.length; i++) { var mover = movers[i]; this.drawMoverForCamera(mover, camera); } for (var i = 0; i < compassPoints.length; i++) { var compassPoint = compassPoints[i]; this.drawCompassPointForCamera(compassPoint, camera); } this.drawTextAtPos("Time:" + Globals.Instance.secondsSoFar() + "s", new Coords(10, 10)); this.drawTextAtPos("Kills:" + venue.killsSoFar, new Coords(10, 20)); this.drawTextAtPos("Deaths:" + venue.deathsSoFar, new Coords(10, 30)); } Display.prototype.initialize = function() { var canvas = document.createElement("canvas"); canvas.width = this.sizeInPixels.x; canvas.height = this.sizeInPixels.y; this.graphics = canvas.getContext("2d"); document.body.appendChild(canvas); } Display.prototype.transformWorldPosToViewPos = function(drawPos, camera) { var cameraLoc = camera.loc; var cameraOrientation = cameraLoc.orientation; drawPos.subtract ( cameraLoc.pos ).overwriteWithXYZ ( drawPos.dotProduct(cameraOrientation.right), drawPos.dotProduct(cameraOrientation.down), drawPos.dotProduct(cameraOrientation.forward) ) var distanceAlongCameraForward = drawPos.z; drawPos.multiplyScalar ( camera.focalLength ).divideScalar ( distanceAlongCameraForward ).add ( this.sizeInPixelsHalf ); drawPos.z = distanceAlongCameraForward; } } function Globals() { // do nothing } { // instance Globals.Instance = new Globals(); // methods Globals.prototype.initialize = function(timerTicksPerSecond, display, venue) { this.display = display; this.venue = venue; this.inputHelper = new InputHelper(); this.display.initialize(); this.timerTicksSoFar = 0; this.timerTicksPerSecond = timerTicksPerSecond; var millisecondsPerTimerTick = Math.round(1000 / this.timerTicksPerSecond); this.timer = setInterval ( this.handleEventTimerTick.bind(this), millisecondsPerTimerTick ); this.inputHelper.initialize(); } Globals.prototype.secondsSoFar = function() { return Math.floor(this.timerTicksSoFar / this.timerTicksPerSecond); } // events Globals.prototype.handleEventTimerTick = function() { this.venue.updateForTimerTick(); this.timerTicksSoFar++; } } function InputHelper() { // do nothing } { InputHelper.prototype.initialize = function() { this.keyCodesPressed = []; document.body.onkeydown = this.handleEventKeyDown.bind(this); document.body.onkeyup = this.handleEventKeyUp.bind(this); } // events InputHelper.prototype.handleEventKeyDown = function(event) { var keyCode = "_" + event.keyCode; if (this.keyCodesPressed[keyCode] == null) { this.keyCodesPressed.push(keyCode); this.keyCodesPressed[keyCode] = keyCode; } } InputHelper.prototype.handleEventKeyUp = function(event) { var keyCode = "_" + event.keyCode; delete this.keyCodesPressed[keyCode]; this.keyCodesPressed.splice ( this.keyCodesPressed.indexOf(keyCode), 1 ); } } function Location(pos, orientation) { this.pos = pos; this.orientation = orientation; this.vel = new Coords(0, 0, 0); this.accel = new Coords(0, 0, 0); } function Plane(normal, distanceFromOrigin) { this.normal = normal; this.distanceFromOrigin = distanceFromOrigin; } function Mesh(vertices, faces) { this.vertices = vertices; this.faces = faces; this.recalculate(); } { Mesh.prototype.recalculate = function() { for (var f = 0; f < this.faces.length; f++) { var face = this.faces[f]; face.recalculateForMesh(this); } } } function MeshFace(vertexIndices) { this.vertexIndices = vertexIndices; this.plane = new Plane(new Coords(), 0); } { MeshFace.prototype.recalculateForMesh = function(mesh) { var vertex0 = mesh.vertices[this.vertexIndices[0]]; var vertex1 = mesh.vertices[this.vertexIndices[1]]; var vertex2 = mesh.vertices[this.vertexIndices[2]]; var edge0 = vertex1.clone().subtract(vertex0); var edge1 = vertex2.clone().subtract(vertex1); this.plane.normal.overwriteWith(edge0).crossProduct(edge1); this.plane.distanceFromOrigin = this.plane.normal.dotProduct(vertex0); } } function Mover ( name, integrity, speedMax, accelPerTick, turnPerTick, loc, activity, color, mesh ) { this.name = name; this.integrity = integrity; this.speedMax = speedMax; this.accelPerTick = accelPerTick; this.turnPerTick = turnPerTick; this.loc = loc; this.activity = activity; this.color = color; this.mesh = mesh; this.displacementFromPlayer = new Coords(); } { Mover.prototype.updateForVenueTimerTick = function(venue) { this.activity.performForActor(this); var loc = this.loc; loc.vel.add(loc.accel); loc.accel.clear(); loc.vel.trimToMagnitudeMax(this.speedMax); loc.pos.add(loc.vel); loc.pos.trimToRangeMinMax ( venue.sizeInPixelsHalfNegative, venue.sizeInPixelsHalf ); var moverForPlayer = venue.moverForPlayer; if (this != moverForPlayer) { this.displacementFromPlayer.overwriteWith ( loc.pos ).subtract ( moverForPlayer.loc.pos ); var distanceFromPlayer = this.displacementFromPlayer.magnitude(); var distanceMinForKill = 10; if (distanceFromPlayer <= distanceMinForKill) { if (this.name == "Prey") { venue.killsSoFar++; } else if (this.name == "Predator") { venue.deathsSoFar++; } loc.pos.randomize().multiply ( venue.sizeInPixels ).subtract ( venue.sizeInPixelsHalf ).divideScalar ( 1000 ).add ( moverForPlayer.loc.pos ); } } } } function Orientation(forward, right, down) { this.forward = forward; this.right = right; this.down = down; this.axes = [ this.forward, this.right, this.down ]; } { // static methods Orientation.fromForwardAndDown = function(forward, down) { return new Orientation ( forward, new Coords(0, 0, 0), down ).orthogonalizeAxes(); } // instance methods Orientation.prototype.normalizeAxes = function() { this.forward.normalize(); this.right.normalize(); this.down.normalize(); return this; } Orientation.prototype.orthogonalizeAxes = function() { this.right.overwriteWith ( this.down ).crossProduct ( this.forward ); this.down.overwriteWith ( this.forward ).crossProduct ( this.right ); this.normalizeAxes(); return this; } Orientation.prototype.overwriteWith = function(other) { this.forward.overwriteWith(other.forward); this.right.overwriteWith(other.right); this.down.overwriteWith(other.down); } Orientation.prototype.toString = function() { var returnValue = this.forward.toString() + "x" + this.right.toString() + "x" + this.down.toString(); return returnValue; } } function Transform_Orient(orientation) { this.orientation = orientation; this.orientationTemp = new Orientation(new Coords(), new Coords(), new Coords()); this.result = new Coords(); } { Transform_Orient.prototype.applyToCoords = function(coordsToTransform) { this.orientationTemp.overwriteWith(this.orientation); this.result.clear().add ( this.orientationTemp.forward.multiplyScalar(coordsToTransform.z) ).add ( this.orientationTemp.right.multiplyScalar(coordsToTransform.x) ).add ( this.orientationTemp.down.multiplyScalar(coordsToTransform.y) ); coordsToTransform.overwriteWith(this.result); } } function Venue(name, sizeInPixels, movers) { this.name = name; this.sizeInPixels = sizeInPixels; this.movers = movers; this.sizeInPixelsHalf = this.sizeInPixels.clone().divideScalar(2); this.sizeInPixelsHalfNegative = this.sizeInPixelsHalf.clone().multiplyScalar(-1); this.moverForPlayer = this.movers[0]; this.camera = new Camera ( 300, // focalLength this.moverForPlayer.loc ); var directionNamesAndOffsets = [ [ "West", new Coords(-1, 0, 0) ], [ "East", new Coords(1, 0, 0) ], [ "North", new Coords(0, -1, 0) ], [ "South", new Coords(0, 1, 0) ], [ "Up", new Coords(0, 0, -1) ], [ "Down", new Coords(0, 0, 1) ], ]; this.compassPoints = []; for (var i = 0; i < directionNamesAndOffsets.length; i++) { var directionNameAndOffset = directionNamesAndOffsets[i]; var directionName = directionNameAndOffset[0]; var directionOffset = directionNameAndOffset[1]; var compassPoint = new CompassPoint(directionName, directionOffset); this.compassPoints.push(compassPoint); } this.killsSoFar = 0; this.deathsSoFar = 0; } { Venue.prototype.updateForTimerTick = function() { for (var i = 0; i < this.movers.length; i++) { var mover = this.movers[i]; mover.updateForVenueTimerTick(this); } for (var i = 0; i < this.compassPoints.length; i++) { var compassPoint = this.compassPoints[i]; compassPoint.updateForVenueTimerTick(this); } Globals.Instance.display.drawVenue(this); } } // run main(); </script> </body> </html>