Stage 5

Stage 5 Overview

Now for the finishing touches.

We need to give our game a name and display the player's score.

We also might want to play around with the various variables involved such as the rate of gravity, the frame rate, the size of the holes, etc.

Lastly we'll add some color to make sure the different elements (the flappy square, the walls, and the "Game Over" text) are distinctly visible and separated.

Overview
 

Lesson: Text In The Game

We've used text in the past to show the "Game Over" message.

We'll also want to use text to display the name of the game and a score counter.

The canvas provides a number of methods and attributes that can affect the text. You can change the font, color, size, boldness, italics, etc. You can align it left, center, or right, or any number of vertical alignments.

You can also rotate text by rotating the canvas underneath, which is what we are doing in this example, creating a shell-like pattern from the text. We're also using context.measureText() to determine the width of the text and figure out where to start the next text.

Editor (write code below)
var canvas = document.getElementById('flappy_square_stage5_lesson1'); var context = canvas.getContext('2d'); function spiralText(size) { var text = 'spiraling around'; context.font = size + 'px serif'; context.textBaseline = 'middle'; context.fillText(text, 0, 0); return context.measureText(text); } var size = 42; var textArea; context.translate(size/2, size/2); textArea = spiralText(size); while (size > 0) { context.translate(textArea.width, size/2); context.rotate(90 * (Math.PI/180)); size -= 2; textArea = spiralText(size); }
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

Challenge 1

Challenge1example
Challenge 1 Sample Solution

Let's give our game a name. We've used "Flappy Square" in the example solution, but you can name it however you want.

You can use any font style or size that you want. The example provided shows the "serif" font at 15px.

Play around with the different alignments until your happy with the placement. Here is the reference for horizontal alignment (context.textAlign) and Vertical alignment (context.textBaseline).

Remember the text you draw for the game's name will be erased in each frame when we call context.clearRect(x, y, width, height) so you might want to draw the game's name in a function that you cal from programSteps() so that the title is drawn in each frame after you clear the area outside of the game area.

We've removed all of the template code for these challenges so you should just copy in your code from the previous challenge and work from there. You'll need to make all of the code design decisions yourself.

In the end your flappy square should look like the example provided at the right.

