鸿蒙开发案例:打地鼠

【引言】
打地鼠游戏是一款经典的休闲游戏,玩家需要在地鼠出现时快速点击它们以获得分数。使用鸿蒙框架创建组件、管理状态、实现基本的动画效果以及处理用户交互。本文将详细介绍游戏的结构、核心算法以及代码实现。注意完整代码在最后面。
【项目概述】
游戏的主要功能包括:
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) // 设置内容对齐方式 } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了