本文基于当下主流的 HarmonyOS NEXT 系统 + API 12,采用 ArkTS 语言与 ArkUI 声明式开发范式,手把手带你从零开发经典人机对战小游戏 —— 石头剪刀布。

项目覆盖鸿蒙应用开发核心基础知识点@State 响应式状态管理原理、随机数算法、业务逻辑分层、ArkUI 主流布局组件、组件动画、点击事件交互等。全文代码注释详尽、架构规范,不仅适合零基础鸿蒙开发者入门练手,也可作为高校实训、课程设计、技术面试的参考案例。文末附带问题排查、功能进阶拓展、代码优化思路,由浅入深帮你吃透基础语法与开发思想。

关键字:HarmonyOS NEXT、ArkTS、鸿蒙开发、ArkUI、石头剪刀布、移动应用、前端开发、声明式 UI


目录

一、项目整体概述 二、开发环境配置要求 三、整体设计思路与架构分层 四、完整可运行源码 五、核心代码逐段深度解析 六、项目运行实操演示 七、代码优化与功能进阶拓展 八、知识点总结与学习延伸 九、开发常见问题 & 完整解决方案 十、写在最后


一、项目整体概述

1.1 项目背景与定位

石头剪刀布是移动端经典入门小游戏,逻辑简单、交互直观,非常适合用来学习鸿蒙声明式 UI 开发。本项目不追求复杂功能,重点聚焦语法规范、编码习惯、状态管理思想,帮大家建立鸿蒙开发的基础认知。

1.2 核心功能清单

  1. 玩家点击按钮选择石头、剪刀、布三种手势;
  2. 程序通过随机算法模拟电脑出拳,完成人机对战;
  3. 内置标准对战规则,自动判定胜利、失败、平局三种结果;
  4. 实时统计全局对局数据:胜利、失败、平局次数;
  5. 支持一键重置游戏状态,清空所有统计数据;
  6. 为手势组件添加过渡动画,提升 APP 交互质感;
  7. 全局采用弹性布局,自动适配鸿蒙手机不同屏幕尺寸。

1.3 本次学习技术栈 & 知识点

通过本项目,你将系统掌握以下内容:

  • ArkUI 基础布局组件:Column 纵向布局、Row 横向布局、Text 文本、Button 按钮
  • ArkTS 核心装饰器:@State 响应式状态(数据驱动视图核心)
  • 组件事件:按钮 onClick 点击事件绑定与回调处理
  • 基础语法:常量定义、数组使用、分支条件判断
  • 工具方法:Math.random() 随机数生成规则与实践
  • 动效开发:ArkUI 内置 animation 动画属性配置
  • 编码规范:逻辑与 UI 分离、方法封装、代码注释规范

二、开发环境配置要求

在开始编码前,请严格匹配以下环境,从根源避免编译、运行报错。

表格

工具 / 软件 推荐版本 用途说明
DevEco Studio 4.0 及以上 鸿蒙官方集成开发 IDE,代码编写、编译、运行一体化工具
HarmonyOS SDK API 12 适配 HarmonyOS NEXT 最新系统,兼容 ArkTS 新语法
运行设备 模拟器 / 鸿蒙真机 优先使用手机模拟器调试,真机可做最终效果验证

环境检查实操步骤:

  1. 打开 DevEco Studio,点击顶部菜单栏 File -> Project Structure
  2. Modules 选项卡中确认 Compile SDK 为 API 12
  3. 新建项目时,模板选择 Empty Ability,语言选择 ArkTS

三、整体设计思路与架构分层

遵循高内聚、低耦合的开发思想,将项目整体划分为四大模块,结构清晰、便于后期维护和拓展:

  1. UI 界面层 采用 Column + Row 组合布局,页面划分为:标题区、对战展示区、结果提示区、数据统计区、功能按钮区,模块化拆分页面结构。

  2. 状态管理层 使用 @State 管理所有动态数据:玩家手势、电脑手势、对局结果、统计数据。利用鸿蒙数据驱动视图特性,数据变更自动刷新页面,无需手动操作 DOM。

  3. 业务逻辑层 单独封装对局方法、胜负判定规则、游戏重置方法,将业务逻辑与 UI 代码解耦,代码可读性更强。

  4. 交互动效层 为手势组件添加过渡动画,优化点击反馈,符合鸿蒙系统流畅交互的设计理念。