Previous Challenge: View your code from Stage 4 Challenge 3 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 4 Challenge 3
Stage 4 Challenge 3
Editor (write code below)
var canvas = document.getElementById('flappy_square_stage5_challenge1'); var context = canvas.getContext('2d');
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage5_challenge1'); var context = canvas.getContext('2d'); var interval; var distance = 0; var gravity = 0.5; var boundary = { minX: 25, minY: 25, width: 425, height: 275 }; var square = { x: 25, y: 75, size: 20, yVelocity: 0, jump: -8 }; var wall = { width: 50, minHole: 75, maxHole: 150, positions: [] }; function drawBoundary() { context.strokeRect(0, 0, boundary.width, boundary.height); } function drawSquare() { context.fillRect(square.x, square.y, square.size, square.size); } function drawWall(wallInfo) { var x = wallInfo.x - distance; context.fillRect( x, 0, wall.width, wallInfo.topHeight ); context.fillRect( x, boundary.height - wallInfo.bottomHeight, wall.width, wallInfo.bottomHeight ); } function random(min, max) { return (min + Math.floor(Math.random() * (max - min))); } function drawWalls() { var wallX = square.x; if (wall.positions.length > 0) { wallX = wall.positions[wall.positions.length - 1].x; } var end = boundary.width + distance + wall.width; while (wallX < end) { wallX += random(100, 200); var topHeight = random(25, 150); var bottomHeight = random(25, 150); if (boundary.height - topHeight - bottomHeight < wall.minHole) { bottomHeight = boundary.height - topHeight - wall.minHole; } if (boundary.height - topHeight - bottomHeight > wall.maxHole) { bottomHeight = boundary.height - topHeight - wall.maxHole; } wall.positions.push({ x: wallX, topHeight: topHeight, bottomHeight: bottomHeight }); } for (var i=0; i < wall.positions.length; ++i) { drawWall(wall.positions[i]); } } function flap() { square.yVelocity = square.jump; } function adjustPosition() { distance += 2; square.yVelocity += gravity; square.y += square.yVelocity; } function clearBoundary() { var maxX = boundary.minX + boundary.width; var maxY = boundary.minY + boundary.height; context.clearRect(0, 0, canvas.width, boundary.minY); context.clearRect(maxX, 0, canvas.width - maxX, canvas.height); context.clearRect(0, maxY, canvas.width, canvas.height - maxY); context.clearRect(0, 0, boundary.minX, canvas.height); } function checkBoundary() { if (square.y >= boundary.height) { endGame(); } } function checkWalls() { var left = square.x; var right = square.x + square.size; var top = square.y; var bottom = square.y + square.size; for (var i=0; i < wall.positions.length; ++i) { var wallInfo = wall.positions[i]; var wallLeft = wallInfo.x - distance; var wallRight = wallLeft + wall.width; if (wallLeft > right) continue; if (wallRight < left) continue; var topWallBottom = wallInfo.topHeight; var bottomWallTop = boundary.height - wallInfo.bottomHeight; if (top < topWallBottom || bottom > bottomWallTop) { endGame(); } } } function drawTitle() { context.save(); context.font = "15px serif"; context.textAlign = 'left'; context.textBaseline = 'bottom'; var x = boundary.minX; var y = boundary.minY; context.fillText('Flappy Square', x, y); context.restore(); } function endGame() { context.font = "20px serif"; context.textAlign = 'center'; var xCenter = boundary.width / 2; var yCenter = boundary.height / 2; context.fillText('Game Over', xCenter, yCenter); pauseAnimation(); } function programSteps() { context.clearRect(0, 0, canvas.width, canvas.height); adjustPosition(); context.save(); context.translate(boundary.minX, boundary.minY); drawBoundary(); drawSquare(); drawWalls(); checkWalls(); checkBoundary(); context.restore(); clearBoundary(); drawTitle(); } function runProgram() { interval = setInterval(programSteps, 80); } canvas.addEventListener('click', flap); // The following code is provided for you. // It creates an eventListener that listens // for the canvas to come into "focus", which // happens when you click on it. // This allows us to stop and start each individual // animation on this whole page separately. function startAnimation() { runProgram(); } function pauseAnimation() { clearInterval(interval); } canvas.addEventListener('focus', startAnimation); canvas.addEventListener('blur', pauseAnimation); canvas.focus();
 

Challenge 2

Challenge2example
Challenge 2 Sample Solution

Ok, so now that we've got some practice work with text under our belts let's try something a bit more tricky: displaying the score.

We'll display our score counter, something like "Score: 30" in the top right corner. With each frame we'll update the score.

The score itself is arbitrary, but since the goal of the game is simply to stay alive as long as possible then the score should reflect that.

In the example to the right we're using a score that increments every 10 frames or roughly once per second (every 800 miliseconds). In the next lesson we can tweak all of these variable to get the game play just right.

