Creating H5 game: big adventure at the fingertips

Time:2020-9-29

Before seeing a fingertip adventure game, I thought it was very interesting, so I wanted to learn how to realize it. After all, when the industry and economy put forward similar requirements and asked me whether the development could be realized, I would not answer that I didn’t know.
The main idea of this article, reference is concave convex lab this article: H5 game development: fingertip adventure, through this article and code, learn the overall idea and key technology points of game building. Through the Chinese tutorial of createjs, learn the foundation of createjs, and then turn to the document if you are not clear about the API.
Click here to try the game

Introduction to createjs

To get a general idea of the composition of createjs, the functions of each part and the commonly used APIs, please refer to this article.
There are four parts in createjs:

  • Easeljs: for sprites, graphics and bitmap drawing, is the core library to operate canvas
  • Tweenjs: for animation
  • Soundjs: audio playback engine
  • Preloadjs: Website Resources preloading, providing loading progress callback, and resource acquisition

Easeljs common methods

Easeljs is the encapsulation of canvas API, which is convenient for us to operate canvas to draw graphics. Easeljs defines many types for us to use.

Stage class

The stage class is used to instantiate a stage. In fact, it is a wrapper for the canvas element. A canvas element corresponds to a stage. Our final element should be added to the stage using the addchild method.

const canvas = document.querySelector('#canvas');
//Create a stage
const stage = new createjs.Stage(canvas);

Shape class

The shape class is used to draw graphics. Each drawing of a shape requires a new shape object. Many methods of object inheritance can be called in chain, which is quite convenient to use. For example, if we want to draw a circle, we only need the following simple code to complete

//Create a shape object
const circle = new createjs.Shape();
//Use brush to set color, call method to draw rectangle, rectangle parameter: X, y, W, H
circle.graphics.beginFill("#f00").drawCircle(0, 0, 100);
//Add to stage
stage.addChild(circle);
//Refresh the stage
stage.update();

Graphics is actually an instance of the graphics class, which contains many methods.

Bitmap、SpriteSheet

These two classes are used to operate pictures. Bitmap is used to draw a single image to a stage. Spritesheet can be compared to sprite image in CSS. It can be used to extract multiple sprite images from one image, and it can also be used to make picture frame animation conveniently.
For example, if we want to use leaf pictures in the game, we can add them as follows

const img = new Image();
img.src = './imgs/leaf.png';
let leaf = null;
img.onload = () => {
    leaf = new Createjs.Bitmap('./imgs/leaf.png');
    stage.addChild(leaf);
    stage.update();
}

The above steps are cumbersome to ensure that the images are loaded and then rendered to the stage. Preloadjs provides us with a more easy-to-use preloading method. The above code can be modified as follows:

const queue = new createjs.LoadQueue();

queue.loadManifest([
  { id: 'leaf', src: require('./imgs/leaf.png') },
]);

let leaf = null;
queue.on('complete', () => {
  leaf = new createjs.Bitmap(preload.getResult('leaf'));
  stage.addChild(leaf);
  stage.update();
});

Spritesheet can be used to facilitate the operation of Sprite map. For example, in the game, obstacles and stairs are all on a sprite map. We can easily get the sprite we want by the following methods. We want to obtain the ladder as follows:

const spriteSheet = new createjs.SpriteSheet({
      images: [preload.getResult('stair')],
      frames: [
        [0, 0, 150, 126],
        [0, 126, 170, 180],
        [170, 126, 170, 180],
        [340, 126, 170, 180],
        [510, 126, 170, 180],
        [680, 126, 170, 180],
      ],
      animations: {
        stair: [0],
        wood: [1],
        explosive: [2],
        ice: [3],
        mushroom: [4],
        stone: [5],
      },
    });

 const stair = new createjs.Sprite(spriteSheet, 'stair');

At the same time, it is convenient to make frame animation, such as the jumping animation of robot

const spriteSheet = new createjs.SpriteSheet({
      images: [prelaod.getResult('player')],
      frames: {
        width: 150,
        height: 294,
        count: 17,
      },
      animations: {
        work: [0, 9, 'walk', 0.2],
        jump: [10, 16, 0, 0.5],
      },
    });
const sprite = new createjs.Sprite(spriteSheet);
sprite.gotoAndPlay('jump');

Container class

