Having Fun With Code: Building a Flappy Bird Clone using CreateJS and the HTML Canvas

Having Fun With Code: Building a Flappy Bird Clone using CreateJS and the HTML Canvas

Today we will build a Flappy Bird Clone using the HTML Canvas and CreateJS, A suite of modular libraries and tools which work together or independently to enable rich interactive content on open web technologies via HTML5. We will build an HTML5 game that is playable on any browser.

Step 1: Setting up the Environment with Easel.js

To get started, we draw the background and clouds using Easel onto our HTML canvas. We have a folder with the clouds, pipes, and grass stored in an assets folder which we will load using Preload.js. Our background is a gradient that mimics a blue sky. The index.html file below will draw the environment for us.



#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.

The Gradient:

Using the game.js file, we create an init function where we define our stage and create our background with the gradient. This function initializes the stage, where we will add all our items to draw. Note that StageGL means we will use WebGL. 


#game.js
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 Result:


No alt text provided for this image

Loading Assets

#game.js
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 assets folder. It specifies a complete handler, so we can put the clouds on the canvas once all the images are loaded.

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

We need a way to update the static canvas on a regular basis. To avoid calling stage.update() manually any time a change is made, we make use of Tween.js which includes a ticker that we can use to update our stage automatically at a specific rate.


#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);
  ...
}
        


#game.js
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.

Adding Player Movements

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


#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!");
};        

Step 3: Detecting Collision and Creating Obstacles

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


#game.js
var started = false; // place this at the top with other global
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 source of the pipe 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.

The Result:

No alt text provided for this image

Ending the Game 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

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.


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();
  }
}        

That's it for the game, the link to this project can be found here. Stay safe and happy coding.

To view or add a comment, sign in

More articles by Tinashe Matembo

Others also viewed

Explore content categories