Previous Challenge: View your code from Stage 5 Challenge 1 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 5 Challenge 1
Stage 5 Challenge 1
Editor (write code below)
var canvas = document.getElementById('flappy_square_stage5_challenge2'); var context = canvas.getContext('2d');
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage5_challenge2'); var context = canvas.getContext('2d'); var interval; var distance = 0; var gravity = 0.5; var boundary = { minX: 25, minY: 25, width: 425, height: 275 }; var square = { x: 25, y: 75, size: 20, yVelocity: 0, jump: -8 }; var wall = { width: 50, minHole: 75, maxHole: 150, positions: [] }; function drawBoundary() { context.strokeRect(0, 0, boundary.width, boundary.height); } function drawSquare() { context.fillRect(square.x, square.y, square.size, square.size); } function drawWall(wallInfo) { var x = wallInfo.x - distance; context.fillRect( x, 0, wall.width, wallInfo.topHeight ); context.fillRect( x, boundary.height - wallInfo.bottomHeight, wall.width, wallInfo.bottomHeight ); } function random(min, max) { return (min + Math.floor(Math.random() * (max - min))); } function drawWalls() { var wallX = square.x; if (wall.positions.length > 0) { wallX = wall.positions[wall.positions.length - 1].x; } var end = boundary.width + distance + wall.width; while (wallX < end) { wallX += random(100, 200); var topHeight = random(25, 150); var bottomHeight = random(25, 150); if (boundary.height - topHeight - bottomHeight < wall.minHole) { bottomHeight = boundary.height - topHeight - wall.minHole; } if (boundary.height - topHeight - bottomHeight > wall.maxHole) { bottomHeight = boundary.height - topHeight - wall.maxHole; } wall.positions.push({ x: wallX, topHeight: topHeight, bottomHeight: bottomHeight }); } for (var i=0; i < wall.positions.length; ++i) { drawWall(wall.positions[i]); } } function flap() { square.yVelocity = square.jump; } function adjustPosition() { distance += 2; square.yVelocity += gravity; square.y += square.yVelocity; } function clearBoundary() { var maxX = boundary.minX + boundary.width; var maxY = boundary.minY + boundary.height; context.clearRect(0, 0, canvas.width, boundary.minY); context.clearRect(maxX, 0, canvas.width - maxX, canvas.height); context.clearRect(0, maxY, canvas.width, canvas.height - maxY); context.clearRect(0, 0, boundary.minX, canvas.height); } function checkBoundary() { if (square.y >= boundary.height) { endGame(); } } function checkWalls() { var left = square.x; var right = square.x + square.size; var top = square.y; var bottom = square.y + square.size; for (var i=0; i < wall.positions.length; ++i) { var wallInfo = wall.positions[i]; var wallLeft = wallInfo.x - distance; var wallRight = wallLeft + wall.width; if (wallLeft > right) continue; if (wallRight < left) continue; var topWallBottom = wallInfo.topHeight; var bottomWallTop = boundary.height - wallInfo.bottomHeight; if (top < topWallBottom || bottom > bottomWallTop) { endGame(); } } } function drawTitle() { context.save(); context.font = "15px serif"; context.textAlign = 'left'; context.textBaseline = 'bottom'; var x = boundary.minX; var y = boundary.minY; context.fillText('Flappy Square', x, y); context.restore(); } function drawScore() { context.save(); context.font = "15px serif"; context.textAlign = 'right'; context.textBaseline = 'bottom'; var x = boundary.minX + boundary.width; var y = boundary.minY; context.fillText('Score: ' + Math.floor(distance / 20), x, y); context.restore(); } function endGame() { context.font = "20px serif"; context.textAlign = 'center'; var xCenter = boundary.width / 2; var yCenter = boundary.height / 2; context.fillText('Game Over', xCenter, yCenter); pauseAnimation(); } function programSteps() { context.clearRect(0, 0, canvas.width, canvas.height); adjustPosition(); context.save(); context.translate(boundary.minX, boundary.minY); drawBoundary(); drawSquare(); drawWalls(); checkWalls(); checkBoundary(); context.restore(); clearBoundary(); drawTitle(); drawScore(); } function runProgram() { interval = setInterval(programSteps, 80); } canvas.addEventListener('click', flap); // The following code is provided for you. // It creates an eventListener that listens // for the canvas to come into "focus", which // happens when you click on it. // This allows us to stop and start each individual // animation on this whole page separately. function startAnimation() { runProgram(); } function pauseAnimation() { clearInterval(interval); } canvas.addEventListener('focus', startAnimation); canvas.addEventListener('blur', pauseAnimation); canvas.focus();
 

Challenge 3

Challenge3example
Challenge 3 Sample Solution

Well our game is almost done, but the gameplay could probably use a little improving.

There's no right or wrong answer to this but you can play around a number of variables to adjust the difficulty of the game.

  • How strong gravity is.
  • How many frames are drawn per second.
  • The size of the flappy square.
  • The size of the holes in each wall.
  • The width of each wall.
  • The possible distance between walls.
  • The rate at which the score goes up.

