Now that you are familiar with the basics of drawing and animating shapes, we can finally start working on the lunar lander. We begin with creating the spaceship, as it is the main element of the game.
Before delving into the graphics, it is necessary to have a mechanism for tracking the state of the spaceship at any given point in time. To do this, we declare an object spaceship
with the following properties:
var spaceship =
{
color: "black",
width: 8,
height: 22,
position:
{
x: 0,
y: 0
},
angle: 0,
engineOn: false,
rotatingLeft: false,
rotatingRight: false
}
As we are just beginning game development, we will keep the main body of the spaceship simple to draw. In fact, it will just be a rectangle with the dimensions specified in the object. However, it is not as simple as writing a single draw command, because we must deal with the spaceship facing various angles at various positions.
In a canvas, shapes are rotated by transforming the context before the shape is drawn. For example, a square can be rotated 45° as such:
context.rotate(Math.PI / 4);
context.rect(0, 0, 50, 50);
context.fill();
Notice that angle parameter for the rotate function must be specified in radians. Furthermore, if you were to test out the above code, you would see that the entire canvas rotates about the origin point in the top left corner. This means the square is starting to rotate out of view.
Rotations can be positioned better by:
Our rotated square can be drawn in the center of the canvas like this:
context.translate(canvas.width / 2, canvas.height / 2); // translate context to center of canvas
context.rotate(Math.PI / 4); // rotate by 45°
context.rect(-25, -25, 50, 50); // center square at origin by setting x and y to -1/2 of sides
context.fill();
If you are worried about what happens after transformations have been made to a context, you are thinking like a programmer! Unfortunately, what happens is that the context remains transformed and further drawing will conform to the transformation. Luckily, there is a way to restore the context to the original state. This is done by calling context.save()
before the transformations and calling context.restore()
once the drawing is complete.
So the safe way to draw a centered square is:
context.save()
context.translate(canvas.width / 2, canvas.height / 2);
context.rotate(Math.PI / 4);
context.rect(-25, -25, 50, 50);
context.fill();
context.restore();
We now know everything we need to draw the spaceship at the desired position with the desired rotation.
context.save();
context.translate(spaceship.position.x, spaceship.position.y);
context.rotate(spaceship.angle);
context.rect(spaceship.width * -0.5, spaceship.height * -0.5, spaceship.width, spaceship.height);
context.fillStyle = spaceship.color
context.fill()
context.restore()
Wrap this code in a drawSpaceship()
function and add it to our JavaScript file:
var canvas = document.getElementById("game");
var context = canvas.getContext("2d");
var spaceship =
{
color: "black",
width: 8,
height: 22,
position:
{
x: 0,
y: 0
},
angle: 0,
engineOn: false,
rotatingLeft: false,
rotatingRight: false
}
function drawSpaceship()
{
context.save();
context.beginPath();
context.translate(spaceship.position.x, spaceship.position.y);
context.rotate(spaceship.angle);
context.rect(spaceship.width * -0.5, spaceship.height * -0.5, spaceship.width, spaceship.height);
context.fillStyle = spaceship.color;
context.fill();
context.closePath();
context.restore();
}
drawSpaceship();
Let's animate an exhaust flame at the bottom of the spaceship for the user whenever the engine is on. The flame will just be an inverted triangle, and to mimic a flame, we'll vary height of the triangle randomly.
Recall the method to drawing polygons from Lesson 1: Drawing Shapes. We'll draw the triangle at the bottom of the spaceship in the same way, inserting the code into the existing drawSpaceship()
function:
function drawSpaceship()
{
context.save();
context.beginPath();
context.translate(spaceship.position.x, spaceship.position.y);
context.rotate(spaceship.angle);
context.rect(spaceship.width * -0.5, spaceship.height * -0.5, spaceship.width, spaceship.height);
context.fillStyle = spaceship.color;
context.fill();
context.closePath();
// Draw the flame if engine is on
if(spaceship.engineOn)
{
context.beginPath();
context.moveTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.lineTo(spaceship.width * 0.5, spaceship.height * 0.5);
context.lineTo(0, spaceship.height * 0.5 + Math.random() * 5);
context.lineTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.closePath();
context.fillStyle = "orange";
context.fill();
}
context.restore();
}
The flame is drawn by:
Y
from the center of the bottom of the spaceship.
Y
to vary randomly between 0 and 5 pixels, so we multiply the result of a call to JavaScript's Math.random()
library function (returns a random floating point number between 0 and 1) by 5.You may have noticed that drawSpaceship()
only gets called once, so the exhuast flame does not actually animate at this point. Recall the method to animating drawings in canvas
from Lesson 2: Animating Shapes. We will now write the draw()
function that gets called every animation frame. This function will contain a call to our existing drawSpaceship()
function:
function draw()
{
// Clear entire screen
context.clearRect(0, 0, canvas.width, canvas.height);
// Begin drawing
drawSpaceship();
/* other draw methods (to add later) */
requestAnimationFrame(draw);
}
Our JavaScript file:
var canvas = document.getElementById("game");
var context = canvas.getContext("2d");
var spaceship =
{
color: "black",
width: 8,
height: 22,
position:
{
x: 0,
y: 0
},
angle: 0,
engineOn: false,
rotatingLeft: false,
rotatingRight: false
}
function drawSpaceship()
{
context.save();
context.beginPath();
context.translate(spaceship.position.x, spaceship.position.y);
context.rotate(spaceship.angle);
context.rect(spaceship.width * -0.5, spaceship.height * -0.5, spaceship.width, spaceship.height);
context.fillStyle = spaceship.color;
context.fill();
context.closePath();
// Draw the flame if engine is on
if(spaceship.engineOn)
{
context.beginPath();
context.moveTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.lineTo(spaceship.width * 0.5, spaceship.height * 0.5);
context.lineTo(0, spaceship.height * 0.5 + Math.random() * 5);
context.lineTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.closePath();
context.fillStyle = "orange";
context.fill();
}
context.restore();
}
function draw()
{
// Clear entire screen
context.clearRect(0, 0, canvas.width, canvas.height);
// Begin drawing
drawSpaceship();
/* other draw methods (to add later) */
requestAnimationFrame(draw);
}
draw();
Currently, our spaceship is only a static drawing in the canvas, so it is time to add user interactivity. The spaceship will be controlled by the Left, Right and Up Arrow Keys.
In JavaScript, a keystroke is regarded as an event, specifically a 'keydown'
event, so the way to detect keystrokes is to listen for this event. We do this by calling the document.addEventListener()
function, which takes two parameters:
'keydown'
in our casefunction keyPressed(event)
{
// implement this later
}
document.addEventListener('keydown', keyPressed);
We now have a function keyPressed()
that gets called every time the user presses a key, but how do we distinguish between different keys being pressed? The answer lies in the event
parameter. event
has a property keyCode
that encodes which key was pressed. You don't have to worry about character encoding, because all you need to know is that the Left, Right and Up Arrow Keys have codes 37
, 39
and 38
, respectively. Knowing the codes, we can use a switch statement to identify the key and update the properties in spaceship
.
function keyPressed(event)
{
switch(event.keyCode)
{
case 37:
// Left Arrow key
spaceship.rotatingLeft = true;
break;
case 39:
// Right Arrow key
spaceship.rotatingRight = true;
break;
case 38:
// Up Arrow key
spaceship.engineOn = true;
break;
}
}
We now run into a problem: the properties of spaceship
get updated properly when an arrow key is pressed, but how about when the key is let go?
The answer is they don't, because currently, the properties remain true
indefinitely. The solution to this problem is listening for another event provided by JavaScript: 'keyup'
, which fires when the user has let go of a key:
function keyLetGo(event)
{
switch(event.keyCode)
{
case 37:
// Left Arrow key
spaceship.rotatingLeft = false;
break;
case 39:
// Right Arrow key
spaceship.rotatingRight = false;
break;
case 38:
// Up Arrow key
spaceship.engineOn = false;
break;
}
}
document.addEventListener('keyup', keyLetGo);
The above code should be fairly easy to follow, as it is nearly identical to the previous code, except for the properties being set to false
.
Your JavaScript file should now look like:
var canvas = document.getElementById("game");
var context = canvas.getContext("2d");
var spaceship =
{
color: "black",
width: 8,
height: 22,
position:
{
x: 0,
y: 0
},
angle: 0,
engineOn: false,
rotatingLeft: false,
rotatingRight: false
}
function drawSpaceship()
{
context.save();
context.beginPath();
context.translate(spaceship.position.x, spaceship.position.y);
context.rotate(spaceship.angle);
context.rect(spaceship.width * -0.5, spaceship.height * -0.5, spaceship.width, spaceship.height);
context.fillStyle = spaceship.color;
context.fill();
context.closePath();
// Draw the flame if engine is on
if(spaceship.engineOn)
{
context.beginPath();
context.moveTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.lineTo(spaceship.width * 0.5, spaceship.height * 0.5);
context.lineTo(0, spaceship.height * 0.5 + Math.random() * 5);
context.lineTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.closePath();
context.fillStyle = "orange";
context.fill();
}
context.restore();
}
function draw()
{
// Clear entire screen
context.clearRect(0, 0, canvas.width, canvas.height);
// Begin drawing
drawSpaceship();
/* other draw methods (to add later) */
requestAnimationFrame(draw);
}
function keyLetGo(event)
{
switch(event.keyCode)
{
case 37:
// Left Arrow key
spaceship.rotatingLeft = false;
break;
case 39:
// Right Arrow key
spaceship.rotatingRight = false;
break;
case 38:
// Up Arrow key
spaceship.engineOn = false;
break;
}
}
document.addEventListener('keyup', keyLetGo);
function keyPressed(event)
{
switch(event.keyCode)
{
case 37:
// Left Arrow key
spaceship.rotatingLeft = true;
break;
case 39:
// Right Arrow key
spaceship.rotatingRight = true;
break;
case 38:
// Up Arrow key
spaceship.engineOn = true;
break;
}
}
document.addEventListener('keydown', keyPressed);
draw();
We now have event listeners that update the state of spaceship
, but the spaceship in our canvas does not update to reflect these state changes. This is because we have yet to implement the math that changes the angle
and position
properties of spaceship
accordingly.
To do this, we revisit our code for drawing the spaceship. The only place where we can regularly perform calculations is in the draw()
function, so we insert a call to a new function updateSpaceship()
inside draw()
.
function updateSpaceship()
{
// implement this later
}
function draw()
{
// Clear entire screen
context.clearRect(0, 0, canvas.width, canvas.height);
updateSpaceship();
// Begin drawing
drawSpaceship();
/* other draw methods (to add later) */
requestAnimationFrame(draw);
}
Inside updateSpaceship()
, we need to update spaceship
's angle
and position
properties.
The math for updating angle
is quite straightforward, because it will either increase or decrease depending on the states of rotatingLeft
and rotatingRight
:
function updateSpaceship()
{
if(spaceship.rotatingRight)
{
spaceship.angle += Math.PI / 180;
}
else if(spaceship.rotatingLeft)
{
spaceship.angle -= Math.PI / 180;
}
}
The angle
property on spaceship
now increments or decrements by an arbitrary value of 1° = pi / 180 rad depending on the rotatingRight
and rotatingLeft
properties.
The tricky part of updateSpaceship()
is updating the position
property depending on the engineOn
property. We cannot simply increment position
's x
and y
properties, because that would not account for the spaceship's angle. Therefore, we must use trigonometry to separate the displacement of the spaceship into x and y components as such:
function updateSpaceship()
{
if(spaceship.rotatingRight)
{
spaceship.angle += Math.PI / 180;
}
else if(spaceship.rotatingLeft)
{
spaceship.angle -= Math.PI / 180;
}
if(spaceship.engineOn)
{
spaceship.position.x += Math.sin(spaceship.angle);
spaceship.position.y -= Math.cos(spaceship.angle);
}
}
The x-component of the displacement is calculated with Math.sin()
and incremented onto the x
property of spaceship
's position
. The y-component of the displacement is calcualted with Math.cos()
but decremented instead of incremented onto the spaceship.position.y
because the y-axis of the canvas is flipped.
Congratulations, you have succesfully written a functional interactive spaceship in HTML5. Play around with your game! The code you wrote for the model, drawing, user interaction and animation of the spaceship will serve as the foundation for the next sections.
Your final JavaScript file should look like:
var canvas = document.getElementById("game");
var context = canvas.getContext("2d");
var spaceship =
{
color: "black",
width: 8,
height: 22,
position:
{
x: 20,
y: 20
},
velocity:
{
x: 0,
y: 0
},
angle: Math.PI / 2,
engineOn: false,
rotatingLeft: false,
rotatingRight: false,
crashed: false
}
function drawSpaceship()
{
context.save();
context.beginPath();
context.translate(spaceship.position.x, spaceship.position.y);
context.rotate(spaceship.angle);
context.rect(spaceship.width * -0.5, spaceship.height * -0.5, spaceship.width, spaceship.height);
context.fillStyle = spaceship.color;
context.fill();
context.closePath();
// Draw the flame if engine is on
if(spaceship.engineOn)
{
context.beginPath();
context.moveTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.lineTo(spaceship.width * 0.5, spaceship.height * 0.5);
context.lineTo(0, spaceship.height * 0.5 + Math.random() * 10);
context.lineTo(spaceship.width * -0.5, spaceship.height * 0.5);
context.closePath();
context.fillStyle = "orange";
context.fill();
}
context.restore();
}
function updateSpaceship()
{
if(spaceship.rotatingRight)
{
spaceship.angle += Math.PI / 180;
}
else if(spaceship.rotatingLeft)
{
spaceship.angle -= Math.PI / 180;
}
if(spaceship.engineOn)
{
spaceship.position.x += Math.sin(spaceship.angle);
spaceship.position.y -= Math.cos(spaceship.angle);
}
}
function draw()
{
// Clear entire screen
context.clearRect(0, 0, canvas.width, canvas.height);
updateSpaceship();
// Begin drawing
drawSpaceship();
/* other draw methods (to add later) */
requestAnimationFrame(draw);
}
function keyLetGo(event)
{
console.log(spaceship);
switch(event.keyCode)
{
case 37:
// Left Arrow key
spaceship.rotatingLeft = false;
break;
case 39:
// Right Arrow key
spaceship.rotatingRight = false;
break;
case 38:
// Up Arrow key
spaceship.engineOn = false;
break;
}
}
document.addEventListener('keyup', keyLetGo);
function keyPressed(event)
{
console.log(spaceship);
switch(event.keyCode)
{
case 37:
// Left Arrow key
spaceship.rotatingLeft = true;
break;
case 39:
// Right Arrow key
spaceship.rotatingRight = true;
break;
case 38:
// Up Arrow key
spaceship.engineOn = true;
break;
}
}
document.addEventListener('keydown', keyPressed);
draw();