四、完整可运行源码

文件路径:entry/src/main/ets/pages/Index.ets 使用方式:新建空白 ArkTS 项目后,直接替换该文件全部代码,即可编译运行。

typescript

运行

@Entry
@Component
struct RockPaperScissors {
  // 手势常量定义:使用常量替代硬编码,提升代码可维护性
  private readonly ROCK: number = 0;
  private readonly SCISSORS: number = 1;
  private readonly PAPER: number = 2;

  // 手势表情与文字映射数组,统一管理UI展示内容
  private readonly handEmojis: string[] = ['✊', '✌️', '🖐️'];
  private readonly handNames: string[] = ['石头', '剪刀', '布'];

  // ========== 全局响应式状态变量 ==========
  // 玩家选择手势,初始值-1代表未选择
  @State userChoice: number = -1;
  // 电脑随机手势,初始值-1代表未生成
  @State computerChoice: number = -1;
  // 对局结果提示文本
  @State resultText: string = '请选择你的手势开始游戏!';
  // 对局统计数据
  @State winCount: number = 0;
  @State loseCount: number = 0;
  @State drawCount: number = 0;

  build() {
    // 页面根布局:纵向全局布局
    Column() {
      // 页面标题模块
      Column() {
        Text('✂️ 石头剪刀布 人机对战')
          .fontSize(32)
          .fontWeight(FontWeight.Bold)
          .fontColor('#000000')
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)
      .margin({ top: 30, bottom: 40 })

      // 对战核心区域:玩家 VS 电脑
      Row() {
        // 玩家展示区域
        Column() {
          Text('玩家')
            .fontSize(20)
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 15 })
          Text(this.userChoice === -1 ? '❓' : this.handEmojis[this.userChoice])
            .fontSize(100)
            // 手势切换过渡动画
            .animation({ duration: 300, curve: Curve.EaseOut })
        }
        .alignItems(HorizontalAlign.Center)
        .flexGrow(1)

        // 对战标识
        Text('VS')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ff3b30')
          .margin({ left: 20, right: 20 })

        // 电脑展示区域
        Column() {
          Text('电脑')
            .fontSize(20)
            .fontWeight(FontWeight.Medium)
            .margin({ bottom: 15 })
          Text(this.computerChoice === -1 ? '❓' : this.handEmojis[this.computerChoice])
            .fontSize(100)
            .animation({ duration: 300, curve: Curve.EaseOut })
        }
        .alignItems(HorizontalAlign.Center)
        .flexGrow(1)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceAround)
      .margin({ bottom: 40 })

