鸿蒙开发案例:打地鼠

【引言】

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

【项目概述】

游戏的主要功能包括:

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. 随机抽取算法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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. 停留时间检查算法

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

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

3. 游戏结束处理算法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(); // 结束游戏
}

【完整代码】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
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) // 设置内容对齐方式
  }
}

  

posted @   zhongcx  阅读(149)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示