Container class is used to create a new container object. It can contain other easeljs elements such as text, bitmap, shape, Sprite, etc. multiple elements are contained in a container to facilitate unified management. For example, in the game, floor objects and robot objects are actually added to the same container to ensure that floor and robot are always in the center of the screen.

const contain = new createjs.Container();
contain.addChild(floor, robot);
stage.addChild(contain);

Stage refresh

The stage refresh calls update, but it is not always possible to manually call it. We usually call in the ticker event inside createjs, and each time we trigger tick event, we will stage update.

createjs.Ticker.addEventListener(“tick”, tick);
function tick(e) {
    if (e.paused !== 1) {
        //Processing
        stage.update (); // refresh the stage
    }else {}
}
createjs.Ticker.paused  =1; // if this is called anywhere in the function, the processing in tick will be suspended
createjs.Ticker.paused  =0; // resume the game
createjs.Ticker.setFPS (60); // used to set the frequency of the tick

TweenJS

Tweenjs is mainly responsible for animation processing. For example, the displacement animation of leaves in the game is as follows:

createjs.Tween.get(this.leafCon1, { override: true })
                    .to({ y: this.nextPosY1 }, 500)
                    .call(() => { this.moving = false; });

When override is set to true, it is to ensure that no other animation is executing when the current animation is executed. To set the Y coordinate of leafcon1 to nextposy1, and call is the callback after the animation is executed.
In the process of writing the game, there are so many commonly used APIs, and there are many usages. You can refer to the documents when you need to.

The realization of the game

The whole game is divided into scene layer, ladder layer and background layer according to the rendering level. At each level, you only need to focus on its own rendering and the logical interface exposed to the control layer.
We split the game into four objects. Leaves are used to render the leaves background with infinite rolling effect; floor, a ladder class, is used to render stairs and obstacles to realize the generation and drop method of stairs; robot human robot is used to render robots, The game class is used to control the whole process of the game, responsible for the final rendering of the whole stage and the logical operation of each object.

Leaves

For the scene layer, it is used to render the leaves on both sides. The rendering of the leaves is relatively simple. We only render two leaf images to canvas. In createjs, all our instances are added to the stage through the method of addchild. We use bitmap to create the two images, set the corresponding x coordinates (one is close to the left side of the screen, and the other is close to the right side). At the same time, we add two bitmap instances to the container to operate as a whole. Because the scene layer needs to make the effect of infinite extension, it is necessary to copy a container to create the illusion of continuous movement. For specific principles, refer to fingertip adventure. In each click event, call translatey (offset) to make the leaf move a certain distance.

class Leaves {
  constructor(options, canvas) {
    this.config = {
      transThreshold: 0,
    };
    Object.assign(this.config, options);

    this.moving = false;
    this.nextPosY1 = 0;
    this.nextPosY2 = 0;
    this.canvas = canvas;
    this.leafCon1  =Null; // container for leaf background
    this.leafCon2 = null;
    this.sprite = null;
    this.leafHeight = 0;
    this.init();
  }

  init() {
    const left = new createjs.Bitmap(preload.getResult('left'));
    const right = new createjs.Bitmap(preload.getResult('right'));
    left.x = 0;
    right.x = this.canvas.width - right.getBounds().width;
    this.leafCon1 = new createjs.Container();
    this.leafCon1.addChild(left, right);
    this.leafHeight = this.leafCon1.getBounds().height;
    this.nextPosY1 = this.leafCon1.y = this.canvas.height - this.leafHeight; // eslint-disable-line
    this.leafCon2  = this.leafCon1 . clone (true); // in some createjs versions, this method will report an error that the image cannot be found
    this.nextPosY2 = this.leafCon2.y = this.leafCon1.y - this.leafHeight; // eslint-disable-line
    this.sprite = new createjs.Container();
    this.sprite.addChild(this.leafCon1, this.leafCon2);
  }