      // 对局结果提示文本
      Text(this.resultText)
        .fontSize(22)
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 30 })

      // 数据统计区域:胜/负/平展示
      Row() {
        Text(`胜利:${this.winCount}`)
          .fontSize(18)
          .fontColor('#34c759')
          .margin({ right: 25 })
        Text(`失败:${this.loseCount}`)
          .fontSize(18)
          .fontColor('#ff3b30')
          .margin({ right: 25 })
        Text(`平局:${this.drawCount}`)
          .fontSize(18)
          .fontColor('#666666')
      }
      .margin({ bottom: 50 })

      // 手势选择按钮组
      Row() {
        Button(`${this.handEmojis[this.ROCK]} 石头`)
          .width(120)
          .height(50)
          .fontSize(16)
          .backgroundColor('#007aff')
          .onClick(() => {
            this.play(this.ROCK);
          })

        Button(`${this.handEmojis[this.SCISSORS]} 剪刀`)
          .width(120)
          .height(50)
          .fontSize(16)
          .backgroundColor('#007aff')
          .margin({ left: 15, right: 15 })
          .onClick(() => {
            this.play(this.SCISSORS);
          })

        Button(`${this.handEmojis[this.PAPER]} 布`)
          .width(120)
          .height(50)
          .fontSize(16)
          .backgroundColor('#007aff')
          .onClick(() => {
            this.play(this.PAPER);
          })
      }
      .margin({ bottom: 30 })

      // 游戏重置按钮
      Button('🔄 重新开始游戏')
        .width(250)
        .height(50)
        .fontSize(16)
        .backgroundColor('#8e8e93')
        .onClick(() => {
          this.resetGame();
        })

    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .padding({ left: 20, right: 20, top: 20 })
    .backgroundColor('#ffffff')
  }

  /**
   * 游戏核心对局方法
   * @param userChoice 玩家选中的手势下标
   */
  play(userChoice: number): void {
    // 赋值玩家选择的手势
    this.userChoice = userChoice;
    // 电脑随机生成 0 / 1 / 2 对应三种手势
    this.computerChoice = Math.floor(Math.random() * 3);

    // 胜负逻辑判断
    if (userChoice === this.computerChoice) {
      // 双方手势一致:平局
      this.resultText = '🤝 本局平局,再来一局吧!';
      this.drawCount++;
    } else if (
      (userChoice === this.ROCK && this.computerChoice === this.SCISSORS) ||
      (userChoice === this.SCISSORS && this.computerChoice === this.PAPER) ||
      (userChoice === this.PAPER && this.computerChoice === this.ROCK)
    ) {
      // 玩家满足获胜条件
      this.resultText = '🎉 恭喜你,本局获胜!';
      this.winCount++;
    } else {
      // 其余情况判定为玩家失败
      this.resultText = '😥 本局失利,再接再厉!';
      this.loseCount++;
    }
  }

  /**
   * 重置游戏方法:恢复所有状态至初始值
   */
  resetGame(): void {
    this.userChoice = -1;
    this.computerChoice = -1;
    this.resultText = '请选择你的手势开始游戏!';
    this.winCount = 0;
    this.loseCount = 0;
    this.drawCount = 0;
  }
}

五、核心代码逐段深度解析

5.1 常量与数组定义(编码规范讲解)

typescript

运行

private readonly ROCK: number = 0;
private readonly SCISSORS: number = 1;
private readonly PAPER: number = 2;
private readonly handEmojis: string[] = ['✊', '✌️', '🖐️'];
  • 使用 readonly 定义只读常量,运行期间不可修改,规避数据误改问题;
  • 用常量代替数字硬编码,语义清晰,后期修改、拓展无需全局查找替换;
  • 数组统一管理展示文本与表情,实现数据与视图分离

5.2 @State 响应式状态(鸿蒙核心原理)

typescript

运行

@State userChoice: number = -1;
@State computerChoice: number = -1;
@State resultText: string = '请选择你的手势开始游戏!';

@State 是 ArkUI 实现数据驱动视图的核心装饰器:

  1. @State 修饰的变量为响应式变量
  2. 当变量值发生变更时,框架会自动检测并刷新页面中依赖该变量的组件;
  3. 区别于传统安卓 /iOS 命令式开发,无需手动调用刷新视图方法,开发效率更高。

5.3 布局组件详解

  • Column:纵向弹性布局,页面整体、独立模块均使用该组件,是鸿蒙最常用布局;
  • Row:横向弹性布局,用于实现并排展示的元素(对战区、按钮组、统计行);
  • flexGrow:弹性占比属性,实现左右区域等分屏幕宽度,适配不同机型;
  • margin / padding:控制内边距、外边距,规范页面留白,提升视觉体验。

5.4 组件动画配置

typescript

运行

.animation({ duration: 300, curve: Curve.EaseOut })
  • duration:动画时长,单位毫秒,本案例设置 300ms,符合移动端通用动效标准;
  • curve:动画运动曲线,Curve.EaseOut 表示先快后慢,过渡效果更自然;
  • 动画跟随状态变化自动触发,无需额外手动调用。

5.5 随机数算法解析

typescript

运行

this.computerChoice = Math.floor(Math.random() * 3);
  • Math.random():生成范围在 [0, 1) 的随机小数;
  • 乘以 3 后范围变为 [0, 3)
  • Math.floor() 向下取整,最终得到 0、1、2 三个整数,完美对应三种手势。

5.6 胜负判定逻辑

遵循经典游戏规则:

  1. 石头(0)击败 剪刀(1)
  2. 剪刀(1)击败 布(2)
  3. 布(2)击败 石头(0)
  4. 下标相等直接判定为平局 逻辑采用多分支判断,写法直观,新手极易理解。

