Simple small game production, only two or three hundred lines of code. The game can be extended by itself.

Source code has been released to Github, like a little star, source entry: game- Snake

The game has been released, the game entrance: Snake. Game. Yanjd.top

Step 1 – Create an idea

How the game will be implemented is the first thing to think about, and here are my thoughts:

  1. Use canvas to draw maps (grid).
  2. Using canvas to draw snake is occupying map grid. Make the snake move, i.e. update the snake coordinates and redraw.
  3. Create four direction buttons that control the next direction of the snake.
  4. Random fruit is drawn on the map, and snakes “eat” fruit as they move, increasing their length and “speed”.
  5. Start key and end key configuration, score display, history records

Step 2 – Frame selection

From the first step, I want to achieve this game, only need to use canvas drawing, no physics engine, no advanced UI effects. You can choose a simpler one to facilitate the operation of canvas drawing. EaselJS is selected after careful selection, which is lightweight and used to draw canvas and dynamic effect of canvas.

Step 3 – Development

To prepare

Directories and files:

| – index.html

| – js

| – | – main.js

| – css

| – | – stylesheet.css

Index.html imports the associated dependencies, as well as style files and script files. The design is that 80% of the screen height is canvas drawing area, 20% height is action bar and display score area.


      
<html lang="zh">

<head>
  <meta charset="UTF-8">
  <meta name="viewport"
    content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible"
    content="ie=edge">
  <title>snake</title>
  <link rel="stylesheet" href="css/stylesheet.css">
  <meta name="viewport"
    content="Width =device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal- UI">
</head>

<body>
  <div id="app">
    <div class="content-canvas">
      <canvas></canvas>
    </div>
    <div class="control">
    </div>
  </div>
  <script src="https://cdn.bootcss.com/EaselJS/1.0.2/easeljs.min.js"></script>
  <! Load jquery for DOM manipulation -->
  <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  <! -- Sweetalert
  <script src="https://cdn.bootcss.com/sweetalert/2.1.2/sweetalert.min.js"></script>
  <script src="js/main.js"></script>
</body>

</html>
Copy the code

stylesheet.css

* {
  padding: 0;
  margin: 0;
}
body {
  position: fixed;
  width: 100%;
  height: 100%;
}
#app {
  max-width: 768px;
  margin-left: auto;
  margin-right: auto;
}
/* Canvas drawing area */
.content-canvas {
  width: 100%;
  max-width: 768px;
  height: 80%;
  position: fixed;
  overflow: hidden;
}
.content-canvas canvas {
  position: absolute;
  width: 100%;
  height: 100%;
}
/* Operation area */
.control {
  position: fixed;
  width: 100%;
  max-width: 768px;
  height: 20%;
  bottom: 0;
  background-color: #aeff5d;
}
Copy the code

main.js

$(function() {
  // Main coding area
})
Copy the code

1. Draw a grid

Points to note (problems encountered and solutions) :

  1. The line drawn by canvas has no width, but the line has width. For example, if you draw a 10px line from (0, 0) to (0, 100), half of the line will be invisible outside the area. For example, draw a line with a width of 10px from (0, 0) to (0, 100). Instead, draw a line from (5,0) to (5,100) with an offset of half the line width.
  2. The width and height coordinate of canvas defined with the style will be stretched. The solution is to set the width and height attribute of canvas element, and the value is its current actual width and height.

code

main.js

