HarmonyOS NEXT 实战:从零开发「猜数字」小游戏
本文介绍了如何使用HarmonyOS NEXT的ArkTS框架开发一个经典的猜数字小游戏。文章从游戏规则设计入手,详细讲解了数据结构设计,包括状态变量定义和猜测记录类型。核心逻辑部分展示了新游戏初始化、提交猜测判断和快捷选择按钮的实现方法。界面构建方面,采用卡片式布局,包含输入区域、提示信息和历史记录列表。该实现涵盖了状态管理、列表渲染、条件渲染等核心技术,适合初学者学习HarmonyOS应用开发
HarmonyOS NEXT 实战:从零开发「猜数字」小游戏
经典游戏新玩法!本文将带你使用 HarmonyOS NEXT 的 ArkTS 框架,开发一个功能完整、交互友好的猜数字小游戏。涵盖状态管理、列表渲染、条件渲染、动画过渡等核心技术,适合初学者快速上手。
一、游戏设计思路
1.1 游戏规则
猜数字是一个经典的逻辑推理游戏:
- 系统随机生成 1-100 之间的整数
- 玩家输入猜测的数字
- 系统提示「大了」或「小了」
- 直到猜中为止,显示总猜测次数
1.2 功能需求
核心功能:
- 随机数生成
- 数字输入与验证
- 大小提示反馈
- 猜测历史记录
- 游戏重开
体验优化:
- 快捷数字按钮(常用数字一键填入)
- 猜中后的庆祝动画
- 输入框自动清空
- 猜中后禁用输入
1.3 界面设计
采用卡片式布局:
- 顶部:标题和副标题
- 中部:提示区域 + 输入区域(白色卡片)
- 快捷选择按钮组
- 猜测历史记录列表
- 底部:新游戏按钮(游戏结束后显示)
二、数据结构设计
2.1 状态变量定义
@State targetNumber: number = 0 // 目标数字(答案)
@State userInput: string = '' // 用户输入
@State attempts: number = 0 // 猜测次数
@State hintText: string = '输入 1-100 之间的数字' // 提示文字
@State hintColor: string = '#888888' // 提示颜色
@State isGameOver: boolean = false // 游戏是否结束
@State guessHistory: GuessRecord[] = [] // 猜测历史
@State showCelebration: boolean = false // 是否显示庆祝动画
状态分析:
targetNumber:每次新游戏随机生成,玩家不可见(开发时可打印日志调试)userInput:绑定输入框,玩家输入时实时更新attempts:每次提交猜测加 1hintText/hintColor:根据猜测结果动态变化,给玩家直观反馈isGameOver:控制界面状态,猜中后禁用输入、显示重开按钮guessHistory:数组存储每次猜测记录,用于列表渲染
2.2 猜测记录类型
interface GuessRecord {
guess: number // 猜测的数字
result: string // 结果描述("✅ 正确" / "⬆ 小了" / "⬇ 大了")
resultColor: string // 结果颜色
}
使用 TypeScript 接口定义数据结构,代码更清晰、IDE 提示更友好。
三、核心逻辑实现
3.1 新游戏初始化
newGame(): void {
this.targetNumber = Math.floor(Math.random() * 100) + 1
this.userInput = ''
this.attempts = 0
this.hintText = '输入 1-100 之间的数字'
this.hintColor = '#888888'
this.isGameOver = false
this.guessHistory = []
this.showCelebration = false
console.info(`[猜数字] 新游戏开始,答案是: ${this.targetNumber}`)
}
要点:
Math.random() * 100生成 0-99 的随机小数Math.floor()向下取整得到 0-99 的整数+1后范围变成 1-100- 重置所有状态变量,确保游戏干净开始
3.2 提交猜测逻辑
submitGuess(): void {
const guess = parseInt(this.userInput)
// 输入验证
if (isNaN(guess) || guess < 1 || guess > 100) {
this.hintText = '⚠️ 请输入 1-100 的整数'
this.hintColor = '#E67E22'
return
}
this.attempts++
let newHint = ''
let newColor = ''
let correct = false
// 判断大小
if (guess === this.targetNumber) {
newHint = `🎉 恭喜!就是 ${this.targetNumber}!用了 ${this.attempts} 次猜中!`
newColor = '#27AE60'
correct = true
this.isGameOver = true
this.showCelebration = true
} else if (guess < this.targetNumber) {
newHint = `👆 ${guess} 小了`
newColor = '#E74C3C'
} else {
newHint = `👇 ${guess} 大了`
newColor = '#E74C3C'
}
this.hintText = newHint
this.hintColor = newColor
// 记录历史(最新在前)
this.guessHistory = [
{
guess: guess,
result: correct ? '✅ 正确' : (guess < this.targetNumber ? '⬆ 小了' : '⬇ 大了'),
resultColor: correct ? '#27AE60' : '#E74C3C'
},
...this.guessHistory
]
this.userInput = '' // 清空输入框
}
逻辑流程:
- 输入验证:使用
parseInt转换,isNaN检查有效性,范围判断确保 1-100 - 次数累加:每次有效猜测都
attempts++ - 结果判断:
- 相等:恭喜 + 游戏结束 + 庆祝动画
- 小于:提示「小了」
- 大于:提示「大了」
- 历史记录:使用数组展开运算符
...将新记录插入到数组开头 - 清空输入:方便玩家继续猜测
3.3 快捷选择按钮
@Builder
quickButton(value: number): void {
Button(`${value}`)
.width(58)
.height(36)
.backgroundColor('#EEEEEE')
.borderRadius(8)
.fontSize(14)
.fontColor('#555555')
.fontWeight(FontWeight.Medium)
.onClick(() => {
if (!this.isGameOver) {
this.userInput = value.toString()
}
})
}
设计思路:
- 使用
@Builder装饰器封装可复用组件 - 传入数字参数,生成对应按钮
- 点击后填入输入框,玩家可以修改或直接提交
按钮布局:
Row({ space: 8 }) {
this.quickButton(10)
this.quickButton(25)
this.quickButton(50)
this.quickButton(75)
this.quickButton(90)
}
选择了一些常用数字:边界值(1、100)、中间值(50)、特殊值(25、75、90)等。
四、主界面构建
4.1 整体布局结构
build() {
Column() {
Scroll() {
Column({ space: 12 }) {
// 标题区域
Text('🎯 猜数字')
.fontSize(28)
.fontWeight(FontWeight.Bold)
Text('猜一个 1 ~ 100 之间的数字')
.fontSize(14)
.fontColor('#999999')
// 输入区域卡片
Column({ space: 16 }) {
// 提示文字
// 输入框 + 按钮
// 庆祝动画
}
// 快捷选择按钮
// 新游戏按钮
// 猜测历史
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
.scrollable(ScrollDirection.Vertical)
}
.width('100%')
.height('100%')
.backgroundColor('#F0F2F5')
}
布局特点:
- 最外层
Column作为容器,设置浅灰背景 Scroll包裹内容,防止小屏幕溢出- 内部
Column居中对齐,卡片宽度 85%
4.2 输入区域卡片
Column({ space: 16 }) {
// 提示文字
Text(this.hintText)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor(this.hintColor)
.textAlign(TextAlign.Center)
.width('100%')
.animation({ duration: 300 })
// 已猜次数
if (this.attempts > 0) {
Text(`已猜 ${this.attempts} 次`)
.fontSize(13)
.fontColor('#AAAAAA')
}
// 输入框 + 提交按钮
Row({ space: 10 }) {
TextInput({ text: this.userInput, placeholder: '输入数字...' })
.type(InputType.Number)
.maxLength(3)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.height(50)
.layoutWeight(1)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.onChange((val: string) => {
this.userInput = val
})
.onSubmit(() => {
if (!this.isGameOver) {
this.submitGuess()
}
})
.enabled(!this.isGameOver)
Button(this.isGameOver ? '🔄' : '↵')
.width(50)
.height(50)
.backgroundColor(this.isGameOver ? '#3498DB' : '#2D3436')
.borderRadius(12)
.fontSize(22)
.fontColor('#FFFFFF')
.onClick(() => {
if (this.isGameOver) {
this.newGame()
} else {
this.submitGuess()
}
})
}
.width('100%')
// 猜中后的庆祝动画
if (this.showCelebration) {
Text('✨ 🌟 ⭐ 🌟 ✨')
.fontSize(24)
.textAlign(TextAlign.Center)
.width('100%')
.margin({ top: 4 })
.transition({ type: TransitionType.Insert, scale: { x: 0, y: 0 } })
}
}
.width('85%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.shadow({ radius: 6, color: '#1A000000', offsetY: 3 })
卡片设计:
- 白色背景 + 圆角 + 阴影,层次分明
TextInput使用InputType.Number只弹出数字键盘maxLength(3)限制最多 3 位(100 也是 3 位)onChange实时更新userInputonSubmit支持回车键提交enabled(!this.isGameOver)猜中后禁用输入
按钮状态:
- 游戏中:黑色,显示 ↵,点击提交
- 结束后:蓝色,显示 🔄,点击新游戏
4.3 快捷选择区域
if (!this.isGameOver) {
Column() {
Text('快捷选择')
.fontSize(14)
.fontColor('#AAAAAA')
.margin({ bottom: 8 })
Row({ space: 8 }) {
this.quickButton(10)
this.quickButton(25)
this.quickButton(50)
this.quickButton(75)
this.quickButton(90)
}
.width('100%')
Row({ space: 8 }) {
this.quickButton(1)
this.quickButton(33)
this.quickButton(66)
this.quickButton(99)
this.quickButton(100)
}
.width('100%')
}
.width('85%')
.margin({ bottom: 8 })
}
游戏结束后隐藏快捷按钮,界面更简洁。
4.4 新游戏按钮
if (this.isGameOver) {
Button('🔄 再来一局')
.width('85%')
.height(48)
.backgroundColor('#3498DB')
.borderRadius(14)
.fontSize(17)
.fontColor('#FFFFFF')
.onClick(() => {
this.newGame()
})
}
只有游戏结束后才显示,防止玩家误触重开。
4.5 猜测历史列表
if (this.guessHistory.length > 0) {
Column({ space: 8 }) {
Text('📋 猜测记录')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#666666')
.width('100%')
Column({ space: 4 }) {
ForEach(this.guessHistory, (item: GuessRecord) => {
Row() {
Text(`#${this.guessHistory.length - this.guessHistory.indexOf(item)}`)
.fontSize(14)
.fontColor('#AAAAAA')
.width(36)
Text(`${item.guess}`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2D3436')
.width(60)
Text(item.result)
.fontSize(15)
.fontColor(item.resultColor)
.fontWeight(FontWeight.Medium)
}
.width('100%')
.padding({ top: 8, bottom: 8, left: 12, right: 12 })
.backgroundColor('#FAFAFA')
.borderRadius(10)
})
}
}
.width('85%')
.margin({ top: 8, bottom: 32 })
}
列表渲染:
- 使用
ForEach遍历guessHistory数组 - 每条记录显示序号、猜测数字、结果
- 序号计算:
数组长度 - 当前索引(因为新记录在数组开头)
五、生命周期回调
5.1 aboutToAppear 初始化
aboutToAppear(): void {
this.newGame()
}
组件即将显示时,调用 newGame() 初始化游戏。
六、踩坑记录与解决方案
6.1 输入框无法清空
问题:调用 this.userInput = '' 后,输入框仍有内容。
原因:TextInput 的 text 属性是单向绑定。
解决方案:使用双向绑定语法或手动触发更新。在 ArkTS 中,onChange 会同步更新状态变量,设置 this.userInput = '' 后输入框会自动清空。
6.2 猜测历史序号错误
问题:历史记录显示的序号是倒序(最新的显示 #1)。
分析:因为新记录插入到数组开头,indexOf 返回的是数组索引(0 是最新的)。
解决方案:
Text(`#${this.guessHistory.length - this.guessHistory.indexOf(item)}`)
用数组长度减去索引,得到正确的序号。
6.3 庆祝动画不显示
问题:猜中后庆祝动画没有出现。
原因:transition 动画需要配合条件渲染。
解决方案:
if (this.showCelebration) {
Text('✨ 🌟 ⭐ 🌟 ✨')
.transition({ type: TransitionType.Insert, scale: { x: 0, y: 0 } })
}
确保使用 if 条件渲染,transition 才会生效。
6.4 游戏结束后仍可输入
问题:猜中后还能继续猜测。
解决方案:
TextInput(...)
.enabled(!this.isGameOver)
Button(...)
.onClick(() => {
if (!this.isGameOver) {
this.submitGuess()
}
})
双重保护:禁用输入框 + 点击事件判断。
6.5 快捷按钮点击无效
问题:点击快捷按钮后输入框没有变化。
原因:忘记判断游戏状态。
解决方案:
.onClick(() => {
if (!this.isGameOver) {
this.userInput = value.toString()
}
})
七、功能扩展思路
7.1 难度选择
enum Difficulty {
EASY = { min: 1, max: 50 },
MEDIUM = { min: 1, max: 100 },
HARD = { min: 1, max: 500 }
}
添加难度选择界面,不同难度对应不同数字范围。
7.2 计时功能
@State startTime: number = 0
@State elapsedTime: number = 0
// 开始计时
startTimer(): void {
this.startTime = Date.now()
this.timerId = setInterval(() => {
this.elapsedTime = Math.floor((Date.now() - this.startTime) / 1000)
}, 1000)
}
记录猜测用时,增加挑战性。
7.3 最佳记录
interface GameRecord {
attempts: number
time: number
date: string
}
@State bestRecords: GameRecord[] = []
保存历史最佳成绩,激励玩家挑战。
7.4 提示功能
getHint(): string {
if (this.attempts > 5) {
const diff = this.targetNumber - parseInt(this.userInput)
if (Math.abs(diff) <= 10) {
return '💡 非常接近了!'
}
}
return ''
}
猜测多次后给出额外提示。
八、运行效果展示

九、总结
通过这个猜数字游戏的开发,我们学习了:
- 状态管理:使用
@State管理游戏状态,实现响应式 UI - 条件渲染:根据游戏状态显示/隐藏不同组件
- 列表渲染:使用
ForEach渲染猜测历史 - 组件封装:使用
@Builder封装快捷按钮 - 输入处理:
TextInput的双向绑定与验证 - 动画效果:
transition实现庆祝动画
这个项目虽然简单,但涵盖了鸿蒙开发的基础知识,非常适合初学者练手。建议动手敲一遍代码,加深理解。
项目信息
- 包名:
com.example.myapplication - API 版本:API 23
- 开发工具:DevEco Studio
相关链接:
如果这篇文章对你有帮助,欢迎点赞收藏!有问题欢迎在评论区讨论~
更多推荐
所有评论(0)