1.需求分析
本项目一共需要2个页面,即首页和游戏页面,其中首页用于呈现关卡菜单,点击对应难度的关卡后进入游戏界面
1.1首页功能需求
首页需要包含标题和关卡列表
关卡列表包含两种游戏模式,即简单模式和困难模式,主要区别在于贪吃蛇移动速度的快慢。
点击关卡图片可以打开对应的游戏界面
1.2游戏页功能需求
游戏页面需要显示当前得分,游戏画面,方向键和“重新开始”按钮
点击方向键可以使贪吃蛇上,下,左,右转方向前进和吃食物
游戏画面由16x16格的小方格组成,主要用于显示贪吃蛇和事物
点击“重新开始”按钮可以重置全部游戏数据并重新开始游戏
2.项目创建
本项目还需创建一个images文件夹,用于存放图片
导航栏设计
app.json
{
"pages":[
"pages/index/index"
],
"window":{
"navigationBarBackgroundColor": "#B00550",
"navigationBarTitleText": "贪吃蛇小游戏"
}
}
页面设计
ustify-content: space-evenly可以使每个元素之间和元素距离边距的距离都相等,但是兼容性比较差(iphone的SE上不支持,会失效,相当于没有设置), space-evenly将剩余空间平均分割,例如有5个元素,这样就有6个相同宽度的间隔空间,间隔空间的数量等于元素的数量加一。
app.wxss
/**app.wxss**/
/* 页面容器样式 */
.container{
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
}
index.wxml
<!--index.wxml-->
<!-- 包含两个关卡选项 -->
<view class="container">
<image src='/images/snake01.png'></image>
<image src="/images/snake02.png"></image>
</view>
index.wxss
/**index.wxss**/
/* 关卡图片 */
image{
width: 400rpx;
height: 400rpx;
}
游戏页面设计
游戏页面包括当前分数,游戏区域,方向键和“重新开始”按钮
由于暂时没有做点击跳转的逻辑设计,所以在开发工具顶端选择“普通编译”下的“添加编译模式”,并携带临时测试参数time=500,表示游戏刷新频率为每隔500ms刷新一次
game.wxml
<!--pages/game/game.wxml-->
<view class="container">
<!-- 关卡提示 -->
<view>当前分数:0</view>
<!-- 游戏画布 -->
<canvas canvas-id="myCanvas"></canvas>
<!-- 方向键 -->
<view class="btnBox">
<button>↑</button>
<view>
<button>←</button>
<button>↓</button>
<button>→</button>
</view>
</view>
<!-- "重新开始"按钮 -->
<button>重新开始</button>
</view>
game.wxss
/* pages/game/game.wxss */
/* 游戏画布 */
canvas{
border: 1rpx solid #e66660;
width: 320px;
height: 320px;
}
/* 方向键按钮整体区域 */
.btnBox{
display: flex;
flex-direction: column;
align-items: center;
}
/* 方向键按钮第二行 */
.btnBox view{
display: flex;
flex-direction: row;
}
/* 所有方向键按钮 */
.btnBox button{
width: 90rpx;
height: 90rpx;
}
/* 所有按钮样式 */
button{
margin: 10rpx;
background-color: blueviolet;
color: white;
}
数据模型设计
本项目将游戏画布分割为16行,16列的网格,每个网格的长,宽均为20像素。画布上的贪吃蛇是由一系列连续的网格填充颜色组成的,食物是由单个网格填充颜色而成
贪吃蛇模型设计
本项目设置贪吃蛇的初始身长为3格,以贪吃蛇的蛇头出现在最左侧第二行并且向右移动为例
目前规定为向右移动,所以随着每次游戏内容刷新都追加填充右侧一个空白网格作为蛇身,直至完整蛇身在网格中全部显现。
使用一个数组snakeMap记录组成蛇身的每一个网格坐标,并依次在画布的指定位置填充颜色,即可形成贪吃蛇从出现到移动直至展现完整身体的过程。
例如向右移动的贪吃蛇初始状态的坐标为
snakeMap=[{'x':0,'y':10}];
随着蛇头向右前进,该数组增加第二组坐标:
snakeMap=[{'x':0,'y':10},{'x':10,'y':10}];
如果继续向右前进。该数组增加第三组坐标:
snakeMap=[{'x':0,'y':10},{'x':10,'y':10},{'x':20,'y':10}];
当蛇身已经完全显示在游戏画面中时,如果蛇继续前进,则需要清除蛇尾的网格颜色,以表现出蛇的移动效果。
继续向右前进,该数组添加新坐标,并且需要删除最早的一组坐标:
snakeMap=[{'x':10,'y':10},{'x':20,'y':10},{'x':30,'y':10}];
因为该数组中的坐标只用于显示当前的蛇身数据,所以需要去掉曾经路过的轨迹。这种绘制方式可以展现贪吃蛇的动态移动效果。
故只要每次游戏界面刷新时保持更新snakeMap数组的记录即可获得贪吃蛇的当前位置
蛇吃食物模型
在蛇吃食物的过程中,蛇遇到食物,蛇身增长一格,并且食物消失,然后在随机位置重新出现。
(20,30)表示食物
食物位于蛇头下方,因此需要控制蛇头向下移动来接近食物;
每当蛇吃食物时,需要将表示蛇身的变量t自增1.然后判断用于记录蛇身坐标的数组snakeMap长度,如果与当前蛇身长度t的值相同,则不必删除最前面的数据。
snakeMap=[{'x':10,'y':10},{'x':20,'y':10},{'x':20,'y':20},{'x':20,'y':30}];
逻辑实现:
首页逻辑
首页主要需要实现点击图片能跳转到游戏界面,并且根据游戏难度模式规定贪吃蛇的行动速度。本项目根据蛇的行动速度快慢计划采用两种游戏模式,即简单模式(0.5s移动一次)和困难模式(0.2s移动一次)
index.wxml
<!--index.wxml-->
<!-- 包含两个关卡选项 -->
<view class="container">
<image src='/images/snake01.png' bindtap="goToGame" data-level="easy"></image>
<image src="/images/snake02.png" bindtap="goToGame" data-level="hard"></image>
</view>
为关卡添加了自定义点击事件函数goToGame,并且使用了data-level属性携带了游戏难度模式信息easy和hard
index.js
/**
* 自定义函数--跳转游戏页面
*/
goToGame: function (e) {
// 获取游戏模式
let level = e.currentTarget.dataset.level
//游戏界面刷新的间隔时间,单位为毫秒
let time=0
//简单模式
if(level == 'easy'){
time=500
}
//困难模式
else if(level == 'hard'){
time=200
}
// 跳转游戏页面
wx.navigateTo({
url: '../game/game?time='+time,
})
},
此时可以点击跳转到game页面,并且成功携带了贪吃蛇的行动速度数据,但是仍需在game页面进行携带数据的接收处理才可显示正确的游戏界面
游戏页逻辑
初始化游戏数据,包括蛇身长度,游戏速度,初始食物位置等;
游戏画面的绘制
4个方向键可以改变贪吃蛇的行动方向
点击“重新开始”按钮可以使游戏回到初始状态
游戏分数的记录
游戏失败的判断
首先在game.js顶端使用一系列数据表示贪吃蛇的初始状态,包括蛇身长度,首次出现的位置和移动方向等。
game.js
// pages/game/game.js
// 游戏参数设置
// 蛇的身长
var t=3
// 记录蛇的运动轨迹,用数组记录每个坐标点
var snakeMap = []
//蛇身单元格大小
var w = 20
//方向代码:上为1,下为2,左为3,右为4
var direction = 1
//蛇的初始坐标
var x=0
var y=0
//食物的初始坐标
var foodX=0
var foodY=0
//画布的宽和高
var width = 320
var height = 320
//游戏界面刷新的间隔时间,单位为毫秒
var time = 1000
Page({
/**
* 页面的初始数据
*/
data: {
score:0 //游戏当前分数
},
此时time只是初始值,还需要在game.js的onLoad函数中读取从首页传递来的参数值对其进一步更新。
game.js
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
// 更新游戏刷新时间
time = options.time
},
还需要在game.js的onLoad函数中对画布上下文进行初始化,以便后续可以进行贪吃蛇和食物的绘制工作。
game.js
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
// 更新游戏刷新时间
time = options.time
// 创建画布上下文
this.ctx = wx.createCanvasContext('myCanvas')
},
修改game.wxml页面,将游戏分数用{ {score}}表示。
game.wxml
<!-- 关卡提示 -->
<view>当前分数:{
{score}}</view>
绘制贪吃蛇逻辑实现
(1)初始化贪吃蛇数据
每次游戏重新开始时需要重新初始化贪吃蛇的一系列数据,例如蛇身长度,蛇身坐标,蛇头坐标和前进方向等
在game.js文件中创建自定义函数gameStart,用于启动游戏,game.js
/**
* 自定义函数--启动游戏
*/
gameStart: function () {
// 初始化蛇身长度
t = 3
//初始化蛇身坐标
snakeMap = []
//随机生成贪吃蛇的初始蛇头坐标
x = Math.floor(Math.random()*width/w)*w
y = Math.floor(Math.random()*height/w)*w
//随机生成蛇的初始前进方向
direction = Math.ceil(Math.random()*4)
},
若蛇的初始位置与方向的初始数据均为固定值,则会降低游戏的难度和可玩度,因此使用随机数重新定义贪吃蛇初始出现的位置和移动方向
(2)绘制蛇身
每次游戏画面刷新,蛇都需要在指定方向上再前进一格。如果蛇没有吃到新食物,则还需要清除原先蛇尾最后一个位置的颜色,以表示出贪吃蛇动态前进了一格的效果。
在game.js中声明自定义函数drawSnake,专门用于绘制贪吃蛇的蛇身,
game.js
/**
* 自定义函数--绘制贪吃蛇
*/
drawSnake: function () {
let ctx = this.ctx
//设置蛇身的填充颜色
ctx.setFillStyle('lightblue')
//绘制全部蛇身
for(var i=0;i<snakeMap.length; i++){
ctx.fillRect(snakeMap[i].x,snakeMap[i].y,w,w)
}
},
由于要通过游戏画面刷新才能实现动画效果,所以在game.js中声明自定义函数gameRefresh专门用于刷新画布,并在该函数中调用drawSnake函数来绘制贪吃蛇的蛇身变化过程。
gameRefresh: function () {
//当前坐标添加到贪吃蛇的运动轨迹坐标数组中
snakeMap.push({
'x':x,
'y':y
})
//数组只保留蛇身长度的数据,如果蛇前进了,则删除最旧的坐标
if(snakeMap.length>t){
snakeMap.shift()
}
//绘制贪吃蛇
this.drawSnake()
//在画布上绘制全部内容
this.ctx.draw()
//根据方向移动蛇头的下一个位置
switch(direction){
case 1:
y -= w
break
case 2:
y += w
break
case 3:
x -= w
break
case 4:
x += w
break
}
},
然后在game.js中修改自定义函数gameStart,使用setInterval()方法设置在间隔规定的时间后重复调用gameRefresh已达到游戏画面刷新的效果。修改后的gameStart函数如下:
gameStart: function () {
// 初始化蛇身长度
t = 3
//初始化蛇身坐标
。。。
//随机生成贪吃蛇的初始蛇头坐标
。。。
//随机生成蛇的初始前进方向
。。。
// 每隔time毫秒刷新一次游戏内容
var that = this
this.interval = setInterval(function(){that.gameRefresh()},time)
},
最后在game.js的onLoad函数中调用gameStart,使动画效果启动
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
//更新游戏刷新时间
time = options.time
//创建画布上下文
this.ctx = wx.createCanvasContext('myCanvas')
//开始游戏
this.gameStart()
},
方向键逻辑实现
在game.wxml页面中的4个方向键<button>,为其绑定点击事件
<!-- 方向键 -->
<view class="btnBox">
<button bindtap="up">↑</button>
<view>
<button bindtap="left">←</button>
<button bindtap="down">↓</button>
<button bindtap="right">→</button>
</view>
</view>
在game.js文件中添加自定义函数up,dowm,left,right,分别用于实现贪吃蛇向上,向下,左,右4个方向移动
// pages/game/game.js
Page({
/**
* 自定义函数--监听方向键
*/
up: function () {
direction = 1
},
down: function () {
direction = 2
},
left: function () {
direction = 3
},
right: function () {
direction = 4
},
绘制食物逻辑
接下来需要在画布上为贪吃蛇绘制食物。食物每次将在随机网络位置出现,占一格位置。
食物每次只在画面中呈现一个,直到被蛇头触碰表示吃掉方可在原先的位置清除,并在下一个随机位置重新产生
初始化食物数据 game.js
//随机生成食物的初始化坐标
foodX = Math.floor(Math.random()*width/w)*w
foodY = Math.floor(Math.random()*height/w)*w
绘制食物 game.js
声明自定义函数drawFood,专门用于绘制食物
/**
* 自定义函数--绘制食物
*/
drawFood:function(){
let ctx = this.ctx
//设置食物的填充颜色
ctx.setFillStyle('red')
//绘制食物
ctx.fillRect(foodX,foodY,w,w)
},
在game.js中修改自定义函数gameRefresh,在该函数中调用drawFood函数在指定位置绘制食物。
/**
* 自定义函数--游戏画面刷新
*/
gameRefresh: function () {
// 随机绘制一个食物
this.drawFood()
。。。
吃到食物的判定
当蛇头和食物出现在同一个方格中时判定吃到了食物,此时食物消失,当前分数增加10分,蛇身增加一格,并且在随机位置重新生下一个食物。
gameRefresh:function(){
...
// 吃到食物的判定
if(foodX == x && foodY == y){
//吃到一次食物增加10分
let score = this.data.score+10
this.setData({
score:score
})
//随机生成下一个食物的初始坐标
foodX = Math.floor(Math.random()*width/w)*w
foodY = Math.floor(Math.random()*height/w)*w
//在新的随机位置绘制食物
this.drawFood()
//蛇身长度增加1
t++
}
...
}
碰撞检测逻辑
如果蛇头撞到了游戏画面的任意一边或者撞到蛇身均判定为游戏失败,此时弹出提示对话框告知玩家游戏失败的原因,并提示其重新开始。玩家点击对话框上的“确定”按钮则可以开始下一局游戏
在game.js中创建自定义函数delectCollision用于进行蛇与障碍物的碰撞检测
碰撞检测有两种可能性,一是蛇头撞到了四周的墙壁,二是蛇头撞到了蛇身
/**
* 自定义函数--碰撞检测
*/
detectCollision:function(){
//如果蛇头撞到了四周的墙壁,游戏失败
if(x>width||y>height||x<0||y<0){
return 1
}
//如果蛇头撞到了蛇身,游戏失败
for(var i=0; i<snakeMap.length; i++){
if(snakeMap[i].x == x && snakeMap[i].y == y){
return 2
}
}
//没有碰撞
return 0
},
然后在game.js中修改gameRefresh函数,要求一旦游戏失败则弹出提示对话框。
gameRefresh:function(){
......
//碰撞检测,返回值为0表示没有撞到障碍物
let code = this.detectCollision()
if(code!=0){
//游戏停止
clearInterval(this.interval)
var msg = ''
if(code == 1){
msg = '失败原因:撞到了墙'
}else if(code == 2){
msg = '失败原因:撞到了蛇身'
}
wx.showModal({
title:'游戏失败,是否重来',
content:msg,
success:res => {
if(res.confirm){
//重新开始游戏
this.gameStart()
}
}
})
}
}
重新开始游戏
game.wxml,为“重新开始”按钮追加自定义函数的点击事件
<!-- "重新开始"按钮 -->
<button type="warn" bindtap="restartGame">重新开始</button>
在game.js中添加restartGame函数,用于重新开始游戏
// 自定义函数--重新开始游戏
restartGame:function(){
//关闭刷新效果
clearInterval(this.interval)
//重新开始游戏
this.gameStart()
},
返回首页时停止游戏
当游戏中途点击左上角返回键返回首页时还需要停止游戏,在game.js的onUnload函数中停止定时器即可。
// 生命周期函数--监听页面卸载
onUnload:function(){
//游戏停止
clearInterval(this.interval)
}
完整代码:
应用文件代码显示:
app.json
{
"pages": [
"pages/index/index",
"pages/game/game"
],
"window": {
"navigationBarBackgroundColor": "#B00550",
"navigationBarTitleText": "贪吃蛇小游戏"
}
}
app.wxss
/**app.wxss**/
/* 页面容器样式 */
.container{
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
}
页面代码显示
首页代码显示
index.wxml
<!--index.wxml-->
<!-- 包含两个关卡选项 -->
<view class="container">
<image src='/images/snake01.png' bindtap="goToGame" data-level="easy"></image>
<image src="/images/snake02.png" bindtap="goToGame" data-level="hard"></image>
</view>
index.wxss
/**index.wxss**/
/* 关卡图片 */
image{
width: 300rpx;
height: 300rpx;
}
index.js
// index.js
// 获取应用实例
Page({
//自定义函数--跳转游戏页面
goToGame:function(e){
//获取游戏模式
let level = e.currentTarget.dataset.level
//游戏界面刷新的间隔时间,单位为毫秒
let time = 0
//简单模式
if(level == 'easy'){
time = 500
}
//困难模式
else if(level == 'hard'){
time = 200
}
//跳转到游戏界面
wx.navigateTo({
url: '../game/game?time='+time,
})
}
})
游戏页代码展示
game.wxml
<!--pages/game/game.wxml-->
<view class="container">
<!-- 关卡提示 -->
<view>当前分数:{
{score}}</view>
<!-- 游戏画布 -->
<canvas canvas-id="myCanvas"></canvas>
<!-- 方向键 -->
<view class="btnBox">
<button bindtap="up">↑</button>
<view>
<button bindtap="left">←</button>
<button bindtap="down">↓</button>
<button bindtap="right">→</button>
</view>
</view>
<!-- "重新开始"按钮 -->
<button type="warn" bindtap="restartGame">重新开始</button>
</view>
game.wxss
/* pages/game/game.wxss */
/* 游戏画布 */
canvas{
border: 1rpx solid #e66660;
width: 320px;
height: 320px;
}
/* 方向键按钮整体区域 */
.btnBox{
display: flex;
flex-direction: column;
align-items: center;
}
/* 方向键按钮第二行 */
.btnBox view{
display: flex;
flex-direction: row;
}
/* 所有方向键按钮 */
.btnBox button{
width: 90rpx;
height: 90rpx;
}
/* 所有按钮样式 */
button{
margin: 10rpx;
background-color: blueviolet;
color: white;
}
game.js
// pages/game/game.js
var t=3
var snakeMap = []
var direction = 1
//蛇身单元格大小
var w = 20
//蛇的初始坐标
var x = 0
var y = 0
//食物的初始坐标
var foodX = 0
var foodY = 0
//画布的宽和高
var width = 320
var height = 320
//游戏界面刷新的间隔时间
var time = 1000
Page({
// 自定义函数--重新开始游戏
restartGame:function(){
//重新开始游戏
this.gameStart()
},
/**
* 自定义函数--碰撞检测
*/
detectCollision:function(){
//如果蛇头撞到了四周的墙壁,游戏失败
if(x>width||y>height||x<0||y<0){
return 1
}
//如果蛇头撞到了蛇身,游戏失败
for(var i=0; i<snakeMap.length; i++){
if(snakeMap[i].x == x && snakeMap[i].y == y){
return 2
}
}
//没有碰撞
return 0
},
/**
* 自定义函数--绘制食物
*/
drawFood:function(){
let ctx = this.ctx
//设置食物的填充颜色
ctx.setFillStyle('red')
//绘制食物
ctx.fillRect(foodX,foodY,w,w)
},
/**
* 自定义函数--监听方向键
*/
up: function () {
direction = 1
},
down: function () {
direction = 2
},
left: function () {
direction = 3
},
right: function () {
direction = 4
},
/**
* 页面的初始数据
*/
data: {
score:0
},
/**
* 自定义函数--启动游戏
*/
gameStart: function () {
//初始化游戏分数
this.setData({
score:0
})
// 初始化蛇身长度
t = 3
//初始化蛇身坐标
snakeMap = []
//随机生成贪吃蛇的初始蛇头坐标
x = Math.floor(Math.random()*width/w)*w
y = Math.floor(Math.random()*height/w)*w
//随机生成蛇的初始前进方向
direction = Math.ceil(Math.random()*4)
//随机生成食物的初始化坐标
foodX = Math.floor(Math.random()*width/w)*w
foodY = Math.floor(Math.random()*height/w)*w
// 每隔time毫秒刷新一次游戏内容
var that = this
this.interval = setInterval(function(){that.gameRefresh()},time)
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
//更新游戏刷新时间
time = options.time
//创建画布上下文
this.ctx = wx.createCanvasContext('myCanvas')
//开始游戏
this.gameStart()
},
/**
* 自定义函数--绘制贪吃蛇
*/
drawSnake: function () {
let ctx = this.ctx
//设置蛇身的填充颜色
ctx.setFillStyle('lightblue')
//绘制全部蛇身
for(var i=0;i<snakeMap.length; i++){
ctx.fillRect(snakeMap[i].x,snakeMap[i].y,w,w)
}
},
/**
* 自定义函数--游戏画面刷新
*/
gameRefresh: function () {
// 随机绘制一个食物
this.drawFood()
//当前坐标添加到贪吃蛇的运动轨迹坐标数组中
snakeMap.push({
'x':x,
'y':y
})
//数组只保留蛇身长度的数据,如果蛇前进了,则删除最旧的坐标
if(snakeMap.length>t){
snakeMap.shift()
}
//绘制贪吃蛇
this.drawSnake()
// 吃到食物的判定
if(foodX == x && foodY == y){
//吃到一次食物增加10分
let score = this.data.score+10
this.setData({
score:score
})
//随机生成下一个食物的初始坐标
foodX = Math.floor(Math.random()*width/w)*w
foodY = Math.floor(Math.random()*height/w)*w
//在新的随机位置绘制食物
this.drawFood()
//蛇身长度增加1
t++
}
//在画布上绘制全部内容
this.ctx.draw()
//根据方向移动蛇头的下一个位置
switch(direction){
case 1:
y -= w
break
case 2:
y += w
break
case 3:
x -= w
break
case 4:
x += w
break
}
//碰撞检测,返回值为0表示没有撞到障碍物
let code = this.detectCollision()
if(code!=0){
//游戏停止
clearInterval(this.interval)
var msg = ''
if(code == 1){
msg = '失败原因:撞到了墙'
}else if(code == 2){
msg = '失败原因:撞到了蛇身'
}
wx.showModal({
title:'游戏失败,是否重来',
content:msg,
success:res => {
if(res.confirm){
//重新开始游戏
this.gameStart()
}
}
})
}
},
// 生命周期函数--监听页面卸载
onUnload:function(){
//游戏停止
clearInterval(this.interval)
}
})