  tranlateY(distance) {
    if (this.moving) return;
    this.moving = true;
    const threshold = this.canvas.height || this.config.transThreshold;
    const curPosY1 = this.leafCon1.y;
    const curPosY2 = this.leafCon2.y;
    this.nextPosY1 = curPosY1 + distance;
    this.nextPosY2 = curPosY2 + distance;

    if (curPosY1 >= threshold) {
      this.leafCon1.y = this.nextPosY2 - this.leafHeight;
    } else {
      createjs.Tween.get(this.leafCon1, { override: true })
                    .to({ y: this.nextPosY1 }, 500)
                    .call(() => { this.moving = false; });
    }

    if (curPosY2 >= threshold) {
      this.leafCon2.y = this.nextPosY1 - this.leafHeight;
    } else {
      createjs.Tween.get(this.leafCon2, { override: true })
                    .to({ y: this.nextPosY2 }, 500)
                    .call(() => { this.moving = false; });
    }
  }
}

Floor

The ladder class is used to generate the ladder and obstacles, and also responsible for the logic of the ladder falling.

class Floor {
  constructor(config, canvas) {
    this.config = {};
    this.stairSequence  =[]; // ladder render the corresponding sequence
    this.barrierSequence  =[]; // obstacle rendering corresponding sequence
    this.stairArr  =[]; // array of spice objects of the ladder 
    this.barrierArr  =[]; // array of spice objects for obstacles
    this.barrierCon  =Null; // obstacle container
    this.stairCon  =Null; // ladder container
    this.canvas = canvas;
    this.lastX  =0; // location of the latest ladder
    this.lastY = 0;
    this.dropIndex = -1;
    Object.assign(this.config, config);
    this.init();
  }

  init() {
    this.stair = new createjs.Sprite(spriteSheet, 'stair');
    this.stair.width = this.stair.getBounds().width;
    this.stair.height = this.stair.getBounds().height;

    let barriers = ['wood', 'explosive', 'ice', 'mushroom', 'stone'];
    barriers = barriers.map((item) => {
      const container = new createjs.Container();
      const st = this.stair.clone(true);
      const bar = new createjs.Sprite(spriteSheet, item);
      bar.y = st.y - 60;
      container.addChild(st, bar);
      return container;
    });

    this.barriers = barriers;

    const firstStair = this.stair.clone(true);
    firstStair.x = this.canvas.width / 2 - this.stair.width / 2; //eslint-disable-line
    firstStair.y = this.canvas.height - this.stair.height - bottomOffset;//eslint-disable-line
    this.lastX = firstStair.x;
    this.lastY = firstStair.y;

    this.stairCon = new createjs.Container();
    this.barrierCon = new createjs.Container();
    this.stairCon.addChild(firstStair);
    this.stairArr.push(firstStair);
    this.sprite = new createjs.Container();
    this.sprite.addChild(this.stairCon, this.barrierCon);
  }

  addOneFloor(stairDirection, barrierType, animation) {
    //Stardirection - 1 represents the left side of the previous ladder and the right side of 1
    //Add a ladder one by one, and add an obstacle to the corresponding selection
  }

  addFloors(stairSequence, barrierSequence) {
    stairSequence.forEach((item, index) => {
      this.addOneFloor (item, barriersequence [index], false); // batch adding without animation
    });
  }

  dropStair(stair) {
   //Drop and touch a ladder, and the y-axis coordinate in the obstacle array is greater than the y-axis coordinate of the current drop step
  }

  drop() {
    const stair = this.stairArr.shift();

    stair && this.dropStair(stair); // eslint-disable-line

    while (this.stairArr.length > 9) {
      this.dropStair ( this.stairArr.shift ()); // the ladder array displays up to 9 ladders
    }
  }
}

Robot

The robot class is used to create robot objects. The robot objects need move method to jump the ladder, and also need to deal with the situations of stepping over and hitting obstacles.

class Robot {
  constructor(options, canvas) {
    this.config = {
      initDirect: -1,
    };
    Object.assign(this.config, options);
    this.sprite = null;
    this.canvas = canvas;
    this.lastX  =0; // last X-axis position
    this.lastY  =0; // last Y-axis position
    this.lastDirect  = this.config.initDirect ; // direction of last jump
    this.init();
  }

  init() {
    const spriteSheet = new createjs.SpriteSheet({
      /*Sprites*/
    });
    this.sprite = new createjs.Sprite(spriteSheet);
    const bounds = this.sprite.getBounds();
    this.sprite.x = this.canvas.width / 2 - bounds.width / 2;
    this.lastX = this.sprite.x;
    this.sprite.y = this.canvas.height - bounds.height - bottomOffset - 40;
    this.lastY = this.sprite.y;
    if (this.config.initDirect === 1) {
      this.sprite.scaleX = -1;
      this.sprite.regX = 145;
    }
    // this.sprite.scaleX = -1;
  }