Try playing around with each of these factors until the game feels appropriately challenging.

In the example solution we've provided we've increased the frame rate to redraw every 50 miliseconds, we've shrunk the flappy bird down to 15x15 pixels, we've increased gravity to 1.0 and the jump velocity to 10. Lastly we've shrunk the holes down so that the smallest hole is 70 pixels and the largest hole is 120 pixels.

Previous Challenge: View your code from Stage 5 Challenge 2 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 5 Challenge 2
Stage 5 Challenge 2
Editor (write code below)
var canvas = document.getElementById('flappy_square_stage5_challenge3'); var context = canvas.getContext('2d');
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage5_challenge3'); var context = canvas.getContext('2d'); var interval; var distance = 0; var gravity = 1; var boundary = { minX: 25, minY: 25, width: 425, height: 275 }; var square = { x: 25, y: 75, size: 15, yVelocity: 0, jump: -10 }; var wall = { width: 50, minHole: 70, maxHole: 120, positions: [] }; function drawBoundary() { context.strokeRect(0, 0, boundary.width, boundary.height); } function drawSquare() { context.fillRect(square.x, square.y, square.size, square.size); } function drawWall(wallInfo) { var x = wallInfo.x - distance; context.fillRect( x, 0, wall.width, wallInfo.topHeight ); context.fillRect( x, boundary.height - wallInfo.bottomHeight, wall.width, wallInfo.bottomHeight ); } function random(min, max) { return (min + Math.floor(Math.random() * (max - min))); } function drawWalls() { var wallX = square.x; if (wall.positions.length > 0) { wallX = wall.positions[wall.positions.length - 1].x; } var end = boundary.width + distance + wall.width; while (wallX < end) { wallX += random(100, 200); var topHeight = random(25, 150); var bottomHeight = random(25, 150); if (boundary.height - topHeight - bottomHeight < wall.minHole) { bottomHeight = boundary.height - topHeight - wall.minHole; } if (boundary.height - topHeight - bottomHeight > wall.maxHole) { bottomHeight = boundary.height - topHeight - wall.maxHole; } wall.positions.push({ x: wallX, topHeight: topHeight, bottomHeight: bottomHeight }); } for (var i=0; i < wall.positions.length; ++i) { drawWall(wall.positions[i]); } } function flap() { square.yVelocity = square.jump; } function adjustPosition() { distance += 2; square.yVelocity += gravity; square.y += square.yVelocity; } function clearBoundary() { var maxX = boundary.minX + boundary.width; var maxY = boundary.minY + boundary.height; context.clearRect(0, 0, canvas.width, boundary.minY); context.clearRect(maxX, 0, canvas.width - maxX, canvas.height); context.clearRect(0, maxY, canvas.width, canvas.height - maxY); context.clearRect(0, 0, boundary.minX, canvas.height); } function checkBoundary() { if (square.y >= boundary.height) { endGame(); } } function checkWalls() { var left = square.x; var right = square.x + square.size; var top = square.y; var bottom = square.y + square.size; for (var i=0; i < wall.positions.length; ++i) { var wallInfo = wall.positions[i]; var wallLeft = wallInfo.x - distance; var wallRight = wallLeft + wall.width; if (wallLeft > right) continue; if (wallRight < left) continue; var topWallBottom = wallInfo.topHeight; var bottomWallTop = boundary.height - wallInfo.bottomHeight; if (top < topWallBottom || bottom > bottomWallTop) { endGame(); } } } function drawTitle() { context.save(); context.font = "15px serif"; context.textAlign = 'left'; context.textBaseline = 'bottom'; var x = boundary.minX; var y = boundary.minY; context.fillText('Flappy Square', x, y); context.restore(); } function drawScore() { context.save(); context.font = "15px serif"; context.textAlign = 'right'; context.textBaseline = 'bottom'; var x = boundary.minX + boundary.width; var y = boundary.minY; context.fillText('Score: ' + Math.floor(distance / 20), x, y); context.restore(); } function endGame() { context.font = "20px serif"; context.textAlign = 'center'; var xCenter = boundary.width / 2; var yCenter = boundary.height / 2; context.fillText('Game Over', xCenter, yCenter); pauseAnimation(); } function programSteps() { context.clearRect(0, 0, canvas.width, canvas.height); adjustPosition(); context.save(); context.translate(boundary.minX, boundary.minY); drawBoundary(); drawSquare(); drawWalls(); checkWalls(); checkBoundary(); context.restore(); clearBoundary(); drawTitle(); drawScore(); } function runProgram() { interval = setInterval(programSteps, 50); } canvas.addEventListener('click', flap); // The following code is provided for you. // It creates an eventListener that listens // for the canvas to come into "focus", which // happens when you click on it. // This allows us to stop and start each individual // animation on this whole page separately. function startAnimation() { runProgram(); } function pauseAnimation() { clearInterval(interval); } canvas.addEventListener('focus', startAnimation); canvas.addEventListener('blur', pauseAnimation); canvas.focus();
 

