一、实验目标
1、综合所学知识创建完整的推箱子游戏;
2、能够在开发过程中熟练掌握真机预览、调试等操作。
二、实验步骤
1.功能需求
本项目一共需要两个页面,即首页和游戏页面,其中首页用于呈现关卡菜单,点击对应难度的关卡后进入游戏画面。
(1)首页功能需求
-
首页需要包含标题和关卡列表。
-
关卡至少要有4个关卡选项,每个关卡显示预览图片和第几关。
-
点击关卡列表可以打开对应的游戏画面。
(2) 游戏页功能需求
-
游戏页面需要显示第几关、游戏画面、方向键和“重新开始”按钮。
-
点击方向键可以使游戏主角自行移动或推动箱子前进。
-
游戏画面由8x8的小方块组成,主要包括地板、围墙、箱子、游戏主角和目的地。
-
点击“重新开始”按钮可以将箱子和游戏主角回归初始位置并重新开始游戏。
2.创建项目
-
新建项目,选择空白文件夹,创建小程序,选择不使用云服务,模板选择JS基础。
-
在app.json中的pages属性下的pages/logs/logs改为pages/game/game,保存后pages文件夹下自动生成game页面。
-
删除logs目录及其所有内容。
-
删除utils文件夹下内容,新建data.js文件。
-
删除 index.wxml和index.wxss全部代码。
-
删除index.js中全部代码,输入关键词page,找到Page选项回车自动补全函数。
-
删除app.wxss中全部代码。
-
删除app.js中全部代码,输入关键词app,找到App选项回车自动补全函数。
-
下载游戏图片链接: https://gaopursuit.oss-cnbeijing.aliyuncs.com/course/mobileDev/boxgame_images.zip,解压,图片素材放在项目中创建的images文件夹。
3.视图设计
3.1 导航栏设计
修改app.json文件中的 window属性,定义导航栏的效果,修改导航栏的背景色为红色,字体为白色
"window":{ "navigationBarBackgroundColor": "#E64340", "navigationBarTitleText": "推箱子游戏", "navigationBarTextStyle":"white" },
3.2 公共样式设计
在app.wxss中设置页面容器和顶端标题的公共样式。
.container{ height: 100vh; color: #E64340; font-weight: bold; display: flex; flex-direction: column; align-items: center; justify-content: space-evenly; } .title{ font-size: 18pt; }
3.3 首页设计
首页主要包含两部分内容,即标题和关卡选择列表,使用<view>容器,内部使用数组循环。
-
容器视图 (container):
-
使用
<view class='container'>
创建一个主要内容的容器。
-
-
标题视图 (title):
-
使用
<view class='title'>
显示标题“选择关卡”。
-
-
关卡盒子视图 (levelBox):
-
使用
<view class='levelBox'>
创建一个包含多个关卡盒子的容器。 -
使用 for 循环遍历
levels
数组,为每个关卡生成一个盒子。每个盒子包含一个<image src='/images/{{item}}'/>
标签,用于显示关卡的图片。每个盒子还包含一个<text>
标签,显示关卡编号(例如,第1关)。
-
注意,levels
是数据源,应该在页面的 JavaScript 文件中定义和赋值。
index.wxml代码:
<view class='container'> <view class='title'>选择关卡</view> <view class='levelBox'> <view class='box' wx:for="{{levels}}" wx:key="levels{{index}}" bind:tap="chooseLevel" data-level="{{index}}"> <image src='/images/{{item}}'/> <text>第{{index + 1}}关</text> </view> </view> </view>
index.wxss代码:
.levelBox{ width: 100%; } .box{ width: 50%; float: left; margin: 20rpx 0; display: flex; flex-direction: column; align-items: center; } image{ width: 300rpx; height: 300rpx; }
3.4游戏页面设计
用户点击首页的关卡列表,然后跳转到游戏页面,其包括游戏关卡标题,游戏画面,方向键和“重新开始”按钮,使用<view><canvas><button>
组件用于实现关卡标题,图形绘制和重新开始按钮。
-
容器视图 (container):
-
使用
<view class="container">
创建一个主要内容的容器。
-
-
标题视图 (title):
-
使用
<view class='title'>
显示当前关卡的标题,例如“第{{level}}关”。
-
-
画布 (canvas):
-
使用
<canvas canvas-id="sokoban"></canvas>
创建一个画布,用于显示游戏内容。
-
-
按钮盒子视图 (btnBox):
-
使用
<view class='btnBox'>
创建一个包含控制按钮的容器。 -
包含一个向上的按钮
<button type='warn' bind:tap="up">⬆</button>
。 -
包含一个嵌套的视图
<view>
,其中有四个方向按钮:-
向左按钮
<button type='warn' bind:tap="left">⬅</button>
-
向下按钮
<button type='warn' bind:tap="down">⬇</button>
-
向右按钮
<button type='warn' bind:tap="right">➡</button>
-
-
-
重新开始按钮 (restart):
-
使用
<button type='warn' bind:tap="restart">重新开始</button>
创建一个重新开始游戏的按钮。
-
game.wxml代码:
<view class="container"> <!--关卡提示--> <view class="title">第1关</view> <!--游戏画布--> <canvas canvas-id="myCanvas"></canvas> <!--方向键--> <view class="btnBox"> <button type="warn">↑</button> <view> <button type="warn">←</button> <button type="warn">↓</button> <button type="warn">→</button> </view> </view> <!--重新开始按钮--> <button type="warn">重新开始</button> </view>
game.wxss代码:
canvas{ border: 1rpx solid; 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; }
4.逻辑实现
4.1 公共逻辑
在data.js文件中配置游戏地图的数据,分别使用map1~map4代表4个不同的关卡地图数据,以二维数组的形式存放。当前的地图均由8*8的放个组成,每个位置的数字代表着对应的图标素材,并且需要在data.js中使用module.exports
语句暴露数据出口。
var map1 = [ [0,1,1,1,1,1,0,0], [0,1,2,2,1,1,1,0], [0,1,5,4,2,2,1,0], [1,1,1,2,1,2,1,1], [1,3,1,2,1,2,2,1], [1,3,4,2,2,1,2,1], [1,3,2,2,2,4,2,1], [1,1,1,1,1,1,1,1] ] var map2 = [ [0,0,1,1,1,0,0,0], [0,0,1,3,1,0,0,0], [0,0,1,2,1,1,1,1], [1,1,1,4,2,4,3,1], [1,3,2,4,5,1,1,1], [1,1,1,1,4,1,0,0], [0,0,0,1,3,1,0,0], [0,0,0,1,1,1,0,0] ] var map3 = [ [0,0,1,1,1,1,0,0], [0,0,1,3,3,1,0,0], [0,1,1,2,3,1,1,0], [0,1,2,2,4,3,1,0], [1,1,2,2,5,4,1,1], [1,2,2,1,4,4,2,1], [1,2,2,2,2,2,2,1], [1,1,1,1,1,1,1,1] ] var map4 = [ [0,1,1,1,1,1,1,0], [0,1,3,2,3,3,1,0], [0,1,3,2,4,3,1,0], [1,1,1,2,2,4,1,1], [1,2,4,2,2,4,2,1], [1,2,1,4,1,1,2,1], [1,2,2,2,5,2,2,1], [1,1,1,1,1,1,1,1] ] module.exports = { maps: [map1, map2, map3, map4] }
在game.js中引用公共的js文件:
var data = require('../../utils/data.js')
4.2 首页逻辑
(1)关卡列表展示
在index.js中的data数据中添加关卡的图片数据信息。
data: { levels:[ 'level01.png', 'level02.png', 'level03.png', 'level04.png', ] },
(2)点击跳转游戏页面
在首页点击游戏关卡,需要跳转到对应的游戏页面,所以为关卡列表绑定点击事件。
index.wxml部分代码:
<view class="box" wx:for="{{levels}}" wx:key="index" bind:tap="chooseLevel" data-level="{{index}}"> <image src="/images/{{item}}" /> <text>第{{index+1}}关</text> </view>
-
定义一个
chooseLevel
函数,处理关卡选择事件。 -
函数通过
e.currentTarget.dataset.level
获取被点击关卡的索引。 -
使用
wx.navigateTo
方法导航到游戏页面,并传递关卡索引作为参数。
chooseLevel:function(e){ let level = e.currentTarget.dataset.level wx.navigateTo({ url:'../game/game?level='+level }) },
4.3 游戏页逻辑
(1)显示当前第几关
在game.js中接受关卡数据:
/** * 页面的初始数据 */ data: { level: 1 }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { let level = options.level this.setData({ level: parseInt(level) + 1 }) },
game.wxml相关代码:
<view class="title">第{{level}}关</view>
(2)游戏逻辑实现
(i)初始化游戏画面
首先 在game.js文件顶端添加一些游戏的初始数据信息,代码如下:
var map = [ [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0] ]; var box = [ [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0] ]; //方块宽度 var w = 40; //初始化游戏主角的行与列 var row = 0; var col = 0;
需要根据当前是第几关读取对应的游戏地图信息,并更新到游戏的初始数据中。
在 game.js
文件中添加initMap
函数,用于初始化游戏地图数据。data.js
文件中应该定义了 maps
数据源。
-
读取地图数据:
-
使用
data.maps[level]
读取对应关卡的原始游戏地图数据。
-
-
双重循环解析地图数据:
-
使用双重
for
循环遍历地图的每一个位置(8x8的地图)。 -
初始化
bos
数组中的每个位置为0
。 -
将
mapData
中的值赋给map
数组。
-
-
更新地图和箱子数据:
-
如果
mapData[i][j]
的值为4
,表示该位置有箱子,将box[i][j]
设为4
,并将map[i][j]
设为2
。 -
如果
mapData[i][j]
的值为5
,表示该位置是主角的位置,将map[i][j]
设为2
,并记录主角的行列位置row
和col
。
-
game.js部分代码:
initMap:function(level){ //读取原始的游戏地图数据 let mapData = data.maps[level]; //使用双重for循环记录地图数据 for(var i = 0; i<8; i++){ for(var j = 0; j<8; j++){ bos[i][j] = 0; map[i][j] = mapData[i][j]; if(mapData[i][j] == 4){ box[i][j] = 4; map[i][j] = 2; } else if(mapData[i][j] == 5){ map[i][j] = 2; row = i; col = j; } } } },
然后在game.js中添加自定义函数drawCanvas
用于将地图数据绘制画布上:
-
清除画布:
-
使用
ctx.clearRect(0, 0, 320, 320)
清除画布内容。
-
-
绘制地图:
-
使用双重
for
循环遍历地图的每一个位置(8x8的地图)。 -
根据
map[i][j]
的值选择不同的图片(ice
、stone
、pig
),并使用ctx.drawImage
绘制到画布上。 -
如果
box[i][j]
的值为4
,表示该位置有箱子,绘制箱子图片。
-
-
绘制主角:
-
使用
ctx.drawImage
在主角的位置绘制主角图片。
-
-
刷新画布:
-
使用
ctx.draw()
刷新画布,显示绘制的内容。
-
drawMap(){ let ctx = this.ctx; ctx.clearRect(0, 0, 320, 320); for(var i = 0;i < 8;i++){ for(var j = 0;j < 8;j++){ let img = 'ice' if(map[i][j] == 1){ img = 'stone' }else if(map[i][j] == 3){ img = 'pig' } ctx.drawImage('/images/' + img + '.png', j * w, i * w, w, w) if(box[i][j] == 4){ ctx.drawImage('/images/box.png', j * w, i * w, w, w) } } } ctx.drawImage('/images/bird.png',col * w, row * w, w, w) ctx.draw(); },
最后在game.js的onLoad
函数中创建画布上下文,并调用initMap
和drawCanvas
函数:
onLoad(options) { let level = options.level this.setData({ level: parseInt(level) + 1 }) //创建画布上下文 this.ctx = wx.createCanvasContext('myCanvas') //初始化地图数据 this.initMap(level) //绘制画布内容 this.drawCanvas() },
(ii)方向键逻辑实现
game.wxml中的4个方向键<button>
,为其绑定点击事件:
<!--方向键--> <view class="btnBox"> <button type="warn" bindtap='up'>↑</button> <view> <button type="warn" bindtap='left'>←</button> <button type="warn" bindtap='down'>↓</button> <button type="warn" bindtap='right'>→</button> </view> </view>
之后在game.js文件中添加自定义函数up
、down
、left
和right
分别用于实现游戏主角的上下左右四个方向的移动。
-
玩家移动:
-
玩家可以通过上下左右按钮移动。
-
每次移动时,检查目标位置是否为空地或目标位置,如果是,则更新玩家位置。
-
如果目标位置是箱子,还需要检查箱子前方的格子是否为空地或目标位置,如果是,则同时移动箱子。
-
-
碰撞检测:
-
玩家不能移动到墙壁或箱子前方是墙壁的格子。
-
箱子不能被推到墙壁或其他箱子前方是墙壁的格子。
-
up: function () { //不在最顶端才考虑上移 if (row > 0) { //如果上方不是墙或箱子,可以移动小鸟 if (map[row - 1][col] != 1 && box[row - 1][col] != 4) { //更新当前小鸟的坐标 row = row - 1 } //如果上方是箱子 else if (box[row - 1][col] == 4) { //箱子不在最顶端才能考虑推动 if (row - 1 > 0) { //如果箱子上方不是墙或箱子 if (map[row - 2][col] != 1 && box[row - 2][col] != 4) { box[row - 2][col] = 4 box[row - 1][col] = 0 //更新当前小鸟的坐标 row = row - 1 } } } //重新绘制地图 this.drawCanvas() //判断是否成功通关 this.checkWin(); } }, down: function () { if (row < 7) { if (map[row + 1][col] != 1 && box[row + 1][col] != 4) { row = row + 1 } else if (box[row + 1][col] == 4) { if (row + 1 < 7) { if (map[row + 2][col] != 1 && box[row + 2][col] != 4) { box[box + 2][col] = 4 box[row + 1][col] = 0 row = row + 1 } } } this.drawCanvas() this.checkWin(); } }, left: function () { if (col > 0) { if (map[row][col - 1] != 1 && box[row][col - 1] != 4) { col = col - 1 } else if (box[row][col - 1] == 4) { if (col - 1 > 0) { if (map[row][col - 2] != 1 && box[row][col - 2] != 4) { box[row][col - 2] = 4 boxd[row][col - 1] = 0 col = col - 1 } } } this.drawCanvas() this.checkWin(); } }, right:function(){ if (col < 7) { if (map[row][col + 1] != 1 && box[row][col + 1] != 4) { col = col + 1 } else if (box[row][col + 1] == 4) { if (col + 1 < 7) { if (map[row][col + 2] != 1 && box[row][col + 2] != 4) { box[row][col + 2] = 4 boxd[row][col + 1] = 0 col = col + 1 } } } this.drawCanvas() this.checkWin(); } },
(3)判断游戏成功
胜利的逻辑是当所有箱子都被推到目标位置时,游戏即告胜利。遍历所有目标位置,检查每个目标位置上是否都有一个箱子。如果所有目标位置上都有箱子,则判定游戏胜利。
在game.js文件中添加自定义函数isWin
用于判断游戏是否已经成功。
isWin:function(){ for(var i = 0;i<8;i++){ for(var j =0;j<8;j++){ if(box[i][j] == 4&&map[i][j]!=3){ return false } } } return true },
之后添加checkWin
函数用于弹出游戏成功提示对话框。
checkWin:function(){ if(this.isWin()){ wx.showModal({ title: '恭喜', content: '游戏成功!', showCancel: false }) } },
(4)重新开始游戏
在game.wxml文件中,为“重新开始”按钮添加自定义点击事件:
<button type="warn" bindtap="restartGame" >重新开始</button>
在game.js文件中添加restartGame
函数,用于重新开始游戏:
restartGame: function () { //初始化地图数据 this.initMap(this.data.level - 1) //绘制画布内容 this.drawCanvas() },
三、程序运行结果
游戏首页:
点击第1关进入关卡:
点击上下左右按钮移动小鸟:
通关成功:
点击确定:
点击重新开始:
点击左上角返回首页:
点击第2关进入关卡:
四、问题总结与体会
在本次实验中,我综合运用了所学的前端开发知识,成功创建了一个完整的推箱子游戏。通过这个项目,我不仅巩固了对微信小程序开发的理解,还提升了实际操作能力。
在项目开始之前,我首先进行了详细的规划和设计。明确了项目的功能需求,包括地图布局、玩家移动、箱子推送、碰撞检测和胜利条件等。通过绘制游戏流程图和界面原型图,我对项目的整体结构有了清晰的认识。这一步骤帮助我在开发过程中有条不紊地进行,避免了不必要的返工。
在代码编写过程中,我充分利用了微信小程序提供的各种组件。例如,使用 <view>
和 <button>
组件构建游戏界面,通过数据绑定和事件处理,实现了玩家的移动和箱子的推送功能。每次玩家移动时,我都进行了碰撞检测,确保玩家和箱子不会穿过墙壁或重叠。
在调试过程中,我学会了使用微信开发者工具进行真机预览和调试。通过实时预览,我能够及时发现和修复页面布局和功能上的问题。此外,利用开发者工具提供的调试功能,我可以查看控制台输出、断点调试代码,进一步提高了调试效率。
在项目中,我还学习了如何管理和存储游戏数据。通过二维数组表示游戏地图,记录玩家、箱子、墙壁和目标位置的坐标。每次玩家移动或推送箱子后,我都更新地图数据,并检查胜利条件。当所有箱子都被推到目标位置时,游戏即告胜利。
在开发过程中,我遇到了一些技术难题,但通过查阅文档、参考示例代码和请教他人,我逐步解决了这些问题。这让我深刻体会到,前端开发是一项需要不断学习和实践的工作。未来,我将继续学习更多的前端技术,不断提升自己的开发能力。
总的来说,这次实验不仅让我掌握了微信小程序开发的基本技能,还培养了我解决问题的能力和团队合作精神。通过这个项目,我对前端开发有了更深入的理解,也为今后的学习和工作打下了坚实的基础。