鸿蒙开发案例:打地鼠_重置

【引言】

打地鼠游戏是一款经典的休闲游戏,玩家需要在地鼠出现时快速点击它们以获得分数。使用鸿蒙框架创建组件、管理状态、实现基本的动画效果以及处理用户交互。本文将详细介绍游戏的结构、核心算法以及代码实现。注意完整代码在最后面。

【项目概述】

游戏的主要功能包括:

1. 地鼠组件的定义:通过Hamster结构体定义了地鼠的外观,包括身体、眼睛等各个部分的样式,并支持根据单元格的宽度动态调整地鼠的尺寸。

2. 单元格类Cell:定义了游戏中的单个单元格,它具有表示地鼠是否显示的状态,并可以设置显示地鼠时的缩放选项。此外,Cell类中还包含了一些方法,比如setSelectedTrueTime()用于设置地鼠显示的时间戳,checkTime()则用来检测地鼠是否应该因为超过了预定的停留时间而被隐藏。

3. 游戏主组件Index:这是游戏的主要入口组件,它维护了游戏的核心状态,如动画间隔、出现的地鼠数量、地鼠的停留时间等。此外,它还包括了开始游戏(startGame)和结束游戏(endGame)的方法,这些方法负责初始化游戏状态和重置游戏数据。

4. 游戏界面构建:在Index组件的build方法中,定义了游戏的界面布局,包括显示计时器、得分板以及游戏区域内的各个单元格。

5. 时间控制与地鼠显示逻辑:通过TextTimer组件来控制游戏的时间,每经过一定的时间间隔,就会随机选择一些单元格显示地鼠。同时,游戏逻辑还包括了在地鼠被点击时增加玩家的得分,并执行相应的动画效果。

6. 用户交互:用户可以通过点击显示地鼠的单元格来获得分数,点击事件触发后,地鼠会被隐藏,并且游戏得分会被更新。

综上所述,该代码提供了一个完整的打地鼠游戏框架,包括地鼠的外观设计、游戏逻辑处理、时间控制以及用户交互等多个方面的功能。

【环境准备】

电脑系统:windows 10

开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

工程版本:API 12

真机:Mate 60 Pro

语言:ArkTS、ArkUI

【算法分析】

1. 随机抽取算法

在游戏中,需要随机选择多个地鼠出现的位置。通过洗牌算法随机抽取可用位置的索引。

let availableIndexList: number[] = []; // 存储可用的索引
for (let i = 0; i < this.cells.length; i++) {
  if (!this.cells[i].isSelected) {
    availableIndexList.push(i); // 添加到可用索引列表
  }
}

// 洗牌算法
for (let i = 0; i < availableIndexList.length; i++) {
  let index = Math.floor(Math.random() * (availableIndexList.length - i));
  let temp = availableIndexList[availableIndexList.length - i - 1];
  availableIndexList[availableIndexList.length - i - 1] = availableIndexList[index];
  availableIndexList[index] = temp;
}

2. 停留时间检查算法

在每个时间间隔内检查地鼠的停留时间,如果超过设定的停留时间,则将地鼠隐藏。

if (elapsedTime % 10 == 0) { // 每间隔100毫秒检查一次
  for (let i = 0; i < this.cells.length; i++) {
    this.cells[i].checkTime(this.hamsterStayDuration); // 检查每个单元格的停留时间
  }
}

3. 游戏结束处理算法

当游戏时间结束时,显示得分并重置游戏状态。

if (elapsedTime * 10 == this.gameDuration) { // 如果计时结束
  let currentScore = this.currentScore; // 获取当前得分
  this.getUIContext().showAlertDialog({ // 显示结果对话框
    title: '游戏结束',
    message: `得分:${currentScore}`,
    confirm: {
      defaultFocus: true,
      value: '我知道了',
      action: () => {}
    },
    alignment: DialogAlignment.Center,
  });
  this.endGame(); // 结束游戏
}

【完整代码】

import { curves, window } from '@kit.ArkUI' // 导入所需的库和模块

// 定义地鼠组件
@Component
struct Hamster {
  @Prop cellWidth: number // 定义一个属性,表示单元格的宽度

