额。。先说几句
前阵子导师大大让我做点小项目练练手,于是就用业余时间做了个H5小游戏——贪吃蛇。。。过程中参考了凹凸实验室(划重点,里面讲解的很到位)以及谷歌上大大小小的贪吃蛇项目,最后做出了这个简化到不能再简化的贪吃蛇。。。新手新手。。大家多多见谅
先上个Demo吧,大家可以玩一下,只有最基本的功能:吃食物,然后会变长,每吃一个食物加一分,撞到自己或者撞到墙的话游戏结束。照理来说应该会越长速度越快,或者倒计时内计算得分,日后会继续完善!下面是该小游戏的二维码
源码在此~~真的是比较粗糙。。还在完善和添加注释的过程中。。都不好意思叫你们star一个了哈哈哈哈
https://github.com/easonhuang123/greedysnake
实现思路
这个项目使用了最基本的MVC设计模型,所以笔者分别从model,view,control三个层面进行分析~
Model
模型层负责管理项目中的各种数据结构,包括蛇,食物,墙和整个活动区域。
蛇snake
看到界面中那条长长的蛇之后可能凭借着直观感受你第一时间就会想到用一个数组来存放它:(11,12,13,14,15),但是想了一下这条蛇会一直移动,每移动一次所有元素都会进行调整,假如用数组来存放的话复杂度一定会很高,然后你可能就会想到用一个链表来存放它,每次移动的时候新增头节点删除尾节点,本文就是用链表来实现蛇的数据结构,但是由于JavaScript没有原生的链表结构,也没有指针一说>_<所以笔者自己写了一个双向链表的数据结构存放这条蛇(关于用JavaScript实现双向链表,可以参考[这篇文章](http://www.jianshu.com/p/298623cc2026)
继续分析蛇的行为,包括:
- 移动(新增头节点,删除尾节点)
- 吃食物(新增头节点)
- 碰撞(撞到自己或者墙壁,游戏结束)
创建蛇的时候通过判断它的上下左右的区域是否是空闲的而且是否是墙壁来创建节点
while (snake.length < config.min) {
let index = snake.length ? neighbour() : (Math.random() * zone.length) >> 0
snake.unshift(index)
}
neighbour () {
let around = [
head.left,
head.right,
head.up,
head.down
]
around = around.filter((index) => {
if (index !== -1) {
if (zone[index].fill === undefined) {
return true
}
}
return false
})
return around[(Math.random() * around.length)>>0]
}
活动区域zone
整个活动区域是一个四边形,看起来应该是用一个二维数组来存放,但是为了方便操作,笔者使用了一维数组存放,每个数组元素包含每个点的行数row,列数column,上下左右的索引值up,down,left,right,其自身的标记值fill("undefined"代表空闲位,"snack"代表蛇,"food"代表食物,墙壁用index=-1 表示)
zone.length = config.row * config.column
for (let i = 0, len = zone.length; i < len; i++) {
let [col, row] = [i % config.column, (i / config.row) >> 0]
zone[i] = {
col: col,
row: row,
left: col > 0 ? i - 1 : -1,
right: col < config.column - 1 ? i + 1 : -1,
up: row > 0 ? i - config.column : -1,
down: row < config.row - 1 ? i + config.column : -1,
fill: undefined
}
}
食物food
随机投食(feed):食物可以随机投放到活动区域中的除了蛇身体以外的任何位置,我们可以简单地先随机投放到区域中任意一点,假如这个点不在蛇身上那就大吉大利,假如刚好在蛇身上的话我们就计算出剩余的空间大小进行随机投食,最后把那个区域的点标记设为"food"即可~
bet() {
let random = Math.random () * zone.length >> 0
return zone[random].fill === undefined ? random : -1
}
feed() {
food = bet()
//假如投放到了蛇身上
if (food === -1) {
let len = zone.length - snake.length
let count = 0
let index = 0
let random = (Math.random() * len >> 0)+ 1
while (count !== random) {
zone[index++].fill === undefined && count++
}
food = index - 1
}
updateZone(food, 'food')
}
蛇的行为go
go (next) {
let cell = -1 === next ? 'bound' : zone[next].fill
switch (cell) {
case 'food':
eat(next)
break
case 'snake':
collision('你自己')
break
case 'bound':
collision('墙')
break
default:
move(next)
}
}
View
view层负责绘制游戏界面,对model层的数据结构进行渲染
笔者也是第一次学习游戏引擎,本文使用了PIXIjs进行游戏渲染
PixiJs
PixiJs是一个速度极快的2D精灵图渲染引擎,它能帮你展示、驱动和管理富有交互性的图形以便于制作游戏和通过使用JavaScript以及其他HTML5技术而创建的一系列应用,容易上手而且功能灰常强大~这里为和我一样的小白们提供一下学习链接:
pixi官网
学习pixi(中文版)
渲染整个活动区域
创建并初始化整个活动区域
let app = PIXI.autoDetectRenderer(width, height,
{
transparent: true
}
)
let node = document.getElementById('snake-game')
node.appendChild(app.view)
let stage = new PIXI.Container()
渲染蛇
我们定义model中的蛇叫snakeM,view中的蛇叫snakeV,我们首先根据snakeM来创建snakeV并对逐个蛇节点进行渲染
drawPoint(color = config.color) {
let node
if (colletion = []) {
node = new PIXI.Graphics()
let { width, height } = config.size
node.beginFill(color)
node.drawRect(0, 0, width, height)
node.endFill()
node.x = 0
node.y = 0
} else {
node = colletion.pop()
}
stage.addChild(node)
return node
}
然后对这些节点进行定位,创建节点和节点定位分开是因为这些节点可以在蛇频繁的移动中进行回收再利用
setPosition(node, index) {
let x = index % config.column
let y = Math.floor(index / config.row)
let { width, height } = config.size
node.x = x * width
node.y = y * height
}
当蛇开始移动或者吃食物的时候就进行增量渲染的操作,只更新有变化的节点,即只更新头或者尾节点
updateSnake(snakeM, snakeV = this.snake) {
this.updateTail(snakeM, snakeV)
.then(() => this.updateHead(snakeM, snakeV))
.then(() => this.render())
}
updateHead(snakeM, snakeV) {
return new Promise(
(resolve, reject) => {
while (snakeV.length <= snakeM.length) {
if(snakeM.chain[snakeM.head].element === snakeV.chain[snakeV.head].element) {
return resolve();
}
else {
snakeV.unshift(snakeM.chain[snakeM.head].element)
}
}
reject()
}
);
}
updateTail(snakeM, snakeV) {
return new Promise(
(resolve, reject) => {
while(snakeV.length !== 0) {
if (snakeM.chain[snakeM.tail].element === snakeV.chain[snakeV.tail].element) {
return resolve()
}
else {
snakeV.pop()
}
}
reject()
}
);
}
Control
control层负责管理用户与游戏互动的所有事件,驱动model层,同步model层和view层
- 初始化: 初始化control的同时创建model和view
- 游戏操作: 开始/暂停游戏,(销毁原来的进度并)重新开始,控制蛇的前进方向
- 同时更新model层和view层
结语
整个贪吃蛇过程就介绍完了,比较粗糙。。。例如游戏主体外的界面丑陋(按钮的位置和触感优化,更多各种友善的提示),游戏规则的逻辑太简单(应该有个规则说明而且可以调速啊等等的)如果有看得不爽的地方可以随时提出来,,笔者会尽力解决哒~~ : )