Create an HTML 5 Game with Create.js: Flappy Bird Clone

If you’ve ever wanted to create your own HTML 5 game, you have come to the right place. In this article we will create the complete Flappy Bird clone seen above using the Create.js suite of libraries: Easel.js for drawing, Tween.js for motion and Preload.js for asset loading.

Step 1: Drawing the Environment with Easel.js

To get started, we’ll draw the background and clouds with Easel.js to a canvas. To load the clouds (and other assets), we’ll use Preload.js. The background is simply a gradient specified in the JavaScript. Start by creating the HTML file below that houses our canvas element.

Creating the Canvas: index.html

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Flappy Bird Clone</title>
    <script src="https://code.createjs.com/1.0.0/createjs.min.js"></script>
    <script src="game.js"></script>
  </head>
  <body onload="init()">
    <canvas id="gameCanvas" width="320" height="480" style="display: block; margin: 0 auto;"></canvas>
  </body>
</html>

This HTML file specifies the canvas element we will draw our game to and loads create.js and our own JavaScript file game.js. It tells the browser to call a function called init in our JavaScript file when the body loads.

Drawing the Gradient Background: game.js

Screenshot of completed gradient created with create.js easel.js
Completed gradient
var stage, loader;

function init() {
  stage = new createjs.StageGL("gameCanvas");

  var background = new createjs.Shape();
  background.graphics.beginLinearGradientFill(["#2573BB", "#6CB8DA", "#567A32"], [0, 0.85, 1], 0, 0, 0, 480)
  .drawRect(0, 0, 320, 480);
  background.x = 0;
  background.y = 0;
  background.name = "background";
  background.cache(0, 0, 320, 480);

  stage.addChild(background);

  stage.update();
}

The Javascript above specifies our gradient and places it on the canvas. Note that this is in the init function, which was specified to be called by the browser when the body loads in our HTML file. This function initializes the stage, where we will add all our items to draw. Note that StageGL means we will use WebGL. To use the Canvas 2D context, you could simply specify Stage instead. Also take note of the cache function. Without this, WebGL will not display the gradient but Canvas 2D would display it with lower performance.

Note that in many modern browsers, such as Chrome, you can’t view WebGL content by simply double clicking the HTML file and opening it in the browser. It must be served by a web server. The python HTTP server works well for testing. Open the directory where your game is and run python3 -m http.server then navigate to localhost:8000.

Loading Our Assets: game.js

Clouds loaded in and displayed
function init() {
  ...

  var manifest = [
    { "src": "cloud.png", "id": "cloud" },
    { "src": "flappy.png", "id": "flappy" },
    { "src": "pipe.png", "id": "pipe" },
  ];

  loader = new createjs.LoadQueue(true);
  loader.addEventListener("complete", handleComplete);
  loader.loadManifest(manifest, true, "./img/");
}

function handleComplete() {
  createClouds();
}

function createClouds() {
  var clouds = [];
  for (var i = 0; i < 3; i++) {
    clouds.push(new createjs.Bitmap(loader.getResult("cloud")));
  }

  clouds[0].x = 40;
  clouds[0].y = 20;
  clouds[1].x = 140;
  clouds[1].y = 70;
  clouds[2].x = 100;
  clouds[2].y = 130;

  for (var i = 0; i < 3; i++) {
    stage.addChild(clouds[i]);
  }

  stage.update();
}

The first part of this code loads all the assets we will need for our game. Images are loaded from the img folder. It specifies a complete handler, so we can put the clouds on the canvas once all the images are loaded. In the mean time, users will look at our empty gradient. If you have many assets to load, you may wish to respond to the progress event in addition to the completed event to provide a progress indicator of some kind. For more information about loading assets with Preload.js, see the documentation.

The second part (handleComplete and createClouds functions) deals with placing the clouds on the canvas. Note that our clouds display above the background because they were added after. Bitmaps do not need to be cached even when using WebGL, and doing so can actually harm performance, so we do not cache the clouds.

Step 2: Setting the Game in Motion with Tween.js

