Snake in CreateJS Tutorial Part 3

This is the last part of the “Snake in CreateJS Tutorial”. Here you can check the other ones: Part 1 and Part 2.

In this part, we are going to implement a scoring system and add new views – main menu and game over.

The code can be found in the repository: Repository.

Implementation of score

The score is a really simple thing. You just need to add the points every time when the snake eats food and draw a new score on the screen.

So, let’s add a score field to World class:

// source/objects/World.js

export class World {
  constructor(stage) {
    this.stage = stage;
    this.score = 0;

    ...
  }

  ...
}

And increase it by 10 every time when the snake eats food:

// source/objects/World.js

export class World {
  render() {
    ...

    if (this.checkFoodCollision()) {
      this.score += 10;

      this.snake.grow();
      this.food.generateRandomPosition();
      this.drawFood();
    }

    ...
  }
}

The second thing we need to do is render the text. We can use a built-in text class from EaselJS (https://createjs.com/docs/easeljs/classes/Text.html). So we need to create a Text instance:

// source/objects/World.js

import { Container, Ticker, Text } from "@createjs/EaselJS";
...

export class World {
  constructor(stage) {
    this.stage = stage;
    this.score = 0;
    ...

    this.scoreText = new Text("Score 0", "72px Helvetica", "#333");
    this.scoreText.x = CONFIG.snakeSize;
    this.scoreText.y = CONFIG.snakeSize;
    ...
    this.stage.addChild(this.snakeContainer, this.foodContainer, this.scoreText);

    ...
  }
}

And of course update it every time we change the score:

// source/objects/World.js

export class World {
  ...

  drawScore() {
    this.scoreText.text = `Score ${this.score}`;
  }

  ...

  render() {
    this.stage.update();

    if (this.isPaused) {
      return;
    }

    if (this.checkFoodCollision()) {
      this.score += 10;
      this.drawScore();

    ...
  }
}

Great, the effect should be following:

Update of the score
Update of the score

Adding main menu

Now, let’s add the main menu view. This will be the default view when you open a game. Add a Scene.js file to objects directory and change the onload function in the app.js file to initialize the Scene class instead of World. The Scene class will manage what view is currently visible.

// source/objects/Scene.js

import { World } from "./World";

export class Scene {
  constructor(stage) {
    this.stage = stage;

    this.world = new World(stage);
  }
}
// source/app.js

...
import { Scene } from "@/objects/Scene";

const init = () => {
  ...

  window.onload = () => {
    ...

    new Scene(stage);

    ...
  };
};

init();

Now, it’s time to add the MainMenu class to generate the main menu view. Create a MainMenu.js file in the objects directory and paste the following code in:

// source/objects/MainMenu.js

import { Container, Graphics, Shape, Text } from "@createjs/EaselJS";
import { CONFIG } from "@/config";

export class MainMenu {
  constructor(stage, changeScene) {
    this.stage = stage;
    this.changeScene = changeScene;

    this.container = new Container();

    this.renderTitle();
    this.renderPlayButton();
  }

  renderTitle() {
    const text = new Text("CreateJS SNAKE", "100px Helvetica", "#336339");
    const bounds = text.getBounds();
    text.regX = bounds.width / 2;
    text.regY = bounds.height / 2;
    text.x = CONFIG.canvasWidth / 2;
    text.y = 200;
    this.container.addChild(text);
  }

  renderPlayButton() {
    const graphics = new Graphics()
      .beginFill("#333")
      .setStrokeStyle(1)
      .beginStroke("#000000")
      .drawRoundRect(CONFIG.canvasWidth / 2 - 375, CONFIG.canvasHeight / 2 - 100, 750, 200, 50);
    const clickToPlayButton = new Shape(graphics);
    clickToPlayButton.addEventListener("click", () => this.changeScene("WORLD"));

    const clickToPlayText = new Text("Click here to play", "72px Helvetica", "#eee");
    clickToPlayText.regX = clickToPlayText.getBounds().width / 2;
    clickToPlayText.regY = clickToPlayText.getBounds().height / 2;
    clickToPlayText.x = CONFIG.canvasWidth / 2;
    clickToPlayText.y = CONFIG.canvasHeight / 2;

    this.container.addChild(clickToPlayButton, clickToPlayText);
  }
}

So, in the constructor, we add a container field for keeping all things connected to the main menu in it (that container will be later added or removed by Scene class). Below are methods for rendering stuff like game title and the “click to play” button. One thing to note is that button (or in fact shape) is interactive – you can assign event listeners to it. In this case, the listener fires the changeScene() function passed to class from Scene.

Ok, now let’s jump to World.js class and add a container for its elements (we were adding elements directly to the stage and now the stage will be managed just by Scene class).

// source/objects/World.js

...

export class World {
  constructor(stage) {
    this.stage = stage;
    this.score = 0;
    this.isPaused = true;

    this.container = new Container();
    this.scoreText = new Text("Score 0", "72px Helvetica", "#333");
    this.scoreText.x = CONFIG.snakeSize;
    this.scoreText.y = CONFIG.snakeSize;
    this.snakeContainer = new Container();
    this.foodContainer = new Container();
    this.container.addChild(this.snakeContainer, this.foodContainer, this.scoreText);

    ...
  }

  ...
}

As you can see I’ve just added the container field which refers to the Container instance and put the children inside it (instead of putting them on stage).

And now we can get back to Scene class and implement changeScene() method that will clear the stage and add a selected view as a child. Besides that, we need to instantiate views and add the default one to the stage as initial. In my case it is the main menu:

// source/objects/Scene.js

import { MainMenu } from "./MainMenu";
import { World } from "./World";

export class Scene {
  constructor(stage) {
    this.stage = stage;

    this.world = new World(stage);
    this.mainMenu = new MainMenu(stage, this.changeScene.bind(this));

    this.stage.addChild(this.mainMenu.container);
  }

  changeScene(scene) {
    this.stage.removeAllChildren();
    if (scene === "WORLD") {
      this.stage.addChild(this.world.container);
    } else if (scene === "MAIN_MENU") {
      this.stage.addChild(this.mainMenu.container);
    }
  }
}

Effect should be following:

The main menu with transition to game
The main menu with transition to game

As you can see the button is interactive and you can switch views just by using changeScene() method.

Snake hits the borders and… game over!

Let’s add game over view now. Create a GameOver.js file under the objects directory:

// source/objects/GameOver.js

import { Container, Graphics, Shape, Text } from "@createjs/EaselJS";
import { CONFIG } from "@/config";

export class GameOver {
  constructor(stage, changeScene) {
    this.stage = stage;
    this.changeScene = changeScene;

    this.container = new Container();

    this.renderTitle();
    this.renderPlayButton();
  }

  renderTitle() {
    const text = new Text("Game Over", "100px Helvetica", "#336339");
    const bounds = text.getBounds();
    text.regX = bounds.width / 2;
    text.regY = bounds.height / 2;
    text.x = CONFIG.canvasWidth / 2;
    text.y = 200;
    this.container.addChild(text);
  }

  renderPlayButton() {
    const graphics = new Graphics()
      .beginFill("#333")
      .setStrokeStyle(1)
      .beginStroke("#000000")
      .drawRoundRect(CONFIG.canvasWidth / 2 - 375, CONFIG.canvasHeight / 2 - 100, 750, 200, 50);
    const clickToPlayButton = new Shape(graphics);
    clickToPlayButton.addEventListener("click", () => this.changeScene("WORLD"));

    const clickToPlayText = new Text("Click to play again", "72px Helvetica", "#eee");
    clickToPlayText.regX = clickToPlayText.getBounds().width / 2;
    clickToPlayText.regY = clickToPlayText.getBounds().height / 2;
    clickToPlayText.x = CONFIG.canvasWidth / 2;
    clickToPlayText.y = CONFIG.canvasHeight / 2;

    this.container.addChild(clickToPlayButton, clickToPlayText);
  }
}

As you can probably see it is more less the same as in the main menu besides the text that is rendered, you can of course customize it and change anything you want. Now, you need to instantiate this class in Scene:

// source/objects/Scene.js

...

export class Scene {
  constructor(stage) {
    ...

    this.gameOver = new GameOver(stage, this.changeScene.bind(this));
    ...
  }

  changeScene(scene) {
    this.stage.removeAllChildren();
    if (scene === "WORLD") {
      this.stage.addChild(this.world.container);
    } else if (scene === "MAIN_MENU") {
      this.stage.addChild(this.mainMenu.container);
    } else if (scene === "GAME_OVER") {
      this.stage.addChild(this.gameOver.container);
    }
  }
}

And because we need to create every time a new instance of a game (of course you can implement init()/destroy() methods but in this case, I prefer to just create a new instance) we don’t need to create it in a constructor – it can be instantiated in changeScene() method:

// source/objects/Scene.js

...

export class Scene {
  constructor(stage) {
    this.stage = stage;

    this.mainMenu = new MainMenu(stage, this.changeScene.bind(this));
    this.gameOver = new GameOver(stage, this.changeScene.bind(this));

    this.stage.addChild(this.mainMenu.container);
  }

  changeScene(scene) {
    this.stage.removeAllChildren();
    if (scene === "WORLD") {
      this.world = new World(this.stage, this.changeScene.bind(this));
      this.stage.addChild(this.world.container);
    } else if (scene === "MAIN_MENU") {
      this.stage.addChild(this.mainMenu.container);
    } else if (scene === "GAME_OVER") {
      this.stage.addChild(this.gameOver.container);
    }
  }
}

But in order to work properly, we need to have access to changeScene() method in World and also remove all the listeners when the game is over. If you don’t do that then you will just add the new ones (keydown and tick events with the same listeners):

// source/objects/World.js

export class World {
  constructor(stage, changeScene) {
    this.stage = stage;
    this.changeScene = changeScene;
    
    ...

    this.boundKeydown = this.handleKeydown.bind(this);
    this.tickListener = Ticker.on("tick", this.render.bind(this));
    document.addEventListener("keydown", this.boundKeydown);
  }
}

We need to attach the handleKeydown.bind(this) to another class field because in order to remove the listener you have to keep the reference to its handler – if you would do removeEventListener(“keydown”, this.handleKeydown.bind(this)) it would not work because when you use bind, you create a new function so the handler is not the same and event listener is not removed.

on() method from Ticker returns the handler (wrapped in another function, documentation here) so you can easily remove the listener using the off() method:

// source/objects/World.js

...

export class World {
  ...

  render() {
    ...

    if (!this.checkSnakeCollision()) {
      this.snake.move();
      this.drawSnake();
    } else {
      this.snake.stop();
      document.removeEventListener("keydown", this.boundKeydown);
      Ticker.off("tick", this.tickListener);
      this.changeScene("GAME_OVER");
    }
  }
}

As you can see above, I’m removing listeners by passing the references to its handlers.

And that’s it. Whole flow, main menu -> world -> game over should work correctly now. Let’s check:

Menu -> World -> Game Over flow
Menu -> World -> Game Over flow

And that’s it for the whole tutorial. Creating a snake game is really simple and, in fact, can be done in one file but I wanted to show you some ideas about creating interactive games/apps in CreateJS.

Thanks for reading and I hope you know got some understanding about the basics elements of CreateJS (or in fact EaselJS).