Shoubashoujiao school h5 games - Snake

Simple little game production, the amount of code that only two or three hundred lines. Games are free to expand extension application.

Source code has been posted to github, like the point of a small star, source entrance: Game-Snake

The game has been released, game entrance: http://snake.game.yanjd.top

The first step - making ideas

How to realize the game is the first thought, here I was thinking as follows:

  1. Use canvas to draw the map (plaid dress).
  2. Use canvas to draw the snake, is occupied by map grid. Let the snake to move, namely: update snake coordinates redrawn.
  3. Create four directional buttons, control the direction of the next snake.
  4. Fruit randomly drawn on the map, when the mobile snake to "eat" into the fruit, increasing the length, and "moving speed."
  5. Start and end keys configuration, the score display, history

The second step - the selection frame

Seen from the first step, I want to achieve in this game, only need to use canvas to draw on it, not what physics engine, there is no advanced UI effects. You may be selected from a simple point, the drawing canvas for convenient operation. After election is carefully selected EaselJS , is relatively light, for drawing canvas, the canvas and dynamic effects.

The third step - Development

ready

Catalog and document preparation:

| - index.html

| - js

| - | - main.js

| - css

| - | - stylesheet.css

index.html import-related dependencies and style files and script files. Design is 80% of the screen height of canvas drawing area, 20% of the height of the action bar and show the score area.

<!DOCTYPE html>
<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>贪吃蛇</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>
  <!-- 载入jquery 方便dom操作 -->
  <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  <!-- sweetalert 美化alert用的 -->
  <script src="https://cdn.bootcss.com/sweetalert/2.1.2/sweetalert.min.js"></script>
  <script src="js/main.js"></script>
</body>

</html>

stylesheet.css

* {
  padding: 0;
  margin: 0;
}
body {
  position: fixed;
  width: 100%;
  height: 100%;
}
#app {
  max-width: 768px;
  margin-left: auto;
  margin-right: auto;
}
/* canvas绘制区域 */
.content-canvas {
  width: 100%;
  max-width: 768px;
  height: 80%;
  position: fixed;
  overflow: hidden;
}
.content-canvas canvas {
  position: absolute;
  width: 100%;
  height: 100%;
}
/* 操作区域 */
.control {
  position: fixed;
  width: 100%;
  max-width: 768px;
  height: 20%;
  bottom: 0;
  background-color: #aeff5d;
}

main.js

$(function() {
  // 主代码编写区域
})

1. Draw grid

Note points (problems encountered and solutions):

  1. canvas to route is no width, but the width of the line is. For example: Starting from (0, 0) to (0, 100) to draw a line width of 10px, the half of the lines are not visible outside the region. Offset processing scheme is the starting point, for example: from (0, 0) to (0, 100) to draw a line width of 10px, changed from (5,0) to (5,100), the offset is half the width of the line .
  2. Is stretched by the width and height of the canvas coordinate style definition, the processing solution is provided to the canvas element width and height properties, its current actual value is high width.

Code

main.js

$(function () {
  var LINE_WIDTH = 1 // 线条宽度
  var LINE_MAX_NUM = 32 // 一行格子数量
  var canvasHeight = $('canvas').height() // 获取canvas的高度
  var canvasWidth = $('canvas').width() // 获取canvas的宽度
  var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // 格子宽度,按一行32个格子计算
  var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // 计算横向和纵向多少个格子,即:横坐标的最大值和纵坐标的最大值

  /**
 * 绘制格子地图
 * @param graphics
 */
  function drawGrid(graphics) {
    var wNum = num.w
    var hNum = num.h
    graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
    // 画横向的线条
    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)
    // 画纵向的线条
    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) // 给canvas设置宽高属性赋值上当前canvas的宽度和高度(单用样式配置宽高会被拉伸)
    $('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()
})

Renderings

Open the browser index.html, you can see the results:

2. Draw a snake

Snake can be imagined as a series of point coordinates (array), "moved" to add a new coordinate head in the array, remove coordinates tail. Similar queues, FIFO.

Code

main.js