What we created above is essentially static content, there is no movement and it is no superior to an image on a page. We now need a way to update the canvas on a regular basis, rather than calling stage.update() manually any time we make a change. Then, we can change the x, y, rotation, and other values of our assets and have them appear on screen.

Updating the Stage: game.js

function init() {
  stage = new createjs.StageGL("gameCanvas");

  createjs.Ticker.timingMode = createjs.Ticker.RAF_SYNCHED;
  createjs.Ticker.framerate = 60;
  createjs.Ticker.addEventListener("tick", stage);
  ...
}

Tween.js includes a ticker that we can use to update the stage automatically at a specific rate. In this case, we ask it to sync to 60 FPS and pass tick events to the stage. With this added to the init function, you can remove any stage.update() calls elsewhere in the code with no ill effects.

Moving the Clouds: game.js

Animated gif screenshot of clouds moving
function createClouds() {
  ...

  for (var i = 0; i < 3; i++) {
    var directionMultiplier = i % 2 == 0 ? -1 : 1;
    createjs.Tween.get(clouds[i], { loop: true})
    .to({ x: clouds[i].x - (200 * directionMultiplier)}, 3000, createjs.Ease.getPowInOut(2))
    .to({ x: clouds[i].x }, 3000, createjs.Ease.getPowInOut(2));
    stage.addChild(clouds[i]);
  }
}

Now that we have the ticker updating the stage at a regular interval, we can add a tween to move our clouds side to side while the game is running. This is simply a decorative effect, but it’s a simple tween to help us get familiar with the Tween.js API. For each cloud, we tell Tween.js to interpolate the x between the current position and a lesser or greater one, and then do the same thing back again, taking 3 seconds for each motion. Tween.js will gradually change the x position following the easing we specified, in this case a PowInOut. PowInOut allows the motion to start slow, get faster, and slow down again before finishing. You can increase the exponent to make the effect more drastic.

Adding Flappy and Player Movement: game.js

var flappy;
function handleComplete() {
  createClouds();
  createFlappy();
  stage.on("stagemousedown", jumpFlappy);
}

function createFlappy() {
  flappy = new createjs.Bitmap(loader.getResult("flappy"));
  flappy.regX = flappy.image.width / 2;
  flappy.regY = flappy.image.height / 2;
  flappy.x = stage.canvas.width / 2;
  flappy.y = stage.canvas.height / 2;
  stage.addChild(flappy);
}

function jumpFlappy() {
  createjs.Tween.get(flappy, { override: true }).to({ y: flappy.y - 60, rotation: -10 }, 500, createjs.Ease.getPowOut(2))
  .to({ y: stage.canvas.height + (flappy.image.width / 2), rotation: 30 }, 1500, createjs.Ease.getPowIn(2))
  .call(gameOver);
}

function gameOver() {
  console.log("Game over!");
}

We add two functions in the above code, one to put flappy on the screen and another to respond when the user clicks the screen to make flappy jump.

In createFlappy(), we initialize flappy as a bitmap and specify x and y coordinates to the middle of the screen. You will note the specification of regX and regY. These are set at the center of flappy, and cause flappy to rotate around the center rather than the top left corner. As a result, flappy’s x and y coordinates are also specified to the center rather than top left corner of the image.

In jumpFlappy(), which is called every time the user clicks the canvas, we use a Tween to create movement for flappy. This is a bit different than how I would do player movement in many other games, but it’s an elegant solution here that provides a realistic feeling simulation of gravity with no actual physics knowledge required. First, we tell the tween to take flappy to a y position of 60 less than the current position. Remember the top left corner of the screen is (0, 0) so to go up we must decrease the y position. When that is done, we ask the tween to push flappy towards the ground. Note that we specified override, so the movement can be stopped and overridden by another jump movement at any time. If the tween gets a chance to finish, it means flappy has hit the ground. In that case, we call gameOver() which simply logs to the console for now.

Step 3: Creating Pipes and Detecting Collision

With our player movement setup, we now need some pipes to jump through and detect when the player hits them.

Generating Pipes When the Game Starts: game.js

