2024年夏季《移动软件开发》实验报告
一、实验目标
1、综合所学知识创建完整的推箱子游戏;2、能够在开发过程中熟练掌握真机预览、调试等操作。
二、实验步骤
1. 项目准备
在实验开始前,首先需要完成以下准备工作:
- 开发环境配置:配置好小程序的项目结构和基本配置文件(如
app.json
等)。
{
"pages": [
"pages/index/index",
"pages/game/game"
],
"window": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "推箱子游戏",
"navigationBarBackgroundColor": "#e64380"
},
"style": "v2",
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}
- 资源文件准备:收集并整理游戏所需的图像资源(如角色、箱子、地图块等),将其放入项目的
images
目录中。
- 数据文件准备:整理游戏关卡所需的地图数据,并将其存储在一个公共数据文件
data.js
中,供游戏逻辑使用。
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]
}
2. 首页设计
首页作为游戏的入口界面,主要展示可选择的游戏关卡。设计步骤如下:
- 界面布局:在首页界面使用
wx:for
循环动态生成关卡选择按钮,并设计按钮的样式。 - 跳转逻辑:在用户点击某个关卡按钮后,触发
chooseLevel
函数,通过wx.navigateTo
跳转到对应的游戏页面,并传递关卡参数。
<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>
3. 游戏页面设计
游戏页面作为推箱子游戏的核心展示区域,设计步骤如下:
- 画布创建:在
game.wxml
中使用<canvas>
标签创建一个画布区域,供游戏渲染使用。 - 界面布局:在画布下方设计控制按钮(上、下、左、右、撤销、重新开始),以及步数和时间显示区域。
<view class='container'>
<!-- 关卡提示 -->
<view class='title'>第{{level}}关</view>
<view class='statusBar'>
<text>步数: {{stepCount}} 步</text>
<text>耗时: {{elapsedTime}} 秒</text>
</view>
<!-- 游戏画布 -->
<view class="canvasBox">
<canvas canvas-id='myCanvas' ></canvas>
</view>
<!-- 方向键 -->
<view class='btnBox'>
<button type='warn' bindtap='up' size="mini">↑</button>
<view>
<button type='warn' bindtap='left' size="mini">←</button>
<button type='warn' bindtap='down' size="mini">↓</button>
<button type='warn' bindtap='right' size="mini">→</button>
</view>
</view>
<button class='undoButton' type='warn' bindtap='undo'>撤销</button>
<button class='restartButton' type='warn' bindtap='restartGame'>重新开始</button>
</view>
4. 首页逻辑
首页的逻辑实现集中在关卡选择和跳转,具体步骤如下:
- 数据绑定:在
index.js
中绑定关卡数据,通过levels
数组管理可选择的关卡图像。 - 选择关卡:编写
chooseLevel
函数,当用户点击关卡按钮时,根据dataset.level
获取关卡信息,并使用wx.navigateTo
跳转到游戏页面。
var data=require('../../utils/data.js');
Page({
/**
* 页面的初始数据
*/
data: {
levels:[
'level01.png',
'level02.png',
'level03.png',
'level04.png'
]
},
/**
* 自定义函数--游戏选关
*/
chooseLevel: function (e) {
let level=e.currentTarget.dataset.level;
wx.navigateTo({
url:'../game/game?level='+level
})
}
})
5. 游戏页功能逻辑
5.1 游戏画面显示
-
地图初始化:在
onLoad
生命周期函数中,通过initMap
函数初始化地图数据,加载对应关卡的地图布局。 -
画布绘制:使用
drawCanvas
函数,根据地图数据绘制游戏画面,包括地图元素、箱子和角色。
// pages/game/game.js
var data=require('../../utils/data.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
Page({
/**
* 页面的初始数据
*/
data: {
level:1,
history: [],
stepCount: 0,
startTime: 0,
elapsedTime: 0,
timer: null
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 获取关卡
let level=options.level
// 更新页面关卡标题
this.setData({
level:parseInt(level)+1,
stepCount: 0,
startTime: Date.now(),
})
// 创建画布上下文
this.ctx=wx.createCanvasContext('myCanvas')
// 初始化地图数据
this.initMap(level)
// 绘制画布内容
this.drawCanvas()
this.data.timer = setInterval(() => {
const currentTime = Date.now();
const elapsed = Math.floor((currentTime - this.data.startTime) / 1000);
this.setData({
elapsedTime: elapsed
});
}, 1000);
},
/**
* 自定义函数--初始化地图数据
*/
initMap:function(level) {
// 读取原始的游戏地图数据
let mapData = data.maps[level]
// 使用双重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
}
}
}
},
/**
* 自定义函数--绘制地图
*/
drawCanvas:function() {
let ctx = this.ctx
// 清空画布
ctx.clearRect(0,0,320,320)
// 使用双重for循环绘制8*8的地图
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()
},
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
},
/**
* 自定义函数--游戏成功处理
*/
checkWin:function() {
if(this.isWin()){
wx.showModal({
title:'恭喜',
content:`游戏成功!\n步数: ${this.data.stepCount}\n耗时: ${this.data.elapsedTime} 秒`,
showCancel:false
})
}
},
})
5.2 游戏移动
-
方向键控制:实现方向键的逻辑(上、下、左、右),通过函数
up
、down
、left
、right
控制角色移动。 -
箱子推动逻辑:当角色遇到箱子时,判断箱子是否可以移动,并更新箱子和角色的坐标。
-
画面更新:每次移动后调用
drawCanvas
函数重新绘制画面。
/**
* 自定义函数--方向键上
*/
up:function() {
this.saveState();
// 不在最顶端才考虑上移
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.setData({
stepCount: this.data.stepCount + 1, // Increment step count
});
}
// 重新绘制地图
this.drawCanvas()
// 检查游戏是否成功
this.checkWin()
}
},
/**
* 自定义函数--方向键下
*/
down:function() {
this.saveState();
// 不在最底端才考虑下移
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.setData({
stepCount: this.data.stepCount + 1, // Increment step count
});
}
// 重新绘制地图
this.drawCanvas()
// 检查游戏是否成功
this.checkWin()
}
},
/**
*自定义函数--方向键左
*/
left:function() {
this.saveState();
// 不在最左侧才考虑左移
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
box[row][col-1]=0
// 更新当前小鸟坐标
col=col-1
}
}
this.setData({
stepCount: this.data.stepCount + 1, // Increment step count
});
}
// 重新绘制地图
this.drawCanvas()
// 检查游戏是否成功
this.checkWin()
}
},
/**
* 自定义函数--方向键:右
*/
right: function () {
this.saveState();
//如果不在最右侧才考虑右移
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
box[row][col + 1] = 0
//更新当前小鸟坐标
col = col + 1
}
}
this.setData({
stepCount: this.data.stepCount + 1, // Increment step count
});
}
//重新绘制地图
this.drawCanvas()
//检查游戏是否成功
this.checkWin()
}
},
5.3 返回上一步和重新开始
-
撤销功能:通过
saveState
函数保存每一步的状态,并在undo
函数中实现状态的回滚。 -
重新开始:编写
restartGame
函数,重置游戏的地图、计时器和步数,并重新开始关卡。
定义一个栈,用来存放每一步的地图位置信息
history: [],
saveState: function() {
this.data.history.push({
row: row,
col: col,
box: JSON.parse(JSON.stringify(box)) // 深拷贝
});
},
undo: function() {
if (this.data.history.length > 0) {
// 从堆栈中弹出最后一个状态
let lastState = this.data.history.pop();
row = lastState.row;
col = lastState.col;
box = lastState.box;
this.drawCanvas(); // 重新绘制画布
// 更新步数(撤销也算步数)
this.setData({
stepCount: this.data.stepCount + 1
});
} else {
wx.showToast({
title: '无法撤销',
icon: 'none',
duration: 2000
});
}
},
restartGame:function(){
// 停止现有的定时器
if (this.data.timer) {
clearInterval(this.data.timer);
this.setData({
timer: null
});
}
// 重置步数和耗时
this.setData({
stepCount: 0,
startTime: Date.now(),
elapsedTime: 0
});
// 重新启动定时器
this.data.timer = setInterval(() => {
const currentTime = Date.now();
const elapsed = Math.floor((currentTime - this.data.startTime) / 1000);
this.setData({
elapsedTime: elapsed
});
}, 1000);
// 初始化地图数据
this.initMap(this.data.level-1)
// 绘制画布内容
this.drawCanvas()
},
5.4 计时和步数统计
-
计时器:在
onLoad
中初始化游戏开始时间,并通过setInterval
每秒更新elapsedTime
,显示经过的时间。 -
步数统计:每次角色移动或撤销时,更新
stepCount
统计步数,在游戏结束时显示总步数和时间。
三、程序运行结果
选关界面
游戏界面可以移动、回到上一步和重新开始
游戏胜利后可以看到移动步数和完成时间
四、问题总结与体会
问题: 在实现游戏的计时和步数统计功能时,遇到了如何在游戏过程中准确地记录时间和步数的问题。特别是如何在游戏结束时展示这些统计数据,以及如何处理游戏重新开始的情况。
解决: 通过使用 setInterval
方法实现了计时器,每秒钟更新一次游戏经过时间。同时,通过在每次角色移动时增加步数计数器来记录步数。在游戏结束时,使用 wx.showModal
显示总时间和步数,并在游戏重新开始时重置这些统计数据。确保在页面卸载时清除计时器以防止内存泄漏。
onUnload: function() {
if (this.data.timer) {
clearInterval(this.data.timer);
this.setData({
timer: null
});
}
},
体会: 实现推箱子游戏不仅帮助我巩固了前端开发的知识,还让我学会了如何处理游戏逻辑、界面更新和用户交互。特别是在计时和步数统计功能的实现过程中,我掌握了如何利用定时器记录游戏时间,并通过设置状态管理和数据绑定来确保游戏的顺畅运行。这次实验提高了我在实际项目中的问题解决能力和编程技能,为今后的开发工作打下了坚实的基础。