2024移动软件开发——实验六

2024年夏季《移动软件开发》实验报告

一、实验目标

1、综合所学知识创建完整的推箱子游戏;2、能够在开发过程中熟练掌握真机预览、调试等操作。

二、实验步骤

1. 项目准备

在实验开始前,首先需要完成以下准备工作:

  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"
}
  1. 资源文件准备:收集并整理游戏所需的图像资源(如角色、箱子、地图块等),将其放入项目的 images 目录中。

在这里插入图片描述

  1. 数据文件准备:整理游戏关卡所需的地图数据,并将其存储在一个公共数据文件 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. 首页设计

首页作为游戏的入口界面,主要展示可选择的游戏关卡。设计步骤如下:

  1. 界面布局:在首页界面使用 wx:for 循环动态生成关卡选择按钮,并设计按钮的样式。
  2. 跳转逻辑:在用户点击某个关卡按钮后,触发 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. 游戏页面设计

游戏页面作为推箱子游戏的核心展示区域,设计步骤如下:

  1. 画布创建:在 game.wxml 中使用 <canvas> 标签创建一个画布区域,供游戏渲染使用。
  2. 界面布局:在画布下方设计控制按钮(上、下、左、右、撤销、重新开始),以及步数和时间显示区域。
<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. 首页逻辑

首页的逻辑实现集中在关卡选择和跳转,具体步骤如下:

  1. 数据绑定:在 index.js 中绑定关卡数据,通过 levels 数组管理可选择的关卡图像。
  2. 选择关卡:编写 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 游戏画面显示
  1. 地图初始化:在 onLoad 生命周期函数中,通过 initMap 函数初始化地图数据,加载对应关卡的地图布局。

  2. 画布绘制:使用 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 游戏移动
  1. 方向键控制:实现方向键的逻辑(上、下、左、右),通过函数 updownleftright 控制角色移动。

  2. 箱子推动逻辑:当角色遇到箱子时,判断箱子是否可以移动,并更新箱子和角色的坐标。

  3. 画面更新:每次移动后调用 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 返回上一步和重新开始
  1. 撤销功能:通过 saveState 函数保存每一步的状态,并在 undo 函数中实现状态的回滚。

  2. 重新开始:编写 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 计时和步数统计
  1. 计时器:在 onLoad 中初始化游戏开始时间,并通过 setInterval 每秒更新 elapsedTime,显示经过的时间。

  2. 步数统计:每次角色移动或撤销时,更新 stepCount 统计步数,在游戏结束时显示总步数和时间。

三、程序运行结果

选关界面

在这里插入图片描述

游戏界面可以移动、回到上一步和重新开始

在这里插入图片描述

游戏胜利后可以看到移动步数和完成时间

在这里插入图片描述

四、问题总结与体会

问题: 在实现游戏的计时和步数统计功能时,遇到了如何在游戏过程中准确地记录时间和步数的问题。特别是如何在游戏结束时展示这些统计数据,以及如何处理游戏重新开始的情况。

解决: 通过使用 setInterval 方法实现了计时器,每秒钟更新一次游戏经过时间。同时,通过在每次角色移动时增加步数计数器来记录步数。在游戏结束时,使用 wx.showModal 显示总时间和步数,并在游戏重新开始时重置这些统计数据。确保在页面卸载时清除计时器以防止内存泄漏。

onUnload: function() {
    if (this.data.timer) {
      clearInterval(this.data.timer);
      this.setData({
        timer: null
      });
    }
  },

体会: 实现推箱子游戏不仅帮助我巩固了前端开发的知识,还让我学会了如何处理游戏逻辑、界面更新和用户交互。特别是在计时和步数统计功能的实现过程中,我掌握了如何利用定时器记录游戏时间,并通过设置状态管理和数据绑定来确保游戏的顺畅运行。这次实验提高了我在实际项目中的问题解决能力和编程技能,为今后的开发工作打下了坚实的基础。

  • 16
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值