var started = false; // place this at the top with other globals
function jumpFlappy() {
  if (!started) {
    startGame();
  }
  ...
}

function createPipes() {
  var topPipe, bottomPipe;
  var position = Math.floor(Math.random() * 280 + 100);

  topPipe = new createjs.Bitmap(loader.getResult("pipe"));
  topPipe.y = position - 75;
  topPipe.x = stage.canvas.width + (topPipe.image.width / 2);
  topPipe.rotation = 180;
  topPipe.name = "pipe";

  bottomPipe = new createjs.Bitmap(loader.getResult("pipe"));
  bottomPipe.y = position + 75;
  bottomPipe.x = stage.canvas.width + (bottomPipe.image.width / 2);
  bottomPipe.skewY = 180;
  bottomPipe.name = "pipe";

  topPipe.regX = bottomPipe.regX = topPipe.image.width / 2;

  createjs.Tween.get(topPipe).to({ x: 0 - topPipe.image.width }, 10000).call(function() { removePipe(topPipe); });
  createjs.Tween.get(bottomPipe).to( { x: 0 - bottomPipe.image.width }, 10000).call(function() { removePipe(bottomPipe); });

  stage.addChild(bottomPipe, topPipe);
}

function removePipe(pipe) {
  stage.removeChild(pipe);
}

function startGame() {
  started = true;
  createPipes();
  setInterval(createPipes, 6000);
}

This code creates a set of pipes every 6 seconds once the player starts the game by making their first jump. In the createPipes() function, you will note that the pipes source from the same image but the top pipe is rotated 180° along the top middle so it is descending from the top of the screen. Then, the bottom pipe is flipped using skewY so that the sheen appears on the same side of each image despite the rotation. A tween moves the pipes across the screen over 10 seconds. You will note no easing is specified, so Tween.js defaults to linear easing (constant speed). When the animation finishes, the pipes remove themselves from the stage.

Detecting Collisions: game.js

Animated GIF of collision detection and pipe generation in create.js flappy bird
//var polygon;
function handleComplete() {
  createClouds();
  createFlappy();
  stage.on("stagemousedown", jumpFlappy);
  createjs.Ticker.addEventListener("tick", checkCollision);
  /*polygon = new createjs.Shape();
  stage.addChild(polygon);*/
}

function checkCollision() {
  var leftX = flappy.x - flappy.regX + 5;
  var leftY = flappy.y - flappy.regY + 5;
  var points = [
    new createjs.Point(leftX, leftY),
    new createjs.Point(leftX + flappy.image.width - 10, leftY),
    new createjs.Point(leftX, leftY + flappy.image.height - 10),
    new createjs.Point(leftX + flappy.image.width - 10, leftY + flappy.image.height - 10)
  ];

  /*polygon.graphics.clear().beginStroke("black");
  polygon.graphics.moveTo(points[0].x, points[0].y).lineTo(points[2].x, points[2].y).lineTo(points[3].x, points[3].y)
  .lineTo(points[1].x, points[1].y).lineTo(points[0].x, points[0].y);*/

  for (var i = 0; i < points.length; i++) {
    var objects = stage.getObjectsUnderPoint(points[i].x, points[i].y);
    if (objects.filter((object) => object.name == "pipe").length > 0) {
      gameOver();
      return;
    }
  }
}

function gameOver() {
  createjs.Tween.removeAllTweens();
}

Collision detection here is a 4-point collision check, checking four points near the corners of flappy. Because the rectangle would otherwise be too large, the points are brought in 5 px from each corner. This allows the player to just barely grace the pipe and survive and minimizes situations where a collision could be detected by getting too close to a pipe but not quite touching. However, it is not perfect. You may wish to do research on other solutions, but this collision detection is good enough for this game for my taste. You could also try adding more points, such as 6, to get a more accurate collision. Uncomment the commented sections to display flappy’s hitbox. If you do, make sure to change stage = new createjs.StageGL("gameCanvas"); to stage = new createjs.Stage("gameCanvas"); so you avoid needing to cache the box. Change it back and remove the commented code when finished.

