Most of the Nokia I used in my childhood are equipped with this little game. Recently, I had a whim to realize this game with Canvas. This article was created.

Of course, the way to achieve this is not only canvas, but also THE form of HTML + CSS. We will not discuss the pros and cons here, so let’s start quickly.

The rules of the game

The specific rules of the game should be designed before development, just as polyfill should be implemented against the specification. The rules of snake game are quite simple and summarized below:

  • At the beginning, the snake is surrounded by a wall. The snake is weak and has only one section. Around the wall is a random egg (bonus).
  • Press the direction key to start the game, eat to the egg body will be side long, at the same time will generate new eggs
  • Hitting walls and bodies ends the game

The rules above are summed up in code: we need to scope an interface, draw a snake and an egg, and make the snake move.

The basic concept

Canvas is the emerging element of HTML5. It can be used in data visualization, animation, games, image manipulation, video and other aspects. It interacts with JavaScript.

Some basic concepts are introduced to facilitate the following understanding

  • var ctx = canvas.getContext(contextType);

GetContext returns the Canvas context. ContextType specifies the context. It has options such as 2D, webGL, webGL2, and bitmapRenderer.

You can determine whether the browser supports canvas elements based on the presence of Canvas. GetContext

  • CanvasRenderingContext2D.clearRect

Specify that all pixels in the rectangular area become transparent. Note that this method does not completely clear the canvas. For some path methods remember to manually closePath().

  • How animation works

Next we need to get the snake to move, but the idea of moving is a false proposition. Think about how TV and movies make us feel moving. It is a visual effect produced by a sequence of frames played quickly.

In the end, the snake moves through the timer to make the canvas constantly redraw to achieve the effect of animation.

Implementation approach

As shown in the sample picture on the front page of this article, the snake can be imagined as a coordinate, from which it is easy to get the following revelation:

  • Snake and egg both appear in coordinates, but not beyond
  • Hitting the boundary of coordinates is hitting the wall
  • Every time the snake moves, it needs to wipe down its path
  • Judge whether it hit the body and eat not eat the egg, judge what is the coordinate value of the movement

That is to say, the core of snake is the concept of coordinates. In order to quickly get information about coordinates, we expect to store data in the following form.

interface CoordinateAll {
  [x: number]: {
    [y: number]: 'block' | 'egg' | 'sanke';
  };
}
// Convert to JavaScript representation
{
  1: {1: 'block'.2: 'egg'.'block'},
  2: {1: 'block'.2: 'block'.'block'},
  // ...
}
Copy the code

To facilitate subsequent use, define two files in advance

// utils.js
// Pass an integer to loop it
export const eachNumber = (n, fn) = > {
  for (let i = 0; i < n; i++) { fn(i); }};// Return random integer
export const randomNumber = (min, max) = > {
  return Math.floor(Math.random() * (max - min + 1)) + min;
};
Copy the code
// const.js
/ / canvas width
export const WIDTH = 400;
/ / height
export const HEIGHT = 400;
/ / pixel x
export const PIXEL_X = 20;
Y / / pixel
export const PIXEL_Y = 20;
/ / line
export const ROW = HEIGHT / PIXEL_Y;
/ / column
export const COLUMN = WIDTH / PIXEL_X;
/ / radius
export const RADIUS = 10;
// Timer interval
export const INTERVAL = 150;
// Egg color
export const EGG_COLOR = '# 767803';
/ / the snake color
export const SANKE_COLOR = '#6f5f06';
// Border color
export const BORDER_COLOR = '#796e09';
Copy the code

I’ll start drawing the initial interface.

Initialization interface

class Sanke {
  constructor() {
    const canvas = document.createElement('canvas');
    canvas.width = WIDTH;
    canvas.height = HEIGHT;
    this.canvas = canvas;
    document.body.appendChild(canvas);
    const ctx = canvas.getContext('2d');
    this.ctx = ctx;

    / / snake
    this.currentSanke = [];
    // Where the egg appears
    this.currentEgg = null;
    / / coordinate system
    this.coordinateAll = {};
    this.init();
  }
  init() {
    // Create the coordinate system
    this.coordinateAll = {};
    eachNumber(ROW, (x) = > {
      this.coordinateAll[x] = {};
      eachNumber(COLUMN, (y) = > {
        this.coordinateAll[x][y] = 'block';
      });
    });
    const sanke = this.randomCoordinate();
    this.setCoordinate(sanke, 'sanke');
    this.currentSanke = [sanke];
    this.currentEgg = this.randomCoordinate();
    this.setCoordinate(this.currentEgg, 'egg');
    this.repaint();
  }
  repaint() {
    const { ctx, canvas, currentSanke } = this;
    // Draw egg and sanke
    ctx.clearRect(0.0, canvas.width, canvas.height);
    ctx.strokeStyle = BORDER_COLOR;
    ctx.strokeRect(0.0, canvas.width, canvas.height);
    ctx.fillStyle = SANKE_COLOR;
    currentSanke.forEach((item) = > {
      ctx.fillRect(item.x * PIXEL_X, item.y * PIXEL_Y, PIXEL_X, PIXEL_Y);
    });
    / / draw an egg
    ctx.beginPath();
    ctx.fillStyle = EGG_COLOR;
    if (!this.currentEgg) {
      this.currentEgg = this.randomCoordinate();
      this.setCoordinate(this.currentEgg, 'egg');
    }
    const { currentEgg } = this;
    ctx.arc(
      0 + RADIUS + currentEgg.x * PIXEL_X,
      0 + RADIUS + currentEgg.y * PIXEL_Y,
      RADIUS,
      0.Math.PI * 2.true
    );
    ctx.fill();
    ctx.closePath();
  }