  move(x, y) {
    this.lastX += x;
    this.lastY += y;

    this.sprite.gotoAndPlay('jump');
    createjs.Tween.get(this.sprite, { override: true })
                  .to({
                    x: this.lastX,
                    y: this.lastY,
                  }, 200);
  }

  moveRight() {
    if (this.lastDirect !== 1) {
      this.lastDirect = 1;
      this.sprite.scaleX = -1;
      this.sprite.regX = 145;
    }
    this.move(moveXOffset, moveYOffset);
  }

  moveLeft() {
    if (this.lastDirect !== -1) {
      this.lastDirect = -1;
      this.sprite.scaleX = 1;
      this.sprite.regX = 0;
    }
    this.move(-1 * moveXOffset, moveYOffset);
  }

  Dropandappear (DIR) {// step down drop processing
    const posY = this.sprite.y;
    const posX = this.sprite.x;
    this.sprite.stop();
    createjs.Tween.removeTweens(this.sprite);
    createjs.Tween.get(this.sprite, { override: true })
                  .to({
                    x: posX + dir * 2 * moveXOffset,
                    y: posY + moveYOffset,
                  }, 240)
                  .to({
                    y: this.canvas.height + this.sprite.y,
                  }, 800)
                  .set({
                    visible: false,
                  });
  }

  Hitanddisappear() {// collision obstacle handling
    createjs.Tween.get(this.sprite, { override: true })
                  .wait(500)
                  .set({
                    visible: false,
                  });
  }
}

Game

The game class is the control center of the whole game. It is responsible for the processing of user click events and the final addition of each object to the stage,

class Game {
  constructor(options) {
    // this.init();
    this.config = {
      initStairs: 8,
      onProgress: () => {},
      onComplete: () => {},
      onGameEnd: () => {},
    };
    Object.assign(this.config, options);
    this.stairIndex  =- 1; // record the current hop level
    this.autoDropTimer = null;
    this.clickTimes = 0;
    this.score = 0;
    this.isStart = false;
    this.init();
  }

  init() {
    this.canvas = document.querySelector('#stage');
    this.canvas.width = window.innerWidth * 2;
    this.canvas.height = window.innerHeight * 2;
    this.stage = new createjs.Stage(this.canvas);

    createjs.Ticker.setFPS(60);
    createjs.Ticker.addEventListener('tick', () => {
      if (e.paused !== true) {
        this.stage.update();
      }
    });

    queue.on('complete', () => {
      this.run();
      this.config.onComplete();
    });
    queue.on('fileload', this.config.onProgress);
  }

  Getinitialsequence() {// get the initial ladder and obstacle sequence
    const stairSeq = [];
    const barrSeq = [];
    for (let i = 0; i < this.config.initStairs; i += 1) {
      stairSeq.push(util.getRandom(0, 2));
      barrSeq.push(util.getRandomNumBySepcial(this.config.barrProbabitiy));
    }
    return {
      stairSeq,
      barrSeq,
    };
  }

  Creategamestage() {// render stage
    this.background = new createjs.Shape();
    this.background.graphics.beginFill('#001605').drawRect(0, 0, this.canvas.width, this.canvas.height);

    const seq = this.getInitialSequence();
    this.leves = new Leaves(this.config, this.canvas);
    this.floor = new Floor(this.config, this.canvas);
    this.robot = new Robot({
      initDirect: seq.stairSeq[0],
    }, this.canvas);
    this.stairs = new createjs.Container();
    this.stairs.addChild(this.floor.sprite, this.robot.sprite);
    //Robot and ladder are integrated, so as to keep the relative distance between robot and stair when jumping
    this.stairs.lastX = this.stairs.x;
    this.stairs.lastY = this.stairs.y;
    this.floor.addFloors(seq.stairSeq, seq.barrSeq);
    this.stage.addChild(this.background, this.stairs, this.leves.sprite);
    //Only when all containers are added again can the stage clear be effective and the stage be re rendered. Otherwise, there will be repeated ones after restart
  }

  bindEvents() {
    this.background.addEventListener ('click',  this.handleClick.bind (this)); // it can only be triggered if there is an element, and clicking on the blank area is invalid
    // this.stage.addEventListener ('click',  this.handleClick ); // it can only be triggered if there is an element. Clicking on the blank area is invalid
  }

