Hand teaching H5 games – greedy snake

Time:2020-3-30

Simple small game production, the amount of code is only two or three hundred lines. 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, and the game portal: http://snake.game.yanjd.top

Step 1 – create ideas

How to realize the game is the first thought. Here are my thoughts:

  1. Use canvas to map (grid).
  2. Using canvas to draw snake is to occupy map grid. Let the snake move, that is, update the snake coordinates and redraw.
  3. Create four direction buttons to control the next direction of the snake.
  4. Randomly draw the fruit on the map. When the snake moves, it will “eat” the fruit, increase the length and “move speed”.
  5. Start key and end key configuration, score display, history

Step 2 – frame selection

From the first step, we can see that I want to realize this game, only need to use canvas to draw. There is no physical engine or other advanced UI effects. You can select a simple point to facilitate the operation of canvas drawing. After careful selection, easeljs is selected, which is lighter and used to draw canvas and the dynamic effect of canvas.

Step 3 – Development

Get ready

Directory and file preparation:

| – index.html

| – js

| – | – main.js

| – css

| – | – stylesheet.css

index.htmlImport related dependencies, as well as style and script files. The design is that 80% of the screen height is canvas drawing area, 20% of the screen height is operation bar and display score area

Snake

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;
}

main.js

$(function() {
  //Main coding area
})

1. Draw grid

Points for attention (problems encountered and solutions):

  1. Canvas draws an alignment without width, but the lines have width. For example: draw a line with a width of 10px from (0, 0) to (0, 100), and half of the line is invisible outside the area. The solution is to offset the starting point. For example, draw a line with a width of 10px from (0, 0) to (0, 100), change it to (5, 0) to (5100), and the offset is half of the line width.
  2. The width and height coordinates of canvas defined by style will be stretched. The processing scheme is to set the width and height attribute to the 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 // number of cells in a row
  Var canvas height = $('canvas'). Height() // get the height of canvas
  Var canvas width = $('canvas'). Width() // get the width of canvas
  Var gridwidth = (canvaswidth - line? Width) / line? Max? Num // grid width, calculated by 32 grids in a row
  Var num = {W: line_max_num, H: math. Floor ((canvasheight - line_width) / gridwidth)} // calculates the number of cells in the horizontal and vertical direction, that is, the maximum value of the abscissa and the maximum value of the 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) // sets the width and height of the current canvas on the canvas width and height attribute assignment (width and height will be stretched by using the style configuration alone)
    $('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()
})

Design sketch

Browser openindex.html, you can see the effect:

2. draw snake

Snake can be thought of as a series of coordinate points (array). When moving, add new coordinates to the array head and remove the coordinates at the tail. Similar to queues, first in, first out.

Code

main.js

$(function () {
  Var line? Width = 1 // line width
  Var line? Max? Num = 32 // number of cells in a row
  Var snake_start_point = [[0, 3], [1, 3], [2, 3], [3, 3]] // initial snake coordinate
  Var dir Ou enum = {up: 1, down: - 1, left: 2, right: - 2} // enumeration value of four directions of movement, the sum of two opposite directions is equal to 0
  Var game? State? Enum = {end: 1, ready: 2} // game state enumeration
  Var canvas height = $('canvas'). Height() // get the height of canvas
  Var canvas width = $('canvas'). Width() // get the width of canvas
  Var gridwidth = (canvaswidth - line? Width) / line? Max? Num // grid width, calculated by 32 grids in a row
  Var num = {W: line_max_num, H: math. Floor ((canvasheight - line_width) / gridwidth)} // calculates the number of cells in the horizontal and vertical direction, that is, the maximum value of the abscissa and the maximum value of the ordinate
  Var directionnow = null // current move 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 according to the moving direction
   *@ param direction
   */
  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
  }

  /**
 *Initialize snake coordinates
 * @returns {[Point,Point,Point,Point,Point ...]}
 * @private
 */
  function initSnake() {
    return SNAKE_START_POINT.map(function (item) {
      return new Point(item[0], item[1])
    })
  }

  /**
   * draw 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 snake body coordinates
 *@ param snakes snake coordinate set
 *@ param direction
 */
  function updateSnake(snakes, direction) {
    var oldHead = snakes[snakes.length - 1]
    var newHead = oldHead.nextPoint(direction)
    //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 the end of your game
      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. U engine) // shut down the engine before restart
    run()
    function run() {
      directionNow = directionNext
      Updatesnake (snakes, directionnow) // update 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 ('end of game ')
  }

  function init() {
    $('canvas'). Attr ('width ', canvaswidth) // sets the width and height of the current canvas on the canvas width and height attribute assignment (width and height will be stretched by using the style configuration alone)
    $('canvas').attr('height', canvasHeight)
    Directionnow = directionnext = dir? Enum. Down // initializes the snake's direction of travel
    var snakes = initSnake()
    var stage = new createjs.Stage($('canvas')[0])
    var grid = new createjs.Shape()
    var snake = new createjs.Shape()
    Drawgrid (grid. Graphics) // draw grid
    drawSnake(snake.graphics, snakes)
    stage.addChild(grid)
    stage.addChild(snake)
    stage.update()
    move(snake.graphics, snakes, stage)
  }

  init()
})