  build() {
    Stack() { // 创建一个堆叠布局
      // 身体
      Text()
        .width(`${this.cellWidth / 2}lpx`) // 设置宽度为单元格宽度的一半
        .height(`${this.cellWidth / 3 * 2}lpx`) // 设置高度为单元格高度的2/3
        .backgroundColor("#b49579") // 设置背景颜色
        .borderRadius({ topLeft: '50%', topRight: '50%' }) // 设置圆角
        .borderColor("#2a272d") // 设置边框颜色
        .borderWidth(1) // 设置边框宽度
      // 嘴巴
      Ellipse()
        .width(`${this.cellWidth / 4}lpx`) // 设置嘴巴的宽度
        .height(`${this.cellWidth / 5}lpx`) // 设置嘴巴的高度
        .fillOpacity(1) // 设置填充不透明度
        .fill("#e7bad7") // 设置填充颜色
        .stroke("#563e3f") // 设置边框颜色
        .strokeWidth(1) // 设置边框宽度
        .margin({ top: `${this.cellWidth / 6}lpx` }) // 设置上边距
      // 左眼睛
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`) // 设置左眼睛的宽度
        .height(`${this.cellWidth / 6}lpx`) // 设置左眼睛的高度
        .fillOpacity(1) // 设置填充不透明度
        .fill("#313028") // 设置填充颜色
        .stroke("#2e2018") // 设置边框颜色
        .strokeWidth(1) // 设置边框宽度
        .margin({ bottom: `${this.cellWidth / 3}lpx`, right: `${this.cellWidth / 6}lpx` }) // 设置下边距和右边距
      // 右眼睛
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`) // 设置右眼睛的宽度
        .height(`${this.cellWidth / 6}lpx`) // 设置右眼睛的高度
        .fillOpacity(1) // 设置填充不透明度
        .fill("#313028") // 设置填充颜色
        .stroke("#2e2018") // 设置边框颜色
        .strokeWidth(1) // 设置边框宽度
        .margin({ bottom: `${this.cellWidth / 3}lpx`, left: `${this.cellWidth / 6}lpx` }) // 设置下边距和左边距
      // 左眼瞳
      Ellipse()
        .width(`${this.cellWidth / 20}lpx`) // 设置左眼瞳的宽度
        .height(`${this.cellWidth / 15}lpx`) // 设置左眼瞳的高度
        .fillOpacity(1) // 设置填充不透明度
        .fill("#fefbfa") // 设置填充颜色
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, right: `${this.cellWidth / 6}lpx` }) // 设置下边距和右边距
      // 右眼瞳
      Ellipse()
        .width(`${this.cellWidth / 20}lpx`) // 设置右眼瞳的宽度
        .height(`${this.cellWidth / 15}lpx`) // 设置右眼瞳的高度
        .fillOpacity(1) // 设置填充不透明度
        .fill("#fefbfa") // 设置填充颜色
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, left: `${this.cellWidth / 6}lpx` }) // 设置下边距和左边距
    }.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`) // 设置组件的宽度和高度
  }
}

// 定义单元格类
@ObservedV2
class Cell {
  @Trace scaleOptions: ScaleOptions = { x: 1, y: 1 }; // 定义缩放选项
  @Trace isSelected: boolean = false // true表示显示地鼠,false表示隐藏地鼠
  cellWidth: number // 单元格宽度
  selectTime: number = 0 // 选择时间

  constructor(cellWidth: number) { // 构造函数
    this.cellWidth = cellWidth // 初始化单元格宽度
  }

  setSelectedTrueTime() { // 设置选择时间
    this.selectTime = Date.now() // 记录当前时间
    this.isSelected = true // 设置为选中状态
  }

  checkTime(stayDuration: number) { // 检查停留时间
    if (this.isSelected) { // 如果当前是选中状态
      if (Date.now() - this.selectTime >= stayDuration) { // 如果停留时间超过设定值
        this.selectTime = 0 // 重置选择时间
        this.isSelected = false // 设置为未选中状态
      }
    }
  }
}

// 定义文本计时器修饰符类
class MyTextTimerModifier implements ContentModifier<TextTimerConfiguration> {
  constructor() {}

  applyContent(): WrappedBuilder<[TextTimerConfiguration]> { // 应用内容
    return wrapBuilder(buildTextTimer) // 返回构建文本计时器的函数
  }
}

// 构建文本计时器的函数
@Builder
function buildTextTimer(config: TextTimerConfiguration) {
  Column() {
    Stack({ alignContent: Alignment.Center }) { // 创建一个堆叠布局,内容居中对齐
      Circle({ width: 150, height: 150 }) // 创建一个圆形
        .fill(config.started ? (config.isCountDown ? 0xFF232323 : 0xFF717171) : 0xFF929292) // 根据状态设置填充颜色
      Column() {
        Text(config.isCountDown ? "倒计时" : "正计时").fontColor(Color.White) // 显示计时状态
        Text(
          (config.isCountDown ? "剩余" : "已经过去了") + (config.isCountDown ?
          (Math.max(config.count / 1000 - config.elapsedTime / 100, 0)).toFixed(0) // 计算剩余时间
            : ((config.elapsedTime / 100).toFixed(0)) // 计算已过去时间
          ) + "秒"
        ).fontColor(Color.White) // 显示时间
      }
    }
  }
}

// 定义游戏主组件
@Entry
@Component
struct Index {
  @State animationIntervalCount: number = 0 // 动画间隔计数
  @State appearanceCount: number = 4 // 每次出现的地鼠数量
  @State animationInterval: number = 1000 // 地鼠出现的间隔时间
  @State hamsterStayDuration: number = 1500 // 地鼠停留时间
  @State gameDuration: number = 30000 // 游戏总时长
  @State randomPositionIndex: number = 0 // 随机位置
  @State cells: Cell[] = [] // 存储地鼠单元格
  @State cellWidth: number = 100 // 单元格宽度
  @State currentScore: number = 0 // 当前游戏得分
  @State timerModifier: MyTextTimerModifier = new MyTextTimerModifier() // 计时器修饰符
  countdownTimerController: TextTimerController = new TextTimerController() // 倒计时控制器
  timerController: TextTimerController = new TextTimerController() // 正计时控制器

  aboutToAppear(): void {
    // 设置当前app以横屏方式显示
    window.getLastWindow(getContext()).then((windowClass) => {
      windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE) // 设置为横屏
    })
    // 显示10个地鼠坑位
    for (let i = 0; i < 10; i++) {
      this.cells.push(new Cell(this.cellWidth)) // 初始化10个单元格
    }
  }

  endGame() { // 结束游戏
    this.animationIntervalCount = 0 // 重置动画间隔计数
    this.currentScore = 0 // 重置得分
    for (let i = 0; i < this.cells.length; i++) {
      this.cells[i].isSelected = false // 隐藏所有地鼠
    }
    this.countdownTimerController.reset() // 重置倒计时
    this.timerController.reset() // 重置正计时
  }

  startGame() { // 开始游戏
    this.endGame() // 结束当前游戏,重置所有状态
    this.countdownTimerController.start() // 启动倒计时控制器
    this.timerController.start() // 启动正计时控制器
  }

  build() { // 构建游戏界面
    Row() { // 创建一个水平布局
      // 显示时间与得分
      Column({ space: 30 }) { // 创建一个垂直布局,设置间距
        // 总时长
        Column({ space: 5 }) { // 创建一个垂直布局,设置间距
          Text(`倒计时长(秒)`).fontColor(Color.Black) // 显示倒计时长度的文本
          Counter() { // 创建一个计数器组件
            Text(`${this.gameDuration / 1000}`) // 显示游戏总时长(秒)
              .fontColor(Color.Black) // 设置字体颜色
          }
          .width(300) // 设置计数器宽度
          .onInc(() => { // 增加按钮的点击事件
            this.gameDuration += 1000; // 每次增加1秒
          }).onDec(() => { // 减少按钮的点击事件
            this.gameDuration -= 1000; // 每次减少1秒
            this.gameDuration = this.gameDuration < 1000 ? 1000 : this.gameDuration; // 确保最小值为1秒
          });
        }

        // 每次出现个数
        Column({ space: 5 }) { // 创建一个垂直布局,设置间距
          Text(`每次出现(个)`).fontColor(Color.Black) // 显示每次出现的地鼠数量的文本
          Counter() { // 创建一个计数器组件
            Text(`${this.appearanceCount}`) // 显示每次出现的地鼠数量
              .fontColor(Color.Black) // 设置字体颜色
          }
          .width(300) // 设置计数器宽度
          .onInc(() => { // 增加按钮的点击事件
            this.appearanceCount += 1; // 每次增加1个
          }).onDec(() => { // 减少按钮的点击事件
            this.appearanceCount -= 1; // 每次减少1个
            this.appearanceCount = this.appearanceCount < 1 ? 1 : this.appearanceCount; // 确保最小值为1
          });
        }

        // 地鼠每隔多长时间显示
        Column({ space: 5 }) { // 创建一个垂直布局,设置间距
          Text(`出现间隔(毫秒)`).fontColor(Color.Black) // 显示地鼠出现间隔的文本
          Counter() { // 创建一个计数器组件
            Text(`${this.animationInterval}`) // 显示地鼠出现的间隔时间
              .fontColor(Color.Black) // 设置字体颜色
          }
          .width(300) // 设置计数器宽度
          .onInc(() => { // 增加按钮的点击事件
            this.animationInterval += 100; // 每次增加100毫秒
          }).onDec(() => { // 减少按钮的点击事件
            this.animationInterval -= 100; // 每次减少100毫秒
            this.animationInterval = this.animationInterval < 100 ? 100 : this.animationInterval; // 确保最小值为100毫秒
          });
        }

        // 地鼠停留时间
        Column({ space: 5 }) { // 创建一个垂直布局,设置间距
          Text(`停留间隔(毫秒)`).fontColor(Color.Black) // 显示地鼠停留时间的文本
          Counter() { // 创建一个计数器组件
            Text(`${this.hamsterStayDuration}`) // 显示地鼠的停留时间
              .fontColor(Color.Black) // 设置字体颜色
          }
          .width(300) // 设置计数器宽度
          .onInc(() => { // 增加按钮的点击事件
            this.hamsterStayDuration += 100; // 每次增加100毫秒
          }).onDec(() => { // 减少按钮的点击事件
            this.hamsterStayDuration -= 100; // 每次减少100毫秒
            this.hamsterStayDuration = this.hamsterStayDuration < 100 ? 100 : this.hamsterStayDuration; // 确保最小值为100毫秒
          });
        }
      }.layoutWeight(1).padding({ left: 50 }) // 设置布局权重和左边距

      // 游戏区
      Flex({ wrap: FlexWrap.Wrap }) { // 创建一个可换行的弹性布局
        ForEach(this.cells, (cell: Cell, index: number) => { // 遍历所有单元格
          Stack() { // 创建一个堆叠布局
            // 洞
            Ellipse()
              .width(`${this.cellWidth / 1.2}lpx`) // 设置洞的宽度
              .height(`${this.cellWidth / 2.2}lpx`) // 设置洞的高度
              .fillOpacity(1) // 设置填充不透明度
              .fill("#020101") // 设置填充颜色
              .stroke("#020101") // 设置边框颜色
              .strokeWidth(1) // 设置边框宽度
              .margin({ top: `${this.cellWidth / 2}lpx` }) // 设置上边距
            // 地鼠
            Hamster({ cellWidth: this.cellWidth }) // 创建地鼠组件
              .visibility(cell.isSelected ? Visibility.Visible : Visibility.None) // 根据状态设置可见性
              .scale(cell.scaleOptions) // 设置缩放选项
          }.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`) // 设置堆叠布局的宽度和高度
          .margin({ left: `${index == 0 || index == 7 ? this.cellWidth / 2 : 0}lpx` }) // 设置左边距
          .onClick(() => { // 点击事件
            if (cell.isSelected) { // 如果当前单元格是选中状态
              animateToImmediately({ // 执行动画
                duration: 200, // 动画持续时间
                curve: curves.springCurve(10, 1, 228, 30), // 动画曲线
                onFinish: () => { // 动画结束后的回调
                  cell.isSelected = false // 隐藏地鼠
                  cell.scaleOptions = { x: 1.0, y: 1.0 }; // 重置缩放
                  this.currentScore += 1 // 增加得分
                }
              }, () => {
                cell.scaleOptions = { x: 0, y: 0 }; // 动画开始时缩放到0
              })
            }
          })
        })
      }.width(`${this.cellWidth * 4}lpx`) // 设置游戏区的宽度

      // 操作按钮
      Column({ space: 20 }) { // 创建一个垂直布局,设置间距
        // 倒计时
        TextTimer({ isCountDown: true, count: this.gameDuration, controller: this.countdownTimerController }) // 创建倒计时组件
          .contentModifier(this.timerModifier) // 应用计时器修饰符
          .onTimer((utc: number, elapsedTime: number) => { // 定义计时器的回调
            // 每隔指定时间随机显示地鼠
            if (elapsedTime * 10 >= this.animationInterval * this.animationIntervalCount) { // 判断是否达到显示地鼠的时间
              this.animationIntervalCount++ // 增加动画间隔计数

              // 获取可以出现的位置集合
              let availableIndexList: number[] = [] // 存储可用的索引
              for (let i = 0; i < this.cells.length; i++) { // 遍历所有单元格
                if (!this.cells[i].isSelected) { // 如果当前单元格未被选中
                  availableIndexList.push(i) // 添加到可用索引列表
                }
              }
              // 根据每次出现次数 appearanceCount 利用洗牌算法随机抽取
              for (let i = 0; i < availableIndexList.length; i++) { // 遍历可用索引列表
                let index = Math.floor(Math.random() * (availableIndexList.length - i)) // 随机选择一个索引
                let temp = availableIndexList[availableIndexList.length - i - 1] // 交换位置
                availableIndexList[availableIndexList.length - i - 1] = availableIndexList[index]
                availableIndexList[index] = temp
              }
              // 随机抽取 appearanceCount,取前几个已经打乱好的顺序
              for (let i = 0; i < availableIndexList.length; i++) { // 遍历可用索引列表
                if (i < this.appearanceCount) { // 如果索引小于每次出现的数量
                  this.cells[availableIndexList[i]].setSelectedTrueTime() // 设置选中的单元格为显示状态
                }
              }
            }
            if (elapsedTime % 10 == 0) { // 每隔100毫秒检查一次
              console.info('检查停留时间是否已过,如果过了就隐藏地鼠') // 输出调试信息
              for (let i = 0; i < this.cells.length; i++) { // 遍历所有单元格
                this.cells[i].checkTime(this.hamsterStayDuration) // 检查每个单元格的停留时间
              }
            }
            if (elapsedTime * 10 >= this.gameDuration) { // 如果计时结束
              let currentScore = this.currentScore // 获取当前得分
              this.getUIContext().showAlertDialog({ // 显示结果对话框
                // 显示结果页
                title: '游戏结束', // 对话框标题
                message: `得分:${currentScore}`, // 显示得分信息
                confirm: { // 确认按钮配置
                  defaultFocus: true, // 默认焦点
                  value: '我知道了', // 按钮文本
                  action: () => { // 点击后的动作
                    // 这里可以添加点击确认后的逻辑
                  }
                },
                onWillDismiss: () => { // 关闭前的动作
                  // 这里可以添加关闭前的逻辑
                },
                alignment: DialogAlignment.Center, // 对齐方式为中心
              });
              this.endGame() // 结束游戏
            }
          })
        Text(`当前得分:${this.currentScore}`) // 显示当前得分
        Button('开始游戏').clickEffect({ level: ClickEffectLevel.LIGHT }).onClick(() => { // 创建开始游戏按钮
          this.startGame() // 点击后开始游戏
        })
        Button('结束游戏').clickEffect({ level: ClickEffectLevel.LIGHT }).onClick(() => { // 创建结束游戏按钮
          this.endGame() // 点击后结束游戏
        })
      }.layoutWeight(1) // 设置布局权重
    }
    .height('100%') // 设置整体高度为100%
    .width('100%') // 设置整体宽度为100%
    .backgroundColor("#61ac57") // 设置背景颜色
    .justifyContent(FlexAlign.SpaceBetween) // 设置内容对齐方式
  }
}