  run() {
    this.clickTimes = 0;
    this.score = 0;
    this.stairIndex = -1;
    this.autoDropTimer = null;
    this.createGameStage();
    this.bindEvents();
    createjs.Ticker.setPaused(false);
  }

  start() {
    this.isStart = true;
  }

  restart() {
    this.stage.clear();
    this.run();
    this.start();
  }

  handleClick(event) {
    if (this.isStart) {
      const posX = event.stageX;
      this.stairIndex += 1;
      this.clickTimes += 1;
      let direct = -1;
      this.autoDrop();
      if (posX > (this.canvas.width / 2)) {
        this.robot.moveRight();
        direct = 1;
        this.centerFloor(-1 * moveXOffset, -1 * moveYOffset);
      } else {
        this.robot.moveLeft();
        direct = -1;
        this.centerFloor(moveXOffset, -1 * moveYOffset);
      }
      this.addStair();
      this.leves.tranlateY(-1 * moveYOffset);
      this.checkJump(direct);
    }
  }

  Centerfloor (x, y) {// keep the staircase in the middle of the stage
    this.stairs.lastX += x;
    this.stairs.lastY += y;

    createjs.Tween.get(this.stairs, { override: true })
                  .to({
                    x: this.stairs.lastX,
                    y: this.stairs.lastY,
                  }, 500);
  }

  Checkjump (direct) {// the robot checks whether it falls or disappears every time it jumps
    const stairSequence =  this.floor.stairSequence ; // like [- 1, 1, 1, - 1], - 1 for left and 1 for right
   
    if (direct !== stairSequence[ this.stairIndex ]) {// if the step direction of the current jump floor is inconsistent with the jump direction, it means failure
      this.drop(direct);
      this.gameOver();
    }
  }

  drop(direct) {
    const barrierSequence = this.floor.barrierSequence;

    if (barrierSequence[this.stairIndex] !== 1) {
      this.robot.dropAndDisappear(direct);
    } else {
      this.shakeStairs();
      this.robot.hitAndDisappear();
    }
  }

  shakeStairs() {
    createjs.Tween.removeTweens(this.stairs);
    createjs.Tween.get(this.stairs, {
      override: true,
    }).to({
      x: this.stairs.x + 5,
      y: this.stairs.y - 5,
    }, 50, createjs.Ease.getBackInOut(2.5)).to({
      x: this.stairs.x,
      y: this.stairs.y,
    }, 50, createjs.Ease.getBackInOut(2.5)).to({
      x: this.stairs.x + 5,
      y: this.stairs.y - 5,
    }, 50, createjs.Ease.getBackInOut(2.5)).to({ // eslint-disable-line
      x: this.stairs.x,
      y: this.stairs.y,
    }, 50, createjs.Ease.getBackInOut(2.5)).pause(); // eslint-disable-line
  }

  Addstair() {// add a ladder with random direction
    const stair = util.getRandom(0, 2);
    const barrier = util.getRandomNumBySepcial(this.config.barrProbabitiy);
    this.floor.addOneFloor(stair, barrier, true);
  }

  Autodrop() {// ladder falls down automatically
    if (!this.autoDropTimer) {
      this.autoDropTimer = createjs.setInterval(() => {
        this.floor.drop();
        if (this.clickTimes === this.floor.dropIndex) {
          createjs.clearInterval(this.autoDropTimer);
          this.robot.dropAndDisappear(0);
          this.gameOver();
        }
      }, 1000);
    }
  }

  gameOver() {
    createjs.clearInterval(this.autoDropTimer);
    this.isStart = false;
    this.config.onGameEnd();
    setTimeout(() => {
      createjs.Ticker.setPaused(true);
    }, 1000);
  }
}

summary

In this paper, based on the H5 game development: fingertip adventure, the code is implemented once. In this process, we not only learn some basic usage of createjs, but also know that the solution of game development problems can be considered from the visual level and the logic bottom layer. There are also some problems in the use of createjs. For example, after the clear stage, the elements on the stage are not empty. I have also made comments in the code. Interested students can take a look at the source code https://github.com/shengbowen…

reference resources

  • H5 game development: fingertip Adventure
  • A quick introduction to create JS
  • Chinese course of createjs
  • Create EJS video