$(function () {
  var LINE_WIDTH = 1 // 线条宽度
  var LINE_MAX_NUM = 32 // 一行格子数量
  var SNAKE_START_POINT = [[0, 3], [1, 3], [2, 3], [3, 3]] // 初始蛇坐标
  var DIR_ENUM = { UP: 1, DOWN: -1, LEFT: 2, RIGHT: -2 }    // 移动的四个方向枚举值,两个对立方向相加等于0
  var GAME_STATE_ENUM = { END: 1, READY: 2 } // 游戏状态枚举
  var canvasHeight = $('canvas').height() // 获取canvas的高度
  var canvasWidth = $('canvas').width() // 获取canvas的宽度
  var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // 格子宽度,按一行32个格子计算
  var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // 计算横向和纵向多少个格子,即:横坐标的最大值和纵坐标的最大值
  var directionNow = null // 当前移动移动方向
  var directionNext = null // 下一步移动方向
  var gameState = null // 游戏状态

  /**
 * 绘制格子地图
 * @param graphics
 */
  function drawGrid(graphics) {
    var wNum = num.w
    var hNum = num.h
    graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
    // 画横向的线条
    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)
    // 画纵向的线条
    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 Point(x, y) {
    this.x = x
    this.y = y
  }

  /**
   * 根据移动的方向,获取当前坐标的下一个坐标
   * @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
  }

  /**
 * 初始化蛇的坐标
 * @returns {[Point,Point,Point,Point,Point ...]}
 * @private
 */
  function initSnake() {
    return SNAKE_START_POINT.map(function (item) {
      return new Point(item[0], item[1])
    })
  }

  /**
   * 绘制蛇
   * @param graphics
   * @param snakes // 蛇坐标
   */
  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)
    }
  }

  /**
 * 改变蛇身坐标
 * @param snakes 蛇坐标集
 * @param direction 方向
 */
  function updateSnake(snakes, direction) {
    var oldHead = snakes[snakes.length - 1]
    var newHead = oldHead.nextPoint(direction)
    // 超出边界 游戏结束
    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) { // ‘吃’到自己 游戏结束
      return newHead.x === p.x && newHead.y === p.y
    })) {
      gameState = GAME_STATE_ENUM.END
    } else {
      snakes.push(newHead)
      snakes.shift()
    }
  }

  /**
   * 引擎
   * @param graphics
   * @param snakes
   */
  function move(graphics, snakes, stage) {
    clearTimeout(window._engine) // 重启时关停之前的引擎
    run()
    function run() {
      directionNow = directionNext
      updateSnake(snakes, directionNow) // 更新蛇坐标
      if (gameState === GAME_STATE_ENUM.END) {
        end()
      } else {
        drawSnake(graphics, snakes)
        stage.update()
        window._engine = setTimeout(run, 500)
      }
    }
  }

  /**
   * 游戏结束回调
   */
  function end() {
    console.log('游戏结束')
  }

  function init() {
    $('canvas').attr('width', canvasWidth) // 给canvas设置宽高属性赋值上当前canvas的宽度和高度(单用样式配置宽高会被拉伸)
    $('canvas').attr('height', canvasHeight)
    directionNow = directionNext = DIR_ENUM.DOWN // 初始化蛇的移动方向
    var snakes = initSnake()
    var stage = new createjs.Stage($('canvas')[0])
    var grid = new createjs.Shape()
    var snake = new createjs.Shape()
    drawGrid(grid.graphics) // 绘制格子
    drawSnake(snake.graphics, snakes)
    stage.addChild(grid)
    stage.addChild(snake)
    stage.update()
    move(snake.graphics, snakes, stage)
  }

  init()
})

Renderings

FIG effect (gif):

3. Move the snake

Produced four buttons control the direction of movement

Code

index.html

...
<div class="control">
  <div class="row">
    <div class="btn">
      <button id="UpBtn">上</button>
    </div>
  </div>
  <div class="row clearfix">
    <div class="btn half-width left">
      <button id="LeftBtn">左</button>
    </div>
    <div class="btn half-width right">
      <button id="RightBtn">右</button>
    </div>
  </div>
  <div class="row">
    <div class="btn">
      <button id="DownBtn">下</button>
    </div>
  </div>
  </div>
</div>
...

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

...
/**
 * 改变蛇行进方向
 * @param dir
 */
function changeDirection(dir) {
  /* 逆向及同向则不改变 */
  if (directionNow + dir === 0 || directionNow === dir) return
  directionNext = dir
}

/**
 * 绑定相关元素点击事件
 */
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()
  ...
}

Renderings

FIG effect (gif):

4. Draw fruit

Two randomly selected coordinate points plotted fruit, it is determined if the "get" the tail is not deleted. Shorten the interval timer to increase the difficulty.

Note points (problems encountered and solutions): Add a fruit can not occupy the coordinates of the snake, it is considered the beginning of a randomly generated coordinates, if the coordinates already occupied, it continues to generate random coordinates. This has then found a problem entire interface is available when the remaining two coordinates (in extreme cases, accounting for the entire screen snake he sent two of the grid), and that this is the case, stop randomly selected coordinates to get to these last two to coordinate consume a lot of time. Later changed method, statistics to all coordinates, then loop snake coordinate, coordinate excluded one unavailable, then one of the available random coordinates.

