学ArkUI16—— 手把手教你开发猜数字游戏App
🎯 新手零基础学ArkUI16—— 手把手教你开发猜数字游戏App
📖 应用场景
“你想在 App 里加一个休闲小游戏来提升用户粘性……”
“老师布置作业:用 ArkUI 实现一个带动画的互动游戏……”
猜数字游戏 是最经典的编程入门项目之一,也是你从"工具类 App"迈向"游戏/互动类 App"的第一步。通过这个项目,你将学到:
| 技术点 | 说明 |
|---|---|
| 游戏状态机 | idle → playing → won → idle 完整游戏循环 |
animateTo 动画 |
显式动画控制数字跳动、卡片翻转 |
| 粒子效果模拟 | 用多个 Text 模拟胜利撒花/彩带 |
| 键盘控制 | TextInput 的数字键盘 + 输入控制 |
| 声音/振动 | vibrator 模块提示用户操作 |
| 分数系统 | 最少次数记录 + 最高分排行榜 |
| 难度选择 | 简单(1-50) / 普通(1-100) / 困难(1-500) |
Canvas 基础 |
游戏进度可视化 |
Timer 定时器 |
猜题计时挑战模式 |
| 数据持久化 | 最高分保存在 AppStorage 中 |
⚙️ 运行环境要求
| 项目 | 要求 |
|---|---|
| IDE | DevEco Studio 4.0 Release 以上 |
| SDK | HarmonyOS SDK API 9+ |
| 调试设备 | 模拟器或真机均可 |
| 前置知识 | 本系列前三篇基础 |
🧩 实战小案例:猜数字游戏
📸 最终效果预览
┌─────────────────────────────────────┐
│ 🎯 猜数字游戏 ⚙️ 🏆 │
├─────────────────────────────────────┤
│ ┌──────────┐ │
│ │ ? ? │ │
│ │ 猜数字 │ │
│ └──────────┘ │
│ │
│ 范围: 1 ~ 100 难度: 普通 │
│ 已猜: 0 次 最佳: 5 次 │
│ │
│ ┌──────────────────────────┐ │
│ │ 请输入你的数字 │ │
│ │ [ 50 ] │ │
│ │ │ │
│ │ [ 🚀 猜一下!] │ │
│ └──────────────────────────┘ │
│ │
│ ⬆️ 小了!再往大了猜 │
│ 剩余范围: 50 ~ 100 │
│ │
│ 上次猜的: 25 ❌ 75 ❌ 62 ❌ │
│ │
│ [ 🔄 重新开始] │
└─────────────────────────────────────┘
🧠 知识点预习:游戏状态机
游戏开发的核心概念是状态机——游戏在任何时刻都处于一个确定的状态:
┌──────────┐
│ 空闲中 │ ← 刚打开App / 一局结束
└────┬─────┘
│ 点击"开始游戏"
▼
┌──────────┐
│ 游戏进行 │ ← 正在猜数字
└────┬─────┘
│ 猜对了!
▼
┌──────────┐
│ 胜利! │ ← 显示庆祝动画
└────┬─────┘
│ 点击"再来一局"
▼
┌──────────┐
│ 空闲中 │ ← 回到起点
└──────────┘
💡 最佳实践: 状态机让游戏逻辑变得清晰可控。每个状态只处理该状态下的交互,不会出现"猜对了还能继续猜"的 bug。
🧱 第一步:定义游戏数据类型
// ===== 游戏状态枚举 =====
enum GameState {
IDLE = 'idle', // 空闲:等待开始
PLAYING = 'playing', // 游戏中
CORRECT = 'correct', // 猜对了(胜利动画)
GAME_OVER = 'gameover' // 超时/次数用完
}
// ===== 难度等级 =====
interface Difficulty {
name: string; // 难度名称
maxNumber: number; // 数字范围上限
maxAttempts: number; // 最大尝试次数
description: string; // 描述
points: number; // 胜利加分
}
// ===== 单次猜测记录 =====
interface GuessRecord {
number: number; // 猜的数字
result: 'high' | 'low' | 'correct'; // 结果
timestamp: number; // 时间戳
}
// ===== 游戏数据 =====
interface GameData {
targetNumber: number; // 目标数字
attempts: number; // 已猜次数
lowBound: number; // 当前下限
highBound: number; // 当前上限
records: GuessRecord[]; // 猜测历史
bestScore: number; // 历史最佳
startTime: number; // 开始时间
}
难度配置
const DIFFICULTIES: Difficulty[] = [
{
name: '简单', maxNumber: 50, maxAttempts: 10,
description: '1~50,10次机会', points: 10
},
{
name: '普通', maxNumber: 100, maxAttempts: 7,
description: '1~100,7次机会', points: 20
},
{
name: '困难', maxNumber: 500, maxAttempts: 10,
description: '1~500,10次机会', points: 50
},
];
🔑 游戏设计思考: 为什么普通难度只有 7 次?因为二分查找法在 1~100 范围内最多需要
⌈log₂(100)⌉ = 7次。如果你会二分法,7 次一定猜对。这就是"运气 vs 策略"的游戏平衡设计。
🏗️ 第二步:状态管理
@Component
struct GuessGame {
// ===== 游戏核心状态 =====
@State gameState: GameState = GameState.IDLE;
@State gameData: GameData = this.createNewGame();
// ===== UI 状态 =====
@State guessInput: string = '';
@State difficultyIndex: number = 1; // 默认"普通"
@State showAnimation: boolean = false;
@State animationText: string = '';
@State isTimerMode: boolean = false;
@State timeRemaining: number = 60;
@State highScores: number[] = [99, 99, 99]; // 三个难度的最高分
@State lastGuessResult: string = '';
// ===== 动画相关 =====
@State particles: Particle[] = [];
private timerId: number = -1;
// ===== 计算属性 =====
get currentDifficulty(): Difficulty {
return DIFFICULTIES[this.difficultyIndex];
}
get remainingRange(): string {
return `${this.gameData.lowBound} ~ ${this.gameData.highBound}`;
}
get remainingAttempts(): number {
return this.currentDifficulty.maxAttempts - this.gameData.attempts;
}
get progressPercent(): number {
// 根据"已排除范围"计算进度
const totalRange = this.currentDifficulty.maxNumber - 1;
const excluded = this.gameData.highBound - this.gameData.lowBound;
return ((totalRange - excluded) / totalRange) * 100;
}
}
⚠️ 避坑指南: 游戏状态适合用枚举而不是布尔值!
❌isPlaying: boolean+isWon: boolean+isStarted: boolean
✅gameState: GameState单一枚举
多个布尔值组合容易产生"死状态"(比如 isPlaying=true 且 isWon=true 同时成立时矛盾)。
🎲 第三步:核心游戏逻辑
初始化 / 开始游戏
createNewGame(): GameData {
const diff = DIFFICULTIES[this.difficultyIndex];
return {
targetNumber: Math.floor(Math.random() * diff.maxNumber) + 1,
attempts: 0,
lowBound: 1,
highBound: diff.maxNumber,
records: [],
bestScore: this.highScores[this.difficultyIndex],
startTime: Date.now(),
};
}
startGame(): void {
this.gameData = this.createNewGame();
this.gameState = GameState.PLAYING;
this.guessInput = '';
this.lastGuessResult = '';
this.showAnimation = false;
}
Math.random()说明: 生成 [0, 1) 的随机浮点数。Math.floor(Math.random() * 100) + 1生成 1~100 的整数。这是 JS/TS 的标准写法,面试高频!
猜数字逻辑
makeGuess(): void {
if (this.gameState !== GameState.PLAYING) return;
const guess = parseInt(this.guessInput);
if (isNaN(guess)) {
this.lastGuessResult = '⚠️ 请输入有效数字';
return;
}
// 范围校验
if (guess < this.gameData.lowBound || guess > this.gameData.highBound) {
this.lastGuessResult =
`⚠️ 请输入 ${this.gameData.lowBound} ~ ${this.gameData.highBound} 之间的数字`;
return;
}
// 更新游戏数据
const newData = { ...this.gameData };
newData.attempts++;
let result: 'high' | 'low' | 'correct';
if (guess === newData.targetNumber) {
result = 'correct';
this.gameState = GameState.CORRECT;
this.triggerWinAnimation();
this.updateBestScore(newData.attempts);
} else if (guess > newData.targetNumber) {
result = 'high';
newData.highBound = Math.min(newData.highBound, guess - 1);
this.lastGuessResult = '⬇️ 大了!往小了猜';
} else {
result = 'low';
newData.lowBound = Math.max(newData.lowBound, guess + 1);
this.lastGuessResult = '⬆️ 小了!往大了猜';
}
// 记录猜测
newData.records = [
...newData.records,
{ number: guess, result: result, timestamp: Date.now() }
];
// 检查是否用完次数
if (newData.attempts >= this.currentDifficulty.maxAttempts
&& result !== 'correct') {
this.gameState = GameState.GAME_OVER;
this.lastGuessResult =
`😅 次数用完了!答案是 ${newData.targetNumber}`;
}
this.gameData = newData;
this.guessInput = '';
// 设备振动反馈
try {
vibrator.vibrate(50);
} catch (e) {
// 模拟器不支持振动,静默失败
}
}
验证流程图:
用户输入数字
│
▼
┌─ 是有效数字吗? ──否──→ "请输入有效数字"
│ 是
▼
┌─ 在范围内吗? ────否──→ "超出范围"
│ 是
▼
┌─ 等于目标? ────是──→ 胜利!进入 CORRECT 状态
│ 否
▼
┌─ 大于目标? ────是──→ 提示"大了",更新上限
│ 否
▼
提示"小了",更新下限
│
▼
┌─ 次数用完了? ──是──→ 游戏结束 GAME_OVER
│ 否
▼
等待下一次猜测
🎨 第四步:UI 构建
数字展示卡片
@Builder
NumberDisplay() {
Column() {
// 数字展示
Text(this.gameState === GameState.IDLE ? '🎯' :
this.gameState === GameState.CORRECT ? '🎉' :
'? ?')
.fontSize(48)
.animation({ duration: 300, curve: Curve.SpringMotion })
// 范围提示
if (this.gameState === GameState.PLAYING) {
Text(`范围: ${this.remainingRange}`)
.fontSize(14)
.fontColor('#667EEA')
.margin({ top: 8 })
} else if (this.gameState === GameState.IDLE) {
Text('点击下方按钮开始游戏')
.fontSize(14).fontColor('#A0AEC0')
}
}
.width(200)
.height(200)
.backgroundColor('#FFFFFF')
.borderRadius(24)
.shadow({ radius: 16, color: '#20000000', offsetY: 4 })
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
✨ 第五步:胜利动画(纯 ArkUI 实现)
这是猜数字游戏的"杀手级功能"——不用任何第三方库,纯用 ArkUI 组件模拟粒子效果:
@Builder
WinAnimation() {
Column() {
// 主标题
Text('🎉🎉🎉')
.fontSize(60)
.animation({ duration: 500, curve: Curve.SpringMotion })
Text('恭喜猜对了!')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#48BB78')
.margin({ top: 12 })
Text(`用了 ${this.gameData.attempts} 次猜中!`)
.fontSize(16)
.fontColor('#667EEA')
.margin({ top: 4 })
// 粒子:用多个 Text 模拟五彩纸屑
ForEach(this.particles, (p: Particle) => {
Text(p.emoji)
.fontSize(p.size)
.position({ x: p.x, y: p.y })
.rotate({ angle: p.rotation, x: 0, y: 0, z: 1 })
.animation({
duration: 2000,
curve: Curve.FastOutLinearIn,
iterations: -1 // 无限循环
})
})
// 分数展示
Row() {
Text(`🏆 历史最佳: ${this.gameData.bestScore === 99 ? '--' : this.gameData.bestScore} 次`)
.fontSize(14).fontColor('#A0AEC0')
}
.margin({ top: 20 })
}
.width('100%')
.height(300)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
粒子生成逻辑:
// 触发胜利动画时生成粒子
triggerWinAnimation(): void {
this.showAnimation = true;
const emojis = ['🎉', '✨', '🌟', '🎊', '💫', '⭐', '🌈', '🔥'];
const newParticles: Particle[] = [];
for (let i = 0; i < 20; i++) {
newParticles.push({
emoji: emojis[Math.floor(Math.random() * emojis.length)],
x: Math.random() * 300,
y: Math.random() * 300,
size: 16 + Math.random() * 20,
rotation: Math.random() * 360,
});
}
this.particles = newParticles;
// 3秒后自动淡出粒子
setTimeout(() => {
this.particles = [];
}, 3000);
}
💡 设计亮点: 这个"粒子系统"虽然只是 20 个随机位置的 emoji 文字,但配合
.animation()的旋转和弹跳曲线,看起来就像五彩纸屑在飘落。成本低、效果好,是新手实现游戏动画的最佳入门方案。
🎮 第六步:游戏交互组件
输入和按钮区域
@Builder
GameControls() {
Column() {
// 输入框
TextInput({
placeholder: '输入 1~100 之间的数字',
text: this.guessInput
})
.type(InputType.Number)
.maxLength(3)
.enabled(this.gameState === GameState.PLAYING)
.onChange((val: string) => {
this.guessInput = val;
})
.onSubmit(() => {
this.makeGuess(); // 按回车直接提交
})
// 提示文字
Text(this.lastGuessResult)
.fontSize(16)
.fontColor(this.getResultColor())
.margin({ top: 8 })
.animation({ duration: 200 })
// 按钮
Row() {
// 猜一下
Button('🚀 猜一下!')
.width(160)
.backgroundColor('#667EEA')
.enabled(this.gameState === GameState.PLAYING && this.guessInput.length > 0)
.onClick(() => { this.makeGuess(); })
// 重新开始
Button('🔄 重新开始')
.width(140)
.backgroundColor('#EDF2F7')
.fontColor('#4A5568')
.margin({ left: 12 })
.onClick(() => { this.startGame(); })
}
.justifyContent(FlexAlign.Center)
.margin({ top: 16 })
}
.width('100%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.shadow({ radius: 8, color: '#10000000' })
}
猜测历史展示
@Builder
GuessHistory() {
Column() {
Text('📋 猜测记录').fontSize(14).fontWeight(FontWeight.Medium)
// 横向滚动展示近期猜测
Scroll() {
Row({ space: 8 }) {
ForEach(this.gameData.records.slice(-8).reverse(), (r: GuessRecord) => {
Column() {
Text(r.number.toString()).fontSize(18).fontWeight(FontWeight.Bold)
Text(r.result === 'high' ? '⬇️' : r.result === 'low' ? '⬆️' : '✅')
.fontSize(14)
}
.width(48).height(56)
.backgroundColor(
r.result === 'correct' ? '#C6F6D5' :
r.result === 'high' ? '#FED7D7' : '#FEFCBF'
)
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
})
}
.padding(8)
}
.scrollable(ScrollDirection.Horizontal)
.height(80)
}
.width('100%')
.margin({ top: 12 })
}
💡 交互细节: 猜测记录用颜色直观反馈——红色(大了)、黄色(小了)、绿色(正确)。用户一眼就能看到自己猜数的走势。
⏱️ 第七步:计时挑战模式(进阶功能)
startTimerMode(): void {
this.isTimerMode = true;
this.timeRemaining = 60;
this.startGame();
// 启动计时器
this.timerId = setInterval(() => {
this.timeRemaining--;
if (this.timeRemaining <= 0) {
this.gameState = GameState.GAME_OVER;
this.lastGuessResult =
`⏰ 时间到!答案是 ${this.gameData.targetNumber}`;
clearInterval(this.timerId);
}
}, 1000);
}
⚠️ 注意: ArkUI 中
setInterval是全局 API,需要在组件销毁时clearInterval清理。在aboutToDisappear生命周期中处理:aboutToDisappear(): void { if (this.timerId > -1) clearInterval(this.timerId); }
🏆 第八步:最高分持久化
// 更新最高分
updateBestScore(attempts: number): void {
const currentBest = this.highScores[this.difficultyIndex];
if (attempts < currentBest) {
this.highScores[this.difficultyIndex] = attempts;
// 持久化存储
AppStorage.set(
`bestScore_${this.difficultyIndex}`,
attempts
);
}
}
// 加载历史记录
loadHighScores(): void {
for (let i = 0; i < DIFFICULTIES.length; i++) {
const saved = AppStorage.get<number>(`bestScore_${i}`);
if (saved !== undefined && saved < 99) {
this.highScores[i] = saved;
}
}
}
// 在页面初始化时加载
aboutToAppear(): void {
this.loadHighScores();
}
🧠 技术深度总结
你学会了什么?
| 知识点 | 用途 | 游戏开发通用性 |
|---|---|---|
| 状态机(枚举) | 控制游戏流程 | ✅ 所有游戏通用 |
| 随机数生成 | 生成目标数字 | ✅ 抽卡、掉落、地图生成 |
| 显式动画 | 胜利庆祝效果 | ✅ 弹窗、转场、反馈 |
| 粒子效果 | 模拟五彩纸屑 | ✅ 特效、爆炸、魔法 |
| 输入验证 | 防止非法输入 | ✅ 任何用户交互 |
| 计时器 | 限时挑战 | ✅ 竞速、倒计时 |
| 数据持久化 | 保存最高分 | ✅ 存档、设置、进度 |
避坑指南 🚫
| 坑 | 错误写法 | 正确写法 |
|---|---|---|
| 随机数边界 | Math.random() * 100 到不了 100 |
Math.floor(Math.random() * 100) + 1 |
| 状态遗漏 | 赢了还能继续猜 | 用 gameState 锁住输入 |
| 定时器泄漏 | 忘记 clearInterval | aboutToDisappear 清理 |
| 动画叠加 | 多次触发闪退 | 用 showAnimation 标志位控制 |
| 输入框未清空 | 猜完后输入仍然保留 | this.guessInput = '' |
最佳实践 ✅
- 状态机设计模式:游戏开发中多用枚举管理状态,少用布尔值组合
- 输入框禁用:非
PLAYING状态时enabled(false)禁止输入 - 振动反馈:猜错/猜对时添加轻振动,提升手感
- 动画分离:胜利动画用单独
if块渲染,不影响游戏主逻辑 - 难度平衡:简单靠运气,普通靠二分法,困难需要有策略
二分查找法小课堂
猜数字游戏的最优策略是二分查找法:
目标: 1~100 中猜 73
第一次: 猜 50 → 小了 (范围: 51~100)
第二次: 猜 75 → 大了 (范围: 51~74)
第三次: 猜 62 → 小了 (范围: 63~74)
第四次: 猜 68 → 小了 (范围: 69~74)
第五次: 猜 71 → 小了 (范围: 72~74)
第六次: 猜 73 → 正确!🎉
每次猜中间的数,最多 7 次必定猜中。学会了这个,你就是"猜数字之王"!
🔮 扩展练习
- 👥 双人模式:两人轮流猜同一数字,比拼谁先猜中
- 📊 统计面板:显示胜率、平均猜中次数、最快记录
- 🎵 音效系统:猜对/猜错播放不同音效
- 🌐 在线排行榜:接入华为云,全国玩家比拼
- 🧩 变体玩法:猜颜色(RGB 值)、猜单词(字母提示)
📚 参考资料
- 官方文档:HarmonyOS 应用开发文档
- 动画 API:animateTo
- 振动 API:@ohos.vibrator
- 开发者社区:华为开发者论坛
- 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net/
更多推荐



所有评论(0)