精品推荐
作者:严俊东
转载自:
https://www.cnblogs.com/jundong/p/11963501.html
第一步 - 制作想法
游戏如何实现是首要想的,这里我的想法如下:
利用canvas进行绘制地图(格子装)。
利用canvas绘制蛇,就是占用地图格子。让蛇移动,即:更新蛇坐标,重新绘制。
创建四个方向按钮,控制蛇接下来的方向。
随机在地图上绘制出果子,蛇移动时“吃”到果子,增加长度和“移速”。
开始键和结束键配置,分数显示、历史记录
第二步 - 框架选型
从第一步可知,我想实现这个游戏,只需要用到canvas绘制就可以了,没有物理引擎啥的,也没有高级的UI特效。可以选个简单点的,用来方便操作canvas绘制。精挑细选后选的是EaselJS,比较轻量,用于绘制canvas,以及canvas的动态效果。
第三步 - 开发
准备
目录和文件准备:
| - index.html
| - js
| - | - main.js
| - css
| - | - stylesheet.css
index.html 导入相关的依赖,以及样式文件和脚本文件。设计是屏幕80%高度为canvas绘制区域,20%高度是操作栏以及展示分数区域.
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>script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js">script>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.绘制格子
注意的点(遇到的问题以及解决方案):
canvas绘制的路线是无宽度的,但线条是有宽度的。好比:从(0, 0)到(0, 100)绘制一条宽度为10px的线,则线条一半是在区域外看不见的。处理方案是起点偏移,好比:从(0, 0)到(0, 100)绘制一条宽度为10px的线,改为从(5,0)到(5,100),偏移量为线条宽度的一半。
用样式定义canvas的宽高坐标会被拉伸,处理方案是给canvas元素设置宽高属性,值为它当前的实际宽高。
代码
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.wvar hNum = num.hgraphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')// 画横向的线条for (var i = 0; iif (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; iif (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()})
效果图
浏览器打开index.html,可以看到效果:

2.绘制蛇
蛇可以想象成一串坐标点(数组),“移动时”在数组头部添加新的坐标,去除尾部的坐标。类似队列,先进先出。
代码
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 } // 移动的四个方向枚举值,两个对立方向相加等于0var 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.wvar hNum = num.hgraphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')// 画横向的线条for (var i = 0; iif (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; iif (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 = xthis.y = y}/*** 根据移动的方向,获取当前坐标的下一个坐标* @param direction 移动的方向*/Point.prototype.nextPoint = function nextPoint(direction) {debuggervar point = new Point(this.x, this.y)switch (direction) {case DIR_ENUM.UP:point.y -= 1breakcase DIR_ENUM.DOWN:point.y += 1breakcase DIR_ENUM.LEFT:point.x -= 1breakcase DIR_ENUM.RIGHT:point.x += 1break}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.lengthfor (var i = 0; iif (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 = directionNextupdateSnake(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()})
效果图
效果图(gif):

3.移动蛇
制作4个按钮,控制移动方向
代码
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) returndirectionNext = 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()...}
效果图
效果图(gif):

4. 绘制果子
随机取两个坐标点绘制果子,判定如果“吃到”,则不删除尾巴。缩短定时器的时间间隔增加难度。
注意的点(遇到的问题以及解决方案):新增一个果子不能占用蛇的坐标,一开始考虑的是随机生成一个坐标,如果坐标已被占用,那就继续生成随机坐标。然后发现这样做有个问题就是整个界面剩余两个坐标可用时(极端情况,蛇占了整个屏幕就差两个格子了),那这样的话,不停随机取坐标,要取到这最后两个坐标要耗不少时间。后面改了方法,先统计所有坐标,然后循环蛇身坐标,一一排除不可用坐标,然后再随机抽取可用坐标的其中一个。
代码
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 } // 移动的四个方向枚举值,两个对立方向相加等于0var 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.wvar hNum = num.hgraphics.setStrokeStyle(LINE_WIDTH).beginStroke('#ffac52')// 画横向的线条for (var i = 0; iif (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; iif (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 = xthis.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 -= 1breakcase DIR_ENUM.DOWN:point.y += 1breakcase DIR_ENUM.LEFT:point.x -= 1breakcase DIR_ENUM.RIGHT:point.x += 1break}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.lengthfor (var i = 0; iif (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 = 0fruits.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 = directionNextupdateSnake(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) returndirectionNext = 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; xfor (var y = 0; ytotals[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; igraphics.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()})
效果图
效果图(gif):

5. 分数显示、游戏结束提示、排行榜
这一部分就比较简单了,处理下数据的展示即可。这部分代码就不展示出来了。
效果图

结语
界面比较粗糙,主要是学习逻辑操作。中间出现一些小问题,但都一一的解决了。createjs这个游戏引擎还是比较简单易学的,整体只用了绘制图形的api。