Lesson: Colors

Our game is a little stark and some times it is hard to read the "Game Over" message because the flappy square, the walls, and the text are all black.

In a future challenge path we'll explore using sprites and background images to give our game a much more realistic feel, but for now we can just provide our game with some simple colors to make the different object more obvious.

The context.fillStyle attribute makes it easy for us to change the color of the flappy square, the walls, and the text.

In this example we show two versions of the same drawing, but one is all black and one uses fillStyle to make the overlapping rectangles and text visible.

Quick Reference: fillStyle fillRect()

Editor (write code below)
var canvas = document.getElementById('flappy_square_stage5_lesson2'); var context = canvas.getContext('2d'); function draw(x, y, color1, color2, color3) { context.save(); context.translate(x, y); context.fillStyle = color1; context.fillRect(0, 0, 300, 100); context.fillStyle = color2; context.fillRect(0, 0, 150, 50); context.fillStyle = color3; context.font = '20px serif'; context.textBaseline = 'middle'; context.fillText('HI THERE!', 25, 25); context.restore(); } draw(50, 50, 'black', 'black', 'black'); draw(50, 200, 'red', 'green', 'black');
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

Challenge 4

Challenge4example
Challenge 4 Sample Solution

Let's try to make the different elements in our game more distinct and visible.

Try changing the colors of the flappy square, the walls, and the "Game Over" text.

You can use any color you want. We've used "red" for our flappy square, "green" for our walls, and we've left the "Game Over" text as black.

Remember that context.fillStyle sets the fillStyle from that point onward so you may want to use context.save() and context.restore() to reset the fillStyle after you change it.

In the end your flappy square should look like the example provided at the right, but with possibly different colors.

Quick Reference: fillStyle fillRect()

Previous Challenge: View your code from Stage 5 Challenge 3 to use on this challenge.

Code Missing: You have not yet entered any code in to the previous challenge: Stage 5 Challenge 3
Stage 5 Challenge 3
Editor (write code below)
var canvas = document.getElementById('flappy_square_stage5_challenge4'); var context = canvas.getContext('2d');
Message Log
This is a lesson, not a challenge, the code runs automatically.

But change it! Play with it! Click "Run" to see your changes.

Run
Run and Focus Canvas
Reset
Canvas (your drawing will display here)

A Solution: Here's the code I wrote to complete this challenge. View One Possible Solution

