🎯 新手零基础学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 = ''

最佳实践 ✅

  1. 状态机设计模式:游戏开发中多用枚举管理状态,少用布尔值组合
  2. 输入框禁用:非 PLAYING 状态时 enabled(false) 禁止输入
  3. 振动反馈:猜错/猜对时添加轻振动,提升手感
  4. 动画分离:胜利动画用单独 if 块渲染,不影响游戏主逻辑
  5. 难度平衡:简单靠运气,普通靠二分法,困难需要有策略

二分查找法小课堂

猜数字游戏的最优策略是二分查找法

目标: 1~100 中猜 73

第一次: 猜 50    → 小了 (范围: 51~100)
第二次: 猜 75    → 大了 (范围: 51~74)
第三次: 猜 62    → 小了 (范围: 63~74)
第四次: 猜 68    → 小了 (范围: 69~74)
第五次: 猜 71    → 小了 (范围: 72~74)
第六次: 猜 73    → 正确!🎉

每次猜中间的数,最多 7 次必定猜中。学会了这个,你就是"猜数字之王"!


🔮 扩展练习

  1. 👥 双人模式:两人轮流猜同一数字,比拼谁先猜中
  2. 📊 统计面板:显示胜率、平均猜中次数、最快记录
  3. 🎵 音效系统:猜对/猜错播放不同音效
  4. 🌐 在线排行榜:接入华为云,全国玩家比拼
  5. 🧩 变体玩法:猜颜色(RGB 值)、猜单词(字母提示)

📚 参考资料


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