Snake in CreateJS Tutorial Part 2

This is the second part of the “Snake in CreateJS Tutorial”. Here you can check the first one: Part 1.

In this part of the tutorial, we are going to implement collisions between the snake and borders, the snake with food, and the snake’s self-collision. We are also going to implement a food generation and snake growing system.

The code can be found in the repository: Repository.

Snake limited by the game borders

In order to check the collision between the snake and borders we need to check if the position of the snake is higher or lower than canvas dimensions:

// source/objects/World.js

export class World {
  ...

  checkBordersCollision() {
    return (
      this.snake.head.x > CONFIG.canvasWidth ||
      this.snake.head.x < 0 ||
      this.snake.head.y > CONFIG.canvasHeight ||
      this.snake.head.y < 0
    );
  }

  checkSnakeCollision() {
    return this.checkBordersCollision();
  }

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

    if (this.isPaused) {
      return;
    }

    if (!this.checkSnakeCollision()) {
      this.snake.move();
      this.drawSnake();
    } else {
      this.snake.stop();
      document.removeEventListener("keydown", this.handleKeydown.bind(this));
    }
  }
}

In the game loop, we are checking if the position of the snake is not off the canvas dimensions, and if it is we are calling snake.stop() method as well as removing listeners for WSAD keys. You should have the following behavior after implementation of this method:

The collision between snake and game borders
The collision between snake and game borders

And that’s it for the game borders.

Snake’s self-collision

Now, we need to check for the self collision. Implement the following method:

// source/objects/World.js

export class World {
  ...

  checkSelfCollision() {
    for (let i = 1; i < this.snake.position.length; i++) {
      if (this.snake.position[i].x === this.snake.head.x && this.snake.position[i].y === this.snake.head.y) {
        return true;
      }
    }

    return false;
  }

  checkSnakeCollision() {
    return this.checkBordersCollision() || this.checkSelfCollision();
  }

  ...
}

This method checks if the head of the snake has the same position as any of its body parts. We just need to add an OR statement to checkSnakeCollision() method and that’s it. It should stop when the head hits the body:

Snake self collision
Snake self collision

Rendering the food

Ok, once we have the collision done, we can move on to the food feature. We need to generate food and grow up the snake every time when it eats.

Let’s create a Food class under the source/models directory:

// source/models/Food.js

import { CONFIG } from "@/config";

export class Food {
  constructor() {
    this.generateRandomPosition();
  }

  generateRandomPosition() {
    const randomXPosition = Math.floor((Math.random() * CONFIG.canvasWidth) / CONFIG.snakeSize);
    const randomYPosition = Math.floor((Math.random() * CONFIG.canvasHeight) / CONFIG.snakeSize);

    this.position = {
      x: randomXPosition * CONFIG.snakeSize,
      y: randomYPosition * CONFIG.snakeSize,
    };
  }
}

This class contains only information about the position – once it’s created the random position is assigned to a position field and after that each time the snake eats a portion of the food we generate a new position by using the generateRandomPosition() method.

Now, let’s add the graphic representation of Food, create a FoodGraphic.js file under the source/graphics directory:

// source/graphics/FoodGraphic.js

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

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

    return shape;
  }
}

Nothing special here. One thing to note that there is a lot bigger radius (last argument of the drawRoundRect() method) than in SnakeHead or SnakePart.

Once we have it done, we can use it in World class:

// source/objects/World.js

...
import { FoodGraphic } from "../graphics/FoodGraphic";
import { Food } from "../models/Food";

export class World {
  constructor(stage) {
    ...
    this.foodContainer = new Container();
    this.stage.addChild(this.snakeContainer, this.foodContainer);

    this.food = new Food();
    this.drawFood();

    ...
  }

  ...

  drawFood() {
    this.foodContainer.removeAllChildren();
    const food = new FoodGraphic(this.food.position.x, this.food.position.y);
    this.foodContainer.addChild(food);
  }
}

Effect should be following:

Rendering a food
Rendering a food

As you can see, there is a portion of food generated randomly.

Eating the portion of food by the snake

Once we have a portion of food rendered, we need to check a collision and in case Snake hits the Food, grow up the snake. Add the grow() method to the Snake model:

// source/models/Snake.js

import { CONFIG } from "@/config";

export class Snake {
  ...

  grow() {
    const snakeTail = this.position[this.position.length - 1];
    this.position.push({ x: snakeTail.x - CONFIG.snakeSize, y: snakeTail.y - CONFIG.snakeSize });
  }

  ...
}

We are just adding a new element to the end of the snake positions (once it’s added then it’s rendered by the drawSnake() method in World class).

Now, let’s check a collision between the food and the snake:

// source/objects/World.js

export class World {
  ...

  checkFoodCollision() {
    return this.snake.head.x === this.food.position.x && this.snake.head.y === this.food.position.y;
  }

  ...

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

    if (this.isPaused) {
      return;
    }

    if (this.checkFoodCollision()) {
      this.snake.grow();
      this.food.generateRandomPosition();
      this.drawFood();
    }

    if (!this.checkSnakeCollision()) {
      this.snake.move();
      this.drawSnake();
    } else {
      this.snake.stop();
      document.removeEventListener("keydown", this.handleKeydown.bind(this));
    }
  }
}

In the checkFoodCollision() method we are just checking if the position of the snake’s head is the same as food. In the render loop, we simply call the checkFoodCollision() method, and if so the snake is growing and we generate a new Food.

Let’s check an effect!

Eating food by the snake
Eating food by the snake

And that’s it for the second part. In the next part, you will find how to make a score and we are going to improve few things.