Code

main.js

$(function () {
  var LINE_WIDTH = 1 // 线条宽度
  var LINE_MAX_NUM = 32 // 一行格子数量
  var SNAKE_START_POINT = [[0, 3], [1, 3], [2, 3], [3, 3]] // 初始蛇坐标
  var DIR_ENUM = { UP: 1, DOWN: -1, LEFT: 2, RIGHT: -2 }    // 移动的四个方向枚举值,两个对立方向相加等于0
  var GAME_STATE_ENUM = { END: 1, READY: 2 } // 游戏状态枚举
  var canvasHeight = $('canvas').height() // 获取canvas的高度
  var canvasWidth = $('canvas').width() // 获取canvas的宽度
  var gridWidth = (canvasWidth - LINE_WIDTH) / LINE_MAX_NUM // 格子宽度,按一行32个格子计算
  var num = { w: LINE_MAX_NUM, h: Math.floor((canvasHeight - LINE_WIDTH) / gridWidth) } // 计算横向和纵向多少个格子,即:横坐标的最大值和纵坐标的最大值
  var directionNow = null // 当前移动移动方向
  var directionNext = null // 下一步移动方向
  var gameState = null // 游戏状态
  var scope = 0 // 分数

  /**
 * 绘制格子地图
 * @param graphics
 */
  function drawGrid(graphics) {
    var wNum = num.w
    var hNum = num.h
    graphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')
    // 画横向的线条
    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)
    // 画纵向的线条
    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 Point(x, y) {
    this.x = x
    this.y = y
  }

  /**
   * 根据移动的方向,获取当前坐标的下一个坐标
   * @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
  }

  /**
 * 初始化蛇的坐标
 * @returns {[Point,Point,Point,Point,Point ...]}
 * @private
 */
  function initSnake() {
    return SNAKE_START_POINT.map(function (item) {
      return new Point(item[0], item[1])
    })
  }

  /**
   * 绘制蛇
   * @param graphics
   * @param snakes // 蛇坐标
   */
  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)
    }
  }

  /**
 * 改变蛇身坐标
 * @param snakes 蛇坐标集
 * @param direction 方向
 */
  function updateSnake(snakes, fruits, direction, fruitGraphics) {
    var oldHead = snakes[snakes.length - 1]
    var newHead = oldHead.nextPoint(direction)
    // 超出边界 游戏结束
    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) { // ‘吃’到自己 游戏结束
      return newHead.x === p.x && newHead.y === p.y
    })) {
      gameState = GAME_STATE_ENUM.END
    } else if (fruits.some(function (p) { // ‘吃’到水果
      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()
    }
  }

  /**
   * 引擎
   * @param graphics
   * @param snakes
   */
  function move(snakeGraphics, fruitGraphics, snakes, fruits, stage) {
    clearTimeout(window._engine) // 重启时关停之前的引擎
    run()
    function run() {
      directionNow = directionNext
      updateSnake(snakes, fruits, directionNow, fruitGraphics) // 更新蛇坐标
      if (gameState === GAME_STATE_ENUM.END) {
        end()
      } else {
        drawSnake(snakeGraphics, snakes)
        stage.update()
        window._engine = setTimeout(run, 500 * Math.pow(0.9, scope))
      }
    }
  }

  /**
   * 游戏结束回调
   */
  function end() {
    console.log('游戏结束')
  }

  /**
   * 改变蛇行进方向
   * @param dir
   */
  function changeDirection(dir) {
    /* 逆向及同向则不改变 */
    if (directionNow + dir === 0 || directionNow === dir) return
    directionNext = dir
  }

  /**
   * 绑定相关元素点击事件
   */
  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) })
  }

  /**
 * 创建水果坐标
 * @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
    }
  }

  /**
 * 绘制水果
 * @param graphics
 * @param fruits 水果坐标集
 */
  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) // 给canvas设置宽高属性赋值上当前canvas的宽度和高度(单用样式配置宽高会被拉伸)
    $('canvas').attr('height', canvasHeight)
    directionNow = directionNext = DIR_ENUM.DOWN // 初始化蛇的移动方向
    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) // 绘制格子
    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()
})

Renderings

FIG effect (gif):

The scores show, the game is over tips, rankings

This part is relatively simple, you can display the next data processing. This part of the code does not show out.

Renderings

Epilogue

Interface is rough, the main learning logical operations. Among some minor problems, but are all solved. createjs this game engine is quite easy to learn, only a whole api graphics rendering.

Guess you like

Origin www.cnblogs.com/jundong/p/11963501.html