  randomCoordinate() {
    const arr = [];
    eachNumber(ROW, (x) = > {
      eachNumber(COLUMN, (y) = > {
        const value = this.coordinateAll[x][y];
        if (value === 'block') { arr.push({ x, y }); }}); });// Indicates that no coordinates are available
    if(! arr.length) {return null;
    }
    return arr[randomNumber(0, arr.length)];
  }
  setCoordinate({ x, y }, value) {
    this.coordinateAll[x][y] = value;
  }
  getCoordinate({ x, y }) {
    if (x < 0 || y < 0 || x > ROW - 1 || y > COLUMN - 1) {
      return null;
    }
    return this.coordinateAll[x][y]; }}Copy the code

The code looks like a lot of code, but I’ve already introduced the idea of implementation, so let’s analyze the implementation step by step:

  • constructor

This is to create a Canvas object and bind some necessary properties on this, such as coordinate system, egg position and Sanke position. CurrentSanke Array is because the snake has multiple sections, and it is convenient to use array management.

  • init

Use init to complete assembly, the first step is to create coordinate system, here are all the block on the initial element, it is a total of three values: ‘block’ | ‘egg’ | ‘sanke’. The position of the egg and the position of the snake are randomly obtained and stored in the coordinateAll. Finally, repaint is called to complete the drawing

  • repaint

This step is to call Canvas to complete the interface drawing, mainly using two apis

Ctx. arc and ctx.fillRect, the former draws the arc and the latter fills the rectangle, clearRect first and then draws because this method is called repeatedly later, Some randomCoordinate, setCoordinate, and getCoordinate are all related to coordinate methods, so it’s a little bit easier to look at.

So now we have a static effect, and then we have to animate it.

move

To get the snake to move, we need to do two things:

  • Listen for arrow keys, the direction of movement (also used to start the game)
  • Use the timer to constantly redraw the position of the snake and egg to create visual movement

Listening for the arrow keys is easy. We just listen for the keyDown on the body. This event is triggered when the keyboard is pressed

this.keydownFn = (e) = > {
  const code = e.code;
  const result = ['ArrowDown'.'ArrowUp'.'ArrowLeft'.'ArrowRight'].indexOf(
    code
  );
  if (result <= -1) {
    return;
  }
  this.direction = code;
};
monitor() {
  document.body.addEventListener('keydown'.this.keydownFn);
};
Copy the code
move() {
  if (this.timeId) {
    clearInterval(this.timeId);
  }
  this.timeId = setInterval(() = > {
    const { direction, currentSanke, currentEgg } = this;
    // Return if arrow keys do not exist
    if(! direction) {return;
    }
    let nextCoordinate;
    // The snake head is always 0
    const { x, y } = currentSanke[0];
    switch (direction) {
      case 'ArrowDown':
      case 'ArrowUp':
        nextCoordinate = { x, y: direction === 'ArrowUp' ? y - 1 : y + 1 };
        break;
      case 'ArrowLeft':
      case 'ArrowRight':
        nextCoordinate = { y, x: direction === 'ArrowLeft' ? x - 1 : x + 1 };
        break;
      default:
        break;
    }
    // What nextCoordinate does is determine what type of next step
    const value = this.getCoordinate(nextCoordinate);
    switch (value) {
      case 'block':
        this.setCoordinate(currentSanke.pop(), 'block');
        currentSanke.unshift(nextCoordinate);
        this.setCoordinate(nextCoordinate, 'sanke');
        break;
      case 'egg':
        this.setCoordinate(currentEgg, 'sanke');
        this.currentEgg = null;
        currentSanke.unshift(nextCoordinate);
        break;
      case 'sanke':
      case null:
        // Game over
        break;
      default:
        break;
    }
    this.repaint();
  }, INTERVAL);
};
Copy the code

And then you insert these two methods in init

init() {
  // ...
  this.monitor();
  this.move();
}
Copy the code

At this point, snake also implemented the requirement to move, in swtich determine the type of value to determine how the coordinate value changes, and then proceed to repaint the interface

End of the game

Move only comments the end of the game, and replace case null: with this.end

end() {
  if (this.timeId) {
    clearInterval(this.timeId);
  }
  this.timeId = null;
  document.body.removeEventListener('keydown'.this.keydownFn);
  alert('Game over, current score:The ${(this.currentSanke.length - 1) * 10}`);
};
Copy the code

The last

If there is any mistake, please don’t hesitate to give advice, to help you can also start.