Snake in CreateJS Tutorial Part 1

Hello

The snake is a really simple game that depends on moving a Snake and eating a food. Moving is restricted by a game borders and food position is randomly generated, so the player needs to move from one position to other having longer snake (each time he eats the food).

We are going to use the CreateJS library and it’s boilerplate that I’ve shown here: https://frontendgamedev.com/createjs-in-5-minutes/.

If you have any troubles with the CreateJS itself, I suggest you check the https://createjs.com/ site.

The code can be found in the repository: https://github.com/szymkab/frontendgamedev-snake.

So… let’s start.

Preparation of repository

In order to start working on the game, we need to clone the repository and run setup commands. I’ve described that process in the article mentioned above, so make sure you have done it and we can start.

Rendering a snake in a game world

Firstly, open the app.js file and remove the objects (graphics and text) that are added (line 25 to 38). Instead of sample code we are going to create a World class that will be responsible for rendering and controlling user input.

Create a World.js file in source/objects directory with class World:

// source/objects/World.js

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

The constructor takes the stage as an argument because we need a stage reference in order to render things. Moreover, in this class, we are going to listen to the user’s input (keyboard or anything you want) and react to it (rendering snake, food, etc).

I very like to have things organized so I’m going to split the data (like position, length, etc.) from the graphic representation. It may be overkill for such a small project but in general, in my opinion, it is a good way to organize the code.

Now, let’s jump into our config file and add a snakeLength (initial length of the snake) and snakeSize (size of one part of snake’s body) properties.

// source/config/index.js

export const CONFIG = {
  canvasWidth: 1000,
  canvasHeight: 1000,
  framerate: 50,
  snakeLength: 10,
  snakeSize: 40,
};

Create a SnakeHead.js and SnakePart.js (we are going to have different look for head and parts of body snake) files in source/graphics directory. Open the SnakeHead.js and write the code:

// source/graphics/SnakeHead.js

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

export class SnakeHead {
  constructor(x, y) {
    const graphics = new Graphics()
      .beginFill("#000")
      .setStrokeStyle(1)
      .beginStroke("#000000")
      .drawRoundRect(x, y, CONFIG.snakeSize, CONFIG.snakeSize, CONFIG.snakeSize / 10);
    const shape = new Shape(graphics);

    return shape;
  }
}

Do the same with the SnakePart.js but adjust the colors as you want:

// source/graphics/SnakePart.js

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

export class SnakePart {
  constructor(x, y) {
    const graphics = new Graphics()
      .beginFill("#fff")
      .setStrokeStyle(1)
      .beginStroke("#333333")
      .drawRoundRect(x, y, CONFIG.snakeSize, CONFIG.snakeSize, CONFIG.snakeSize / 10);
    const shape = new Shape(graphics);

    return shape;
  }
}

As you can see I’m importing the config and using it for size of head or snake’s body parts. The last argument of drawRoundRect method is radius.

Ok, so we have the graphics of Snake. Now let’s make a place for data. Create a Snake.js file in source/models directory:

// source/models/Snake.js

export class Snake {
  constructor() {}
}

We need an initial position of the snake. I like to set it close to the top left corner of the game’s world. In order to have it, I wrote a simple loop (based on snakeSize and snakeLength from the config) that iterates snakeLength times and just sets the X position for each part of snake’s body:

// source/models/Snake.js

import { CONFIG } from "@/config";

export class Snake {
  constructor() {
    this.position = [];

    for (let i = 0; i < CONFIG.snakeLength; i++) {
      this.position.unshift({ x: CONFIG.snakeSize + i * CONFIG.snakeSize, y: CONFIG.snakeSize * 3 });
    }
  }
}

Right now, we are ready to render the snake in the game’s world. Let’s do it.

Open the World.js file and add following code:

// source/objects/World.js

import { Container } from "@createjs/EaselJS";
import { SnakeHead } from "../graphics/SnakeHead";
import { SnakePart } from "../graphics/SnakePart";
import { Snake } from "../models/Snake";

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

    this.snakeContainer = new Container();
    this.stage.addChild(this.snakeContainer);

    this.snake = new Snake();
    this.drawSnake();
  }

  drawSnake() {
    this.snakeContainer.removeAllChildren();
    this.snake.position.forEach(({ x, y }, index) => {
      const part = index === 0 ? new SnakeHead(x, y) : new SnakePart(x, y);
      this.snakeContainer.addChild(part);
    });
    this.stage.update();
  }
}

In the constructor I’ve added the declaration of Container (every object has it’s own – we will add another one for Food later), and instance of Snake’s model to have the data.

In the drawSnake() method I’m just drawing the snake according to his coordinates stored in model. For the first coordinates I draw the head (the head has a different color) and for the other the part. After all I run stage.update() one time to see the effects:

Rendering a snake in the game's world
Rendering a snake in the game’s world

Moving the snake

It’s time to move our snake. We need to collect the user’s keyboard input in two situations – clicking arrows/wsad to change the snake’s direction and clicking space to start/pause the game. We will redirect the user’s input to certain methods that will change the direction of the snake or stop/pause the game.

Open the Snake.js file and class properties:

// source/models/Snake.js

...

export class Snake {
  constructor() {
    ...
    this.isMoving = true;
    this.direction = "RIGHT";
  }
}

And now we need the methods for manipulating the position, direction, or state (isMoving) of the snake. The manipulation will be called by the World class (later).