Design sketch

Rendering (GIF):

3. mobile snake

Make 4 buttons to control the moving direction

Code

index.html

...

  
    
      upper
    
  
  
    
      Left
    
    
      right
    
  
  
    
      lower
    
  
  

...

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;
}

mian.js

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

/**
 *Binding related element click 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()
  ...
}

Design sketch

Rendering (GIF):

4. Draw fruit

Take two coordinate points at random to draw fruit, and judge if it is “eaten”, the tail will not be deleted. Shortening the timer interval increases the difficulty.

Attention points (problems encountered and solutions): a new fruit cannot occupy the snake’s coordinates. At the beginning, it is considered to randomly generate a coordinate. If the coordinate has been occupied, continue to generate random coordinates. Then it is found that there is a problem when the remaining two coordinates of the whole interface are available (in extreme cases, when the snake occupies the whole screen, there are two squares left). In this way, it will take a lot of time to get the last two coordinates without stopping randomly. Later, the method is changed. First, all coordinates are counted, then the snake body coordinates are cycled, one by one the unavailable coordinates are excluded, and then one of the available coordinates is randomly selected.

Code

main.js

$(function () {
  Var line? Width = 1 // line width
  Var line? Max? Num = 32 // number of cells in a row
  Var snake_start_point = [[0, 3], [1, 3], [2, 3], [3, 3]] // initial snake coordinate
  Var dir Ou enum = {up: 1, down: - 1, left: 2, right: - 2} // enumeration value of four directions of movement, the sum of two opposite directions is equal to 0
  Var game? State? Enum = {end: 1, ready: 2} // game state enumeration
  Var canvas height = $('canvas'). Height() // get the height of canvas
  Var canvas width = $('canvas'). Width() // get the width of canvas
  Var gridwidth = (canvaswidth - line? Width) / line? Max? Num // grid width, calculated by 32 grids in a row
  Var num = {W: line_max_num, H: math. Floor ((canvasheight - line_width) / gridwidth)} // calculates the number of cells in the horizontal and vertical direction, that is, the maximum value of the abscissa and the maximum value of the ordinate
  Var directionnow = null // current move 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 according to the moving direction
   *@ param direction
   */
  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
  }

  /**
 *Initialize snake coordinates
 * @returns {[Point,Point,Point,Point,Point ...]}
 * @private
 */
  function initSnake() {
    return SNAKE_START_POINT.map(function (item) {
      return new Point(item[0], item[1])
    })
  }

  /**
   * draw 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 snake body coordinates
 *@ param snakes snake coordinate set
 *@ param direction
 */
  function updateSnake(snakes, fruits, direction, fruitGraphics) {
    var oldHead = snakes[snakes.length - 1]
    var newHead = oldHead.nextPoint(direction)
    //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 the end of your game
      return newHead.x === p.x && newHead.y === p.y
    })) {
      gameState = GAME_STATE_ENUM.END
    }Else if (fruits. Some (function (P) {// 'eat' to fruits
      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. U engine) // shut down the engine before restart
    run()
    function run() {
      directionNow = directionNext
      Updatesnake (snakes, fruits, directionnow, fruit graphics) // update 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 ('end of game ')
  }

  /**
   *Change the direction of the snake
   * @param dir
   */
  function changeDirection(dir) {
    /*Reverse and same direction do not change*/
    if (directionNow + dir === 0 || directionNow === dir) return
    directionNext = dir
  }

  /**
   *Binding related element click 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 coordinates
 * @returns Point
 * @param snakes
 * @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 + '-' + y] = true
      }
    }
    snakes.forEach(function (item) {
      delete totals[item.x + '-' + item.y]
    })
    fruits.forEach(function (item) {
      delete totals[item.x + '-' + item.y]
    })
    var keys = Object.keys(totals)
    if (keys.length) {
      var temp = Math.floor(keys.length * Math.random())
      var key = keys[temp].split('-')
      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) // sets the width and height of the current canvas on the canvas width and height attribute assignment (width and height will be stretched by using the style configuration alone)
    $('canvas').attr('height', canvasHeight)
    Directionnow = directionnext = dir? Enum. Down // initializes the snake's direction of travel
    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 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()
})

Design sketch

Rendering (GIF):

5. Score display, end of game prompt, Leaderboard

This part is relatively simple, just deal with the data display. This part of the code is not shown.

Design sketch

epilogue

The interface is rough, mainly learning logic operation. There are some small problems in the middle, but they have been solved one by one. The game engine createjs is relatively simple and easy to learn. It only uses the graphics API as a whole.

Recommended Today

How many steps does it take from symfony framework to a complete project

preface aboutphpThe framework of bothyii,symfonyOr maybelaravelWe all dabble in our work. For the resource package stored in the frameworkvendorFolders, entry files(index.phpperhapsapp.php)And we all meet with them every day. But are you really familiar with these files / folders? How does a complete project develop from a pure framework? What role does each part play in […]