一、实验目标
1、综合所学知识创建完整的推箱子游戏;2、能够在开发过程中熟练掌握真机预览、调试等操作。
二、实验步骤
1.准备工作
1.1 删除和修改文件
(1)删除utils文件夹及其内部所有内容。 (2) 删除pages 文件夹下的logs目录及其内部所有内容。 (3)删除index.wxml和index.wxss中的全部代码。 (4)删除index.js中的全部代码,并且输入关键词page 我到第二个选项按回车键让其自动补全函数
(5)删除app.wxss中的全部代码。 (6)删除app.js中的全部代码,并且输入关键词app找到第一个选项按回车键让其自动补全函数
(7)将app.json文件内pages属性中的“pages/logs/logs”改成“pages/game/game"。 (8)按快捷键Ctrl+S保存修改后会在pages文件夹下自动生成game目录。
1.2 建立image文件夹
新建一个文件夹,用来存放各个图标
游戏图片下载链接:https://gaopursuit.oss-cn-beijing.aliyuncs.com/course/mobileDev/boxgame_images.zip
1.3创建公共js文件data
右击utils文件夹,选择“新建”,输入data.js,创建完成后删去data.wxml文件,将app.json文件内pages属性中的“utils/data”删去
2.视图设计
2.1 导航栏设计
打开app.json,写入以下代码
"window": { "navigationBarTitleText":"推箱子游戏", "navigationBarBackgroundColor": "#E64340" },
2.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; }
2.3首页设计
首页设计包含两个部分:标题和关卡列表
在index.wxml中写入如下代码
<view class='container'> <!--标题--> <view class='title'>游戏选关</view> <!--关卡列表--> <view class= 'levelBox'> <view class = 'box'wx:for='{{levels}}'wx:key='levels{{index}}'bindtap='chooseLevel' data-level='{{index}}'> <image src='/images/{{item}}'></image> <text>第{{index+1}}关</text> </view> </view> </view>
level初始值设为0
关卡对应的<view>组件添加wx:for属性循环显示关卡列表数据和图片。
chooseLevel函数见下文
循环的key值是level的下标
在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; }
2.4游戏页面设计
<!--pages/game/game.wxml--> <view class = 'container'> <!-- 关卡提示 --> <view class = 'title'>第{{level}}关</view> <!-- 游戏画布 --> <canvas canvas-id= 'myCanvas'></canvas> <!-- 方向键 --> <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> <!-- 重新开始 --> <button type='warn' bindtap='restartGame'>重新开始</button> </view>
bindtap会为每个方向键和restart绑定相关的函数,实现不同的事件。
函数代码见下文。
/*游戏画布样式*/ 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; }
三、逻辑实现
3.1公共逻辑
在data.js中写入如下代码
//地图数据 map1~map4 //地图数据:1为墙、2为路、3为终点、4为箱子、5为人物、0为墙的外围 //关卡1 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] ] //关卡2 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] ] //关卡3 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] ] //关卡4 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] }
这里分别使用map1~map4代表4个不同关卡的地图数据,以二维数组的形式存放。 前地图均由8×8的方格组成,每个位置的数字代表对应的图标素材。
不要忘记在data.js中使用module.exports语句暴露数据出口,
数据是4个地图组成的数组
最后在game.js中引用公共js文件
var data=require('../../utils/data.js')
3.2首页逻辑
3.2.1关卡列表展示
在index.js中写入如下代码
初始数据:
//页面的初始数据 data: { levels:[ 'level01.png', 'level02.png', 'level03.png', 'level04.png' ] },
chooseLevel函数如下:
//自定义函数 -- 游戏选关 chooseLevel: function(e) { let level = e.currentTarget.dataset.level wx.navigateTo( { url: '../game/game?level =' + level }) },
3.2.2初始化地图数据
在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
3.3.3初始化游戏画面
在game.js中的代码如下
//自定义函数 -- 初始化地图数据 initMap: function(level) { //读取原始的游戏地图数据 let mapData = data.maps[level] console.log(mapData) //使用双重for循环记录地图数据 for (var i = 0; i<8; i++) { for (var j = 0; j <8;j++) { box[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 } } } },
上述代码首先从公共函数文件data.js中读取对应关卡的游戏地图数据,然后使用双重 for循环对每一块地图数据进行解析,并更新当前游戏的初始地图数据、箱子数据以及游戏主 角(小鸟)的所在位置。
drawCanvas:function(){ let ctx = this.ctx console.log(ctx) //清空画布 ctx.clearRect(0, 0, 320, 320) //使用双重for循环绘制8x8的地图 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/icons/' + img + '.png', j*w, i* w, w,w) if (box[i][j] == 4) { //叠加绘制箱子 ctx.drawImage('/images/icons/box.png', j * w, i * w, w, w) } } } //叠加绘制小鸟 ctx.drawImage('/images/icons/bird.png', col*w, row*w, w, w) ctx. draw() },
//生命周期函数--监听页面加载 onLoad:function(e) { console.log(e.level) let level=e.level //获取关卡 this.setData({ //更新页面关卡标题 level:parseInt(level)+1 }) //创建画布上下文 this.ctx=wx.createCanvasContext('mycanvas') //初始化地图数据 this. initMap(level) //绘制画布内容 this.drawCanvas() },
在game.js的onLoad函数中创建画布上下文
依次调用函数initMap和drawCanvas
3.3.4方向键逻辑实现
不同的方向都要一个函数,不同方向的函数逻辑实现大致相同,只需更改部分参数和值。为节省篇幅,只截取up和down的片段。
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[row + 2] [col] = 4 box[row + 1] [col] = 0 //更新当前小鸟的坐标 row = row + 1 } } } //重新绘制地图 this. drawCanvas() this.checkWin() } },
每次移动后,存储地图数据的数组会发生变动,所以需要重新绘制地图。
除此之外还要判断游戏是否成功通关。
3.3.5判断游戏成功
在game.js中写入如下代码
//自定义函数 -- 判断游戏是否成功 isWin: function() { //使用双重for循环遍历整个数组 for (var i = 0; i <8; i++) { for (var j = 0; j < 8; j++) { //如果有箱子没在终点 if (box[i][j] == 4 && map[ i][ j] != 3) { //返回false,表示游戏尚未成功 return false } } } //返回true,表示游戏成功 return true },
上述代码的判断逻辑是只要有一个箱子没有在终点位置就判断游戏尚未成功。 然后在game.js中添加自定义函数checkWin,要求一旦游戏成功就弹出提示对话框。
checkWin: function() { if (this. isWin()) { wx. showModal( { title:'恭喜', content:'游戏成功!', showCancel: false }) } },
3.3.6重新开始游戏
restartGame: function() { //初始化地图数据 this. initMap(this.data.level - 1) //绘制画布内容 this. drawCanvas() },
四、程序运行结果
五、问题总结与体会
本次实验,在初始化游戏画面的时候老是出现错误,level值无法传输。后来发现是双重循环中j 被错写为i,导致循环出错。写代码一定要认真仔细,出现错误不要怕,根据错误的情况找到出错的部分,对症下药。