$(function () {
  var LINE_WIDTH = 1 // Line width
  var LINE_MAX_NUM = 32 // The number of rows
  var canvasHeight = $('canvas').height() // Get the canvas height
  var canvasWidth = $('canvas').width() // Get the canvas width
  var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // The width of the grid is calculated by 32 grids in a row
  var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // Calculate the number of horizontal and vertical grids, i.e., the maximum abscissa and the maximum ordinate

  /** * Draw a grid map * @param graphics */
  function drawGrid(graphics) {
    var wNum = num.w
    var hNum = num.h
    graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
    // Draw horizontal lines
    for (var i = 0; i <= hNum; i++) {
      if (i === hNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
      if (i === 1) graphics.setStrokeStyle(0.1)
      graphics.moveTo(LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
        .lineTo(gridWidth * wNum + LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
    }
    graphics.setStrokeStyle(LINE_WIDTH)
    // Draw vertical lines
    for (i = 0; i <= wNum; i++) {
      if (i === wNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
      if (i === 1) graphics.setStrokeStyle(1.)
      graphics.moveTo(i * gridWidth + LINE_WIDTH / 2, LINE_WIDTH / 2)
        .lineTo(i * gridWidth + LINE_WIDTH / 2, gridWidth * hNum + LINE_WIDTH / 2)}}function init() {$('canvas').attr('width', canvasWidth) // Assign the width and height of the current canvas to the canvas property.
    $('canvas').attr('height', canvasHeight)
    var stage = new createjs.Stage($('canvas') [0])
    var grid = new createjs.Shape()
    drawGrid(grid.graphics)
    stage.addChild(grid)
    stage.update()
  }

  init()
})
Copy the code

rendering

When your browser opens index.html, you can see:

2. Draw a snake

The snake can be thought of as a series of coordinate points (arrays), adding new coordinates to the head of the array “as it moves” and removing the tail coordinates. It’s like a queue, first in, first out.

code

main.js

$(function () {
  var LINE_WIDTH = 1 // Line width
  var LINE_MAX_NUM = 32 // The number of rows
  var SNAKE_START_POINT = [[0.3], [1.3], [2.3], [3.3]] // Initial snake coordinates
  var DIR_ENUM = { UP: 1.DOWN: - 1.LEFT: 2.RIGHT: 2 - }    // Enumerates the four directions of movement, the sum of the two opposing directions equals 0
  var GAME_STATE_ENUM = { END: 1.READY: 2 } // Game state enumeration
  var canvasHeight = $('canvas').height() // Get the canvas height
  var canvasWidth = $('canvas').width() // Get the canvas width
  var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // The width of the grid is calculated by 32 grids in a row
  var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // Calculate the number of horizontal and vertical grids, i.e., the maximum abscissa and the maximum ordinate
  var directionNow = null // The current movement direction
  var directionNext = null // Next move direction
  var gameState = null // Game state

  /** * Draw a grid map * @param graphics */
  function drawGrid(graphics) {
    var wNum = num.w
    var hNum = num.h
    graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
    // Draw horizontal lines
    for (var i = 0; i <= hNum; i++) {
      if (i === hNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
      if (i === 1) graphics.setStrokeStyle(0.1)
      graphics.moveTo(LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
        .lineTo(gridWidth * wNum + LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
    }
    graphics.setStrokeStyle(LINE_WIDTH)
    // Draw vertical lines
    for (i = 0; i <= wNum; i++) {
      if (i === wNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
      if (i === 1) graphics.setStrokeStyle(1.)
      graphics.moveTo(i * gridWidth + LINE_WIDTH / 2, LINE_WIDTH / 2)
        .lineTo(i * gridWidth + LINE_WIDTH / 2, gridWidth * hNum + LINE_WIDTH / 2)}}/** * coordinate class */
  function Point(x, y) {
    this.x = x
    this.y = y
  }

  /** * get the next coordinate of the current coordinate based on the direction of movement * @param direction Direction of movement */
  Point.prototype.nextPoint = function nextPoint(direction) {
    debugger
    var point = new Point(this.x, this.y)
    switch (direction) {
      case DIR_ENUM.UP:
        point.y -= 1
        break
      case DIR_ENUM.DOWN:
        point.y += 1
        break
      case DIR_ENUM.LEFT:
        point.x -= 1
        break
      case DIR_ENUM.RIGHT:
        point.x += 1
        break
    }
    return point
  }

  Initialization / * * * * @ returns the coordinates of the snake {[Point, Point, Point, Point, Point... } * @private */
  function initSnake() {
    return SNAKE_START_POINT.map(function (item) {
      return new Point(item[0], item[1])})}/** * Snake * @param graphics * @param snakes // Snake coordinates */
  function drawSnake(graphics, snakes) {
    graphics.clear()
    graphics.beginFill("#a088ff")
    var len = snakes.length
    for (var i = 0; i < len; i++) {
      if (i === len - 1) graphics.beginFill("#ff6ff9")
      graphics.drawRect(
        snakes[i].x * gridWidth + LINE_WIDTH / 2,
        snakes[i].y * gridWidth + LINE_WIDTH / 2,
        gridWidth, gridWidth)
    }
  }

  /** * change the snake coordinate * @param direction */
  function updateSnake(snakes, direction) {
    var oldHead = snakes[snakes.length - 1]
    var newHead = oldHead.nextPoint(direction)
    // Out of bounds game over
    if (newHead.x < 0 || newHead.x >= num.w || newHead.y < 0 || newHead.y >= num.h) {
      gameState = GAME_STATE_ENUM.END
    } else if (snakes.some(function (p) { // eat until your game is over
      return newHead.x === p.x && newHead.y === p.y
    })) {
      gameState = GAME_STATE_ENUM.END
    } else {
      snakes.push(newHead)
      snakes.shift()
    }
  }

  /** * engine * @param graphics * @param snakes */
  function move(graphics, snakes, stage) {
    clearTimeout(window._engine) // Shut down the previous engine during restart
    run()
    function run() {
      directionNow = directionNext
      updateSnake(snakes, directionNow) // Update the snake coordinates
      if (gameState === GAME_STATE_ENUM.END) {
        end()
      } else {
        drawSnake(graphics, snakes)
        stage.update()
        window._engine = setTimeout(run, 500)}}}/** * Game end callback */
  function end() {
    console.log('Game over')}function init() {$('canvas').attr('width', canvasWidth) // Assign the width and height of the current canvas to the canvas property.
    $('canvas').attr('height', canvasHeight)
    directionNow = directionNext = DIR_ENUM.DOWN // Initializes the snake's movement direction
    var snakes = initSnake()
    var stage = new createjs.Stage($('canvas') [0])
    var grid = new createjs.Shape()
    var snake = new createjs.Shape()
    drawGrid(grid.graphics) // Draw the grid
    drawSnake(snake.graphics, snakes)
    stage.addChild(grid)
    stage.addChild(snake)
    stage.update()
    move(snake.graphics, snakes, stage)
  }

  init()
})
Copy the code

rendering

Rendering (GIF) :

3. Move the snake

Make 4 buttons to control the direction of movement

code

index.html

.<div class="control">
  <div class="row">
    <div class="btn">
      <button id="UpBtn">on</button>
    </div>
  </div>
  <div class="row clearfix">
    <div class="btn half-width left">
      <button id="LeftBtn">On the left</button>
    </div>
    <div class="btn half-width right">
      <button id="RightBtn">right</button>
    </div>
  </div>
  <div class="row">
    <div class="btn">
      <button id="DownBtn">Under the</button>
    </div>
  </div>
  </div>
</div>.Copy the code

stylesheet.css

..control .row {
  position: relative;
  height: 33%;
  text-align: center;
}

.control .btn {
  box-sizing: border-box;
  height: 100%;
  padding: 4px;
}

.control button {
  display: inline-block;
  height: 100%;
  background-color: white;
  border: none;
  padding: 3px 20px;
  border-radius: 3px;
}

.half-width {
  width: 50%;
}

.btn.left {
  padding-right: 20px;
  float: left;
  text-align: right;
}

.btn.right {
  padding-left: 20px;
  float: right;
  text-align: left;
}

.clearfix:after {
  content: ' ';
  display: block;
  clear: both;
}
Copy the code

mian.js

./** * change the direction of the snake * @param dir */
function changeDirection(dir) {
  /* In the same direction */
  if (directionNow + dir === 0 || directionNow === dir) return
  directionNext = dir
}

/** * bind the related element to the event */
function bindEvent() {$('#UpBtn').click(function () { changeDirection(DIR_ENUM.UP) })
  $('#LeftBtn').click(function () { changeDirection(DIR_ENUM.LEFT) })
  $('#RightBtn').click(function () { changeDirection(DIR_ENUM.RIGHT) })
  $('#DownBtn').click(function () { changeDirection(DIR_ENUM.DOWN) })
}

function init() {
  bindEvent()
  ...
}
Copy the code

rendering

Rendering (GIF) :

4. Paint the fruit

Randomly take two coordinate points to draw fruit, determine if “eat”, do not delete the tail. Shortening the timer interval increases the difficulty.

Points to note (problems encountered and solutions) : adding a new fruit cannot occupy the snake’s coordinates, the initial consideration is to generate a random coordinate, if the coordinates are already occupied, then continue to generate random coordinates. The problem with this, then, is that when the entire screen has two remaining coordinates available (in extreme cases, the snake takes up the entire screen only two squares away), it takes a lot of time to randomly pick up the last two coordinates. Later changed the method, first count all coordinates, and then cycle snake body coordinates, one by one to exclude the unusable coordinates, and then randomly select one of the available coordinates.

code

main.js

$(function () {
  var LINE_WIDTH = 1 // Line width
  var LINE_MAX_NUM = 32 // The number of rows
  var SNAKE_START_POINT = [[0.3], [1.3], [2.3], [3.3]] // Initial snake coordinates
  var DIR_ENUM = { UP: 1.DOWN: - 1.LEFT: 2.RIGHT: 2 - }    // Enumerates the four directions of movement, the sum of the two opposing directions equals 0
  var GAME_STATE_ENUM = { END: 1.READY: 2 } // Game state enumeration
  var canvasHeight = $('canvas').height() // Get the canvas height
  var canvasWidth = $('canvas').width() // Get the canvas width
  var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // The width of the grid is calculated by 32 grids in a row
  var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // Calculate the number of horizontal and vertical grids, i.e., the maximum abscissa and the maximum ordinate
  var directionNow = null // The current movement direction
  var directionNext = null // Next move direction
  var gameState = null // Game state
  var scope = 0 / / score

  /** * Draw a grid map * @param graphics */
  function drawGrid(graphics) {
    var wNum = num.w
    var hNum = num.h
    graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
    // Draw horizontal lines
    for (var i = 0; i <= hNum; i++) {
      if (i === hNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
      if (i === 1) graphics.setStrokeStyle(0.1)
      graphics.moveTo(LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
        .lineTo(gridWidth * wNum + LINE_WIDTH / 2, i * gridWidth + LINE_WIDTH / 2)
    }
    graphics.setStrokeStyle(LINE_WIDTH)
    // Draw vertical lines
    for (i = 0; i <= wNum; i++) {
      if (i === wNum || i === 0) graphics.setStrokeStyle(LINE_WIDTH)
      if (i === 1) graphics.setStrokeStyle(1.)
      graphics.moveTo(i * gridWidth + LINE_WIDTH / 2, LINE_WIDTH / 2)
        .lineTo(i * gridWidth + LINE_WIDTH / 2, gridWidth * hNum + LINE_WIDTH / 2)}}/** * coordinate class */
  function Point(x, y) {
    this.x = x
    this.y = y
  }

  /** * get the next coordinate of the current coordinate based on the direction of movement * @param direction Direction of movement */
  Point.prototype.nextPoint = function nextPoint(direction) {
    var point = new Point(this.x, this.y)
    switch (direction) {
      case DIR_ENUM.UP:
        point.y -= 1
        break
      case DIR_ENUM.DOWN:
        point.y += 1
        break
      case DIR_ENUM.LEFT:
        point.x -= 1
        break
      case DIR_ENUM.RIGHT:
        point.x += 1
        break
    }
    return point
  }

  Initialization / * * * * @ returns the coordinates of the snake {[Point, Point, Point, Point, Point... } * @private */
  function initSnake() {
    return SNAKE_START_POINT.map(function (item) {
      return new Point(item[0], item[1])})}/** * Snake * @param graphics * @param snakes // Snake coordinates */
  function drawSnake(graphics, snakes) {
    graphics.clear()
    graphics.beginFill("#a088ff")
    var len = snakes.length
    for (var i = 0; i < len; i++) {
      if (i === len - 1) graphics.beginFill("#ff6ff9")
      graphics.drawRect(
        snakes[i].x * gridWidth + LINE_WIDTH / 2,
        snakes[i].y * gridWidth + LINE_WIDTH / 2,
        gridWidth, gridWidth)
    }
  }

  /** * change the snake coordinate * @param direction */
  function updateSnake(snakes, fruits, direction, fruitGraphics) {
    var oldHead = snakes[snakes.length - 1]
    var newHead = oldHead.nextPoint(direction)
    // Out of bounds game over
    if (newHead.x < 0 || newHead.x >= num.w || newHead.y < 0 || newHead.y >= num.h) {
      gameState = GAME_STATE_ENUM.END
    } else if (snakes.some(function (p) { // eat until your game is over
      return newHead.x === p.x && newHead.y === p.y
    })) {
      gameState = GAME_STATE_ENUM.END
    } else if (fruits.some(function (p) { // Eat fruit
      return newHead.x === p.x && newHead.y === p.y
    })) {
      scope++
      snakes.push(newHead)
      var temp = 0
      fruits.forEach(function (p, i) {
        if (newHead.x === p.x && newHead.y === p.y) {
          temp = i
        }
      })
      fruits.splice(temp, 1)
      var newFruit = createFruit(snakes, fruits)
      if (newFruit) {
        fruits.push(newFruit)
        drawFruit(fruitGraphics, fruits)
      }
    } else {
      snakes.push(newHead)
      snakes.shift()
    }
  }

  /** * engine * @param graphics * @param snakes */
  function move(snakeGraphics, fruitGraphics, snakes, fruits, stage) {
    clearTimeout(window._engine) // Shut down the previous engine during restart
    run()
    function run() {
      directionNow = directionNext
      updateSnake(snakes, fruits, directionNow, fruitGraphics) // Update the snake coordinates
      if (gameState === GAME_STATE_ENUM.END) {
        end()
      } else {
        drawSnake(snakeGraphics, snakes)
        stage.update()
        window._engine = setTimeout(run, 500 * Math.pow(0.9, scope))
      }
    }
  }

  /** * Game end callback */
  function end() {
    console.log('Game over')}/** * change the direction of the snake * @param dir */
  function changeDirection(dir) {
    /* In the same direction */
    if (directionNow + dir === 0 || directionNow === dir) return
    directionNext = dir
  }

  /** * bind the related element to the event */
  function bindEvent() {$('#UpBtn').click(function () { changeDirection(DIR_ENUM.UP) })
    $('#LeftBtn').click(function () { changeDirection(DIR_ENUM.LEFT) })
    $('#RightBtn').click(function () { changeDirection(DIR_ENUM.RIGHT) })
    $('#DownBtn').click(function () { changeDirection(DIR_ENUM.DOWN) })
  }

  /** * Create fruit * @returns Point * @param fruits */
  function createFruit(snakes, fruits) {
    var totals = {}
    for (var x = 0; x < num.w; x++) {
      for (var y = 0; y < num.h; y++) {
        totals[x + The '-' + y] = true
      }
    }
    snakes.forEach(function (item) {
      delete totals[item.x + The '-' + item.y]
    })
    fruits.forEach(function (item) {
      delete totals[item.x + The '-' + item.y]
    })
    var keys = Object.keys(totals)
    if (keys.length) {
      var temp = Math.floor(keys.length * Math.random())
      var key = keys[temp].split(The '-')
      return new Point(Number(key[0]), Number(key[1))}else {
      return null}}/** * Draw fruit * @param graphics * @param fruits fruit coordinate set */
  function drawFruit(graphics, fruits) {
    graphics.clear()
    graphics.beginFill("#16ff16")
    for (var i = 0; i < fruits.length; i++) {
      graphics.drawRect(
        fruits[i].x * gridWidth + LINE_WIDTH / 2,
        fruits[i].y * gridWidth + LINE_WIDTH / 2,
        gridWidth, gridWidth)
    }
  }

  function init() {
    bindEvent()
    $('canvas').attr('width', canvasWidth) // Assign the width and height of the current canvas to the canvas property.
    $('canvas').attr('height', canvasHeight)
    directionNow = directionNext = DIR_ENUM.DOWN // Initializes the snake's movement direction
    var snakes = initSnake()
    var fruits = []
    fruits.push(createFruit(snakes, fruits))
    fruits.push(createFruit(snakes, fruits))
    var stage = new createjs.Stage($('canvas') [0])
    var grid = new createjs.Shape()
    var snake = new createjs.Shape()
    var fruit = new createjs.Shape()
    drawGrid(grid.graphics) // Draw the grid
    drawSnake(snake.graphics, snakes)
    drawFruit(fruit.graphics, fruits)
    stage.addChild(grid)
    stage.addChild(snake)
    stage.addChild(fruit)
    stage.update()
    move(snake.graphics, fruit.graphics, snakes, fruits, stage)
  }

  init()
})
Copy the code

rendering

Rendering (GIF) :

5. Score display, game end prompt, leaderboard

This part is relatively easy, dealing with the presentation of the data. I’m not going to show you this part of the code.

rendering

conclusion

The interface is relatively rough, mainly learning logic operation. There were a few small problems, but they all worked out. Createjs is a relatively simple game engine to learn, using only the graphics API.