Game over has been changed to freeze the game when the game ends. We will fix it up later, as it currently doesn’t stop pipes from generating or the player from continuing to jump.

Step 4: Ending, Resetting and Scoring the Game

Our game mechanics are starting to take shape, but you may have noticed that our game over state allows the player to continue clicking the screen to jump, they have no way to reset the game once it ends, and they need a way to see their score.

Ending and Resetting the Game: game.js

var jumpListener; // new global
function handleComplete() {
  started = false;
  createClouds();
  createFlappy();
  jumpListener = stage.on("stagemousedown", jumpFlappy);
  createjs.Ticker.addEventListener("tick", checkCollision);
}

function gameOver() {
  createjs.Tween.removeAllTweens();
  stage.off("stagemousedown", jumpListener);
  clearInterval(pipeCreator);
  createjs.Ticker.removeEventListener("tick", checkCollision);
  setTimeout(function () {
    stage.on("stagemousedown", resetGame, null, true);
  }, 2000);
}

function resetGame() {
  var childrenToRemove = stage.children.filter((child) => child.name != "background");
  for (var i = 0; i < childrenToRemove.length; i++) {
    stage.removeChild(childrenToRemove[i]);
  }
  handleComplete();
}

The above code properly ends the game and resets it when 2 seconds has passed and the player clicks the screen again. Note that the jumpListener needs to be assigned to a global to be stopped later. Anything in init() only occurs once, hence why we avoid removing the background but remove anything else from the stage because it will be recreated by handleComplete().

Scoring the Game: game.js

function handleComplete() {
  ...
  createScore();
  ...
}

function createScore() {
  score = 0;
  scoreText = new createjs.Text(score, "bold 48px Arial", "#FFFFFF");
  scoreText.textAlign = "center";
  scoreText.textBaseline = "middle";
  scoreText.x = 40;
  scoreText.y = 40;
  var bounds = scoreText.getBounds();
  scoreText.cache(-40, -40, bounds.width*3 + Math.abs(bounds.x), bounds.height + Math.abs(bounds.y));

  scoreTextOutline = scoreText.clone();
  scoreTextOutline.color = "#000000";
  scoreTextOutline.outline = 2;
  bounds = scoreTextOutline.getBounds();
  scoreTextOutline.cache(-40, -40, bounds.width*3 + Math.abs(bounds.x), bounds.height + Math.abs(bounds.y));

  stage.addChild(scoreText, scoreTextOutline);
}

function incrementScore() {
  score++;
  scoreText.text = scoreTextOutline.text = score;
  scoreText.updateCache();
  scoreTextOutline.updateCache();
}

function createPipes() {
  ...

  createjs.Tween.get(topPipe).to({ x: 0 - topPipe.image.width }, 10000).call(function() { removePipe(topPipe); })
  .addEventListener("change", updatePipe);
  createjs.Tween.get(bottomPipe).to( { x: 0 - bottomPipe.image.width }, 10000).call(function() { removePipe(bottomPipe); });

  var scoreIndex = stage.getChildIndex(scoreText);

  stage.addChildAt(bottomPipe, topPipe, scoreIndex);
}

function updatePipe(event) {
  var pipeUpdated = event.target.target;
  if ((pipeUpdated.x - pipeUpdated.regX + pipeUpdated.image.width) < (flappy.x - flappy.regX)) {
    event.target.removeEventListener("change", updatePipe);
    incrementScore();
  }
}

Scoring is accomplished by attaching an event to the change event of the tween that moves the pipes to check when the pipe moves past the left corner of flappy. The score is increased and the listener is removed when this happens. Note the change to the stage.addChildAt function in order to ensure the score appears above the pipes.

Congratulations! That’s it!

That’s the game. Hopefully you have been able to follow the tutorial to get a working game. If that is not the case, fear not! Here is the full source code and assets download. Please enjoy and comment any questions/concerns. Use this code/assets as you see fit for personal use only. Software and tutorial are provided “as is” with no warranty of any kind.

One thought on “Create an HTML 5 Game with Create.js: Flappy Bird Clone”

Comments are closed.