// source/models/Snake.js

...

export class Snake {
  ...

  get delta() {
    switch (this.direction) {
      case "RIGHT":
        return {
          x: CONFIG.snakeSize,
          y: 0,
        };
      case "LEFT":
        return {
          x: -CONFIG.snakeSize,
          y: 0,
        };
      case "UP":
        return { x: 0, y: -CONFIG.snakeSize };
      case "DOWN":
        return { x: 0, y: CONFIG.snakeSize };
      default:
        return { x: 0, y: 0 };
    }
  }

  get head() {
    return this.position[0];
  }

  changeDirection(direction) {
    this.direction = direction;
  }

  move() {
    if (this.isMoving) {
      const head = { x: this.head.x + this.delta.x, y: this.head.y + this.delta.y };
      this.position.unshift(head);
      this.position.pop();
    }
  }

  start() {
    this.isMoving = true;
  }

  stop() {
    this.isMoving = false;
  }
}

Let’s describe one by one

  • get delta() – this one returns a delta of position based on the current direction. For example, if the direction is right we are moving the snake just by its size to the right (that’s why we have positive x) and similarly with the others.
  • get head() – this one is just for not writing this.position[0] each time, you can skip it but remember to change the this.head statements
  • changeDirection() – a method for letting a change of direction from the outside of the model
  • move() – this one changes the position of the snake’s head if isMoving flag is true. It is just adding the delta x and y to the current head position, then adding the head as the first element and removing the last.
  • start() – method for flag change
  • stop() – method for flag change

Having the model prepared for manipulation we can do it now in World class. Open the file and add several statements for controlling the main state (paused or not) of the game:

// source/objects/World.js

...

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

    ...
  }

  drawSnake() {...}

  pauseGame() {
    this.isPaused = true;
    this.snake.stop();
  }

  resumeGame() {
    this.isPaused = false;
    this.snake.start();
  }
}

Having those methods we can now listen for user’s input and fire them after space is clicked. Add listener method:

// source/objects/World.js

...

export class World {
  constructor(stage) {...}

  handleKeydown(event) {
    if (event.keyCode === 32) {
      if (this.isPaused) {
        this.resumeGame();
      } else {
        this.pauseGame();
      }
    }
  }

  drawSnake() {...}

  pauseGame() {...}

  resumeGame() {...}
}

Now, after clicking the space the snake will start or stop moving. We don’t see it because the movement happens only in the model. We need to render it to make sure it’s working. Add render() method:

// source/objects/World.js

...

export class World {
  constructor(stage) {...}

  handleKeydown(event) {...}

  drawSnake() {...}

  pauseGame() {...}

  resumeGame() {...}

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

    if (this.isPaused) {
      return;
    }

    this.snake.move();
    this.drawSnake();
  }
}

Render method updates the stage and fires the model’s snake.move() method to change position and drawSnake() to render the new position. To see effects we need to fire this method with every tick of gameplay. Add a listener for keyboard and CreateJS’s tick:

// source/objects/World.js

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

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

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

  handleKeydown(event) {...}

  drawSnake() {...}

  pauseGame() {...}

  resumeGame() {...}

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

    if (this.isPaused) {
      return;
    }

    this.snake.move();
    this.drawSnake();
  }
}

Check the game in the browser now and click space to see what happens:

Moving a snake in a game's world
Moving a snake in a game’s world

Snake is way too fast. To change it, you can simply change the framerate in the config file.

The snake is moving but we can’t control it by using the WSAD. Let’s add some code to handle those keys:

// source/objects/World.js

...

export class World {
  constructor(stage) {...}

  handleKeydown(event) {
    if (event.keyCode === 32) {
      if (this.isPaused) {
        this.resumeGame();
      } else {
        this.pauseGame();
      }
    } else {
      if (!this.isPaused) {
        this.changeDirection(event);
      }
    }
  }

  changeDirection(event) {
    const LEFT_KEY = 37;
    const RIGHT_KEY = 39;
    const UP_KEY = 38;
    const DOWN_KEY = 40;

    const keyPressed = event.keyCode;
    const goingUp = this.snake.direction === "UP";
    const goingDown = this.snake.direction === "DOWN";
    const goingRight = this.snake.direction === "RIGHT";
    const goingLeft = this.snake.direction === "LEFT";

    if (keyPressed === LEFT_KEY && !goingRight) {
      this.snake.changeDirection("LEFT");
    }

    if (keyPressed === UP_KEY && !goingDown) {
      this.snake.changeDirection("UP");
    }

    if (keyPressed === RIGHT_KEY && !goingLeft) {
      this.snake.changeDirection("RIGHT");
    }

    if (keyPressed === DOWN_KEY && !goingUp) {
      this.snake.changeDirection("DOWN");
    }
  }

  drawSnake() {...}

  pauseGame() {...}

  resumeGame() {...}

  render() {...}
}

So, I’ve added another instruction to if statement in the case, clicked key is not a space and the game is not paused the flow goes to changeDirection() method which checks for the direction keys (w, s, a, d) and based on that fires model’s changeDirection() method. The effect is following:

Controlling the snake's movement by keyboard
Controlling the snake’s movement by keyboard

And that’s it for the first part. In the next parts, we will cover possible collisions, eating, growing, and scoring points.

Part 2: https://frontendgamedev.com/snake-in-createjs-tutorial-part-2/