5.7 方法封装思想

将对局、重置功能单独封装为独立函数:

  1. 代码模块化,逻辑集中管理;
  2. 复用性强,一处定义多处调用;
  3. 降低主体 build 函数复杂度,代码层级分明。

六、项目运行实操演示

6.1 完整运行步骤

  1. 打开 DevEco Studio,新建项目,模板选择 Empty Ability,语言选择 ArkTS,SDK 选择 API 12;
  2. 进入路径 entry/src/main/ets/pages/,打开 Index.ets,清空原有代码,粘贴上方完整源码;
  3. 点击工具栏 Run 按钮,选择模拟器或连接好的鸿蒙真机;
  4. 等待编译完成,自动启动应用即可体验。

6.2 功能效果说明

  1. 初始状态:玩家与电脑区域均显示问号,页面提示选择手势;
  2. 对局过程:点击任意手势按钮,页面触发动画,电脑随机出拳并展示结果;
  3. 数据统计:每完成一局,胜利 / 失败 / 平局对应数据自动累加;
  4. 重置功能:点击「重新开始游戏」,所有状态与统计数据恢复为初始状态。

建议:发布文章时在此处插入 4 张运行截图(初始页、对局胜利、对局失败、重置后页面),图文结合大幅提升阅读评分。


七、代码优化与功能进阶拓展

7.1 现有代码优化点(进阶编码习惯)

  1. 防重复点击 对局过程中禁用按钮,避免短时间连续点击造成逻辑错乱,核心优化代码:

    typescript

    运行

    @State isPlaying: boolean = false;
    
    // 按钮增加禁用状态
    Button('石头')
      .enabled(!this.isPlaying)
      .onClick(()=>{
         this.isPlaying = true;
         this.play(this.ROCK);
      })
    
    // play 方法末尾恢复状态
    this.isPlaying = false;
    
  2. 抽离提示文案 将所有提示文字统一抽离为常量,便于后期多语言适配。

7.2 进阶功能拓展(课后练手方向)

  1. 音频拓展:调用鸿蒙音频 API,为出拳、胜利、失败添加音效;
  2. 图片资源替换:使用本地图片替代 Emoji 手势,自定义游戏皮肤;
  3. 连胜统计:新增变量记录连续胜利次数,增加游戏趣味性;
  4. 历史对局记录:使用数组存储每一局结果,实现对战日志功能;
  5. 难度模式:区分简单 / 困难模式,调整电脑随机出拳概率。

八、知识点总结与学习延伸

8.1 本项目核心总结

  1. 鸿蒙 ArkUI 采用声明式 UI + 数据驱动视图,是区别于传统开发的核心思想;
  2. @State 状态管理是所有鸿蒙应用的基础,后续组件通信、全局状态均基于此延伸;
  3. Column + Row 组合布局可以实现移动端绝大多数页面结构,务必熟练掌握;
  4. 方法封装、常量定义、代码注释是专业开发的必备编码规范。

8.2 后续学习路线

掌握本案例后,可以依次练习:计数器、简易计算器、待办清单、图片轮播等入门项目,循序渐进学习 @Link、@Prop、@Provide 等高级状态装饰器、自定义组件、页面路由等知识点。


九、开发常见问题 & 完整解决方案

整理开发过程中高频报错,附带原因 + 解决方案,一站式排错:

  1. 问题:编译报错,提示 API 版本过低 原因:项目 SDK 未切换为 API 12 解决:进入 Project Structure,将 Compile SDK 修改为 API 12,重新编译。

  2. 问题:点击按钮无任何响应 原因:onClick 事件未绑定、方法名书写错误、语法括号缺失 解决:逐行检查回调函数与方法名称,核对大小写与符号。

  3. 问题:页面布局错乱、元素超出屏幕 原因:固定宽高、布局属性搭配不当 解决:移动端优先使用百分比宽高,配合 flexGrow 实现弹性适配。

  4. 问题:模拟器黑屏、应用无法启动 原因:模拟器进程卡死、代码存在语法死循环 解决:关闭并重启模拟器,检查代码语法,重新运行项目。

  5. 问题:动画效果不生效 原因:动画属性未绑定响应式变量 解决:确保动画依赖的状态变量被 @State 修饰

 

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