var canvas = document.getElementById('flappy_square_stage5_challenge4'); var context = canvas.getContext('2d'); var interval; var distance = 0; var gravity = 1; var boundary = { minX: 25, minY: 25, width: 425, height: 275 }; var square = { x: 25, y: 75, size: 15, yVelocity: 0, jump: -10 }; var wall = { width: 50, minHole: 70, maxHole: 120, positions: [] }; function drawBoundary() { context.strokeRect(0, 0, boundary.width, boundary.height); } function drawSquare() { context.save(); context.fillStyle = 'red'; context.fillRect(square.x, square.y, square.size, square.size); context.restore(); } function drawWall(wallInfo) { context.save(); context.fillStyle = 'green'; context.translate(wallInfo.x - distance, 0); context.fillRect( 0, 0, wall.width, wallInfo.topHeight ); context.fillRect( 0, boundary.height - wallInfo.bottomHeight, wall.width, wallInfo.bottomHeight ); context.restore(); } function random(min, max) { return (min + Math.floor(Math.random() * (max - min))); } function drawWalls() { var wallX = square.x; if (wall.positions.length > 0) { wallX = wall.positions[wall.positions.length - 1].x; } var end = boundary.width + distance + wall.width; while (wallX < end) { wallX += random(100, 200); var topHeight = random(25, 150); var bottomHeight = random(25, 150); if (boundary.height - topHeight - bottomHeight < wall.minHole) { bottomHeight = boundary.height - topHeight - wall.minHole; } if (boundary.height - topHeight - bottomHeight > wall.maxHole) { bottomHeight = boundary.height - topHeight - wall.maxHole; } wall.positions.push({ x: wallX, topHeight: topHeight, bottomHeight: bottomHeight }); } for (var i=0; i < wall.positions.length; ++i) { drawWall(wall.positions[i]); } } function flap() { square.yVelocity = square.jump; } function adjustPosition() { distance += 2; square.yVelocity += gravity; square.y += square.yVelocity; } function clearBoundary() { var maxX = boundary.minX + boundary.width; var maxY = boundary.minY + boundary.height; context.clearRect(0, 0, canvas.width, boundary.minY); context.clearRect(maxX, 0, canvas.width - maxX, canvas.height); context.clearRect(0, maxY, canvas.width, canvas.height - maxY); context.clearRect(0, 0, boundary.minX, canvas.height); } function checkBoundary() { if (square.y >= boundary.height) { endGame(); } } function checkWalls() { var left = square.x; var right = square.x + square.size; var top = square.y; var bottom = square.y + square.size; for (var i=0; i < wall.positions.length; ++i) { var wallInfo = wall.positions[i]; var wallLeft = wallInfo.x - distance; var wallRight = wallLeft + wall.width; if (wallLeft > right) continue; if (wallRight < left) continue; var topWallBottom = wallInfo.topHeight; var bottomWallTop = boundary.height - wallInfo.bottomHeight; if (top < topWallBottom || bottom > bottomWallTop) { endGame(); } } } function drawTitle() { context.save(); context.font = "15px serif"; context.textAlign = 'left'; context.textBaseline = 'bottom'; var x = boundary.minX; var y = boundary.minY; context.fillText('Flappy Square', x, y); context.restore(); } function drawScore() { context.save(); context.font = "15px serif"; context.textAlign = 'right'; context.textBaseline = 'bottom'; var x = boundary.minX + boundary.width; var y = boundary.minY; context.fillText('Score: ' + Math.floor(distance / 20), x, y); context.restore(); } function endGame() { context.font = "20px serif"; context.textAlign = 'center'; var xCenter = boundary.width / 2; var yCenter = boundary.height / 2; context.fillText('Game Over', xCenter, yCenter); pauseAnimation(); } function programSteps() { context.clearRect(0, 0, canvas.width, canvas.height); adjustPosition(); context.save(); context.translate(boundary.minX, boundary.minY); drawBoundary(); drawSquare(); drawWalls(); checkWalls(); checkBoundary(); context.restore(); clearBoundary(); drawTitle(); drawScore(); } function runProgram() { interval = setInterval(programSteps, 50); } canvas.addEventListener('click', flap); // The following code is provided for you. // It creates an eventListener that listens // for the canvas to come into "focus", which // happens when you click on it. // This allows us to stop and start each individual // animation on this whole page separately. function startAnimation() { runProgram(); } function pauseAnimation() { clearInterval(interval); } canvas.addEventListener('focus', startAnimation); canvas.addEventListener('blur', pauseAnimation); canvas.focus();