HarmonyOS NEXT 开发手记:用 ArkTS 实现一个童年经典游戏

还记得小时候和小伙伴玩石头剪刀布的快乐时光吗?今天我们就用 HarmonyOS NEXT 和 ArkTS 来实现这个经典游戏,体验一下声明式 UI 开发的魅力!

一、前言

石头剪刀布,这个简单却充满策略的小游戏,承载了我们太多童年回忆。选择它作为鸿蒙开发的学习项目,再合适不过了:

  • 规则简单易懂,不需要复杂的业务逻辑
  • 涉及用户交互、状态管理、动画等核心知识点
  • 界面直观,便于理解布局系统
  • 容易扩展,可以添加各种花里胡哨的功能

话不多说,咱们开始吧!

二、准备工作

开发环境一览

项目 版本/说明
操作系统 Windows 10/11
IDE DevEco Studio 5.0+
HarmonyOS SDK API 23 (6.1.0) 最低版本
目标 SDK API 24 (6.1.1)
开发语言 ArkTS (TypeScript 超集)

创建项目

打开 DevEco Studio,选择 File → New → Create Project,选择 Empty Ability 模板,填写项目信息即可。项目创建后,IDE 会自动生成标准的项目结构。

三、项目结构一览

先来看看项目长什么样:

MyApplication/
├── AppScope/                 # 全局资源
│   └── resources/base/
│       └── element/string.json
├── entry/                    # 主模块
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets  # 入口
│   │   │   └── pages/
│   │   │       └── Index.ets         # 主页面
│   │   └── resources/
│   │       └── base/
│   │           ├── element/          # 字符串、颜色等
│   │           ├── media/            # 图片资源
│   │           └── profile/
│   │               └── main_pages.json  # 页面路由
│   └── build-profile.json5
└── build-profile.json5

关键文件就两个:

  • EntryAbility.ets:应用入口,相当于 Android 的 Application
  • Index.ets:主页面,所有 UI 和逻辑都在这里

四、动手实现

4.1 定义数据结构

首先定义一个接口来描述"出拳选项":

interface Choice {
  emoji: string;   // 显示的表情:✊✋✌️
  label: string;   // 文字标签:石头/布/剪刀
  value: string;   // 值:rock/paper/scissors
}

这样设计的好处是数据和显示分离,后续想改个图标什么的,只改一处就行。

4.2 组件状态定义

ArkTS 用 @State 来声明响应式状态,当状态变化时 UI 会自动更新:

@Entry
@Component
struct Index {
  // 显示相关
  @State playerChoice: string = '❔';
  @State computerChoice: string = '❔';
  @State result: string = '选择你的出拳';
  @State resultColor: string = '#8E8E93';
  
  // 分数统计
  @State playerScore: number = 0;
  @State computerScore: number = 0;
  @State draws: number = 0;
  @State roundCount: number = 0;
  
  // 选项数据(常量)
  readonly choices: Choice[] = [
    { emoji: '✊', label: '石头', value: 'rock' },
    { emoji: '✋', label: '布', value: 'paper' },
    { emoji: '✌️', label: '剪刀', value: 'scissors' }
  ];
}

这里有个坑@State 变量一定要显式声明类型!写成 @State playerScore = 0 可能会有类型推断问题。

4.3 游戏逻辑实现

电脑随机出拳
getComputerChoice(): string {
  const randomIndex = Math.floor(Math.random() * 3);
  return this.choices[randomIndex].value;
}

简单粗暴,随机从三个选项里选一个。

判定胜负

这个游戏的精髓就在这里:

play(playerValue: string): void {
  const computerValue: string = this.getComputerChoice();
  
  // 更新显示
  this.playerChoice = this.choices.find(c => c.value === playerValue)?.emoji ?? '❔';
  this.computerChoice = this.choices.find(c => c.value === computerValue)?.emoji ?? '❔';
  this.roundCount++;
  
  // 判定逻辑
  if (playerValue === computerValue) {
    // 平局
    this.result = '🤝 平局!';
    this.resultColor = '#8E8E93';
    this.draws++;
  } else if (
    (playerValue === 'rock' && computerValue === 'scissors') ||
    (playerValue === 'scissors' && computerValue === 'paper') ||
    (playerValue === 'paper' && computerValue === 'rock')
  ) {
    // 玩家获胜
    this.result = '🎉 你赢了!';
    this.resultColor = '#34C759';
    this.playerScore++;
  } else {
    // 电脑获胜
    this.result = '😅 电脑赢了!';
    this.resultColor = '#FF3B30';
    this.computerScore++;
  }
}

胜负规则大家都懂:

  • 石头克剪刀
  • 剪刀克布
  • 布克石头

4.4 界面布局

ArkTS 的布局很像 Flutter,用 ColumnRow 来组织界面。

整体结构
build() {
  Column() {
    // 标题
    // 对战区域
    // 结果
    // 出拳按钮
    // 统计
    // 重置按钮
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#FFFFFF')
  .padding(20)
}
标题区
Text('✂️ 石头剪刀布')
  .fontSize(26)
  .fontWeight(FontWeight.Bold)
  .fontColor('#1C1C1E')
  .margin({ top: 30, bottom: 8 })

Text('和电脑对战!')
  .fontSize(14)
  .fontColor('#8E8E93')
  .margin({ bottom: 24 })
对战区域(核心布局)

这部分用 Row 水平排列三个区域:

Row() {
  // 玩家
  Column() {
    Text('🧑 你').fontSize(14).fontColor('#8E8E93')
    Text(this.playerChoice).fontSize(64).animation({ duration: 300 })
    Text(`${this.playerScore}`).fontSize(16).fontColor('#34C759')
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
  
  // VS
  Column() {
    Text('VS').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF3B30')
  }
  .layoutWeight(0.5)
  .alignItems(HorizontalAlign.Center)
  
  // 电脑
  Column() {
    Text('🤖 电脑').fontSize(14).fontColor('#8E8E93')
    Text(this.computerChoice).fontSize(64).animation({ duration: 300 })
    Text(`${this.computerScore}`).fontSize(16).fontColor('#FF3B30')
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(20)
.backgroundColor('#F2F2F7')
.borderRadius(16)

注意 layoutWeight 的用法

  • 玩家和电脑各占 1 份
  • VS 占 0.5 份
  • 总共 2.5 份,按比例分配宽度
出拳按钮

ForEach 循环渲染三个按钮,避免写重复代码:

Row() {
  ForEach(this.choices, (choice: Choice) => {
    Column() {
      Text(choice.emoji).fontSize(44)
      Text(choice.label).fontSize(14).fontColor('#1C1C1E')
    }
    .width(96)
    .height(110)
    .backgroundColor('#F2F2F7')
    .borderRadius(16)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.play(choice.value);  // 注意用箭头函数!
    })
  })
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)

又一个坑.onClick(this.play(choice.value)) 这样写会直接执行函数,而不是绑定事件!正确写法是用箭头函数包装。

统计信息和重置按钮
// 统计(条件渲染)
if (this.roundCount > 0) {
  Row() {
    Text(`${this.roundCount}`).fontSize(14).fontColor('#8E8E93')
    Blank()
    Text(`平局 ${this.draws}`).fontSize(14).fontColor('#8E8E93')
  }
  .width('100%')
  .padding({ left: 20, right: 20 })
}

// 重置按钮
Button('🔄 重新开始')
  .type(ButtonType.Capsule)
  .width(180)
  .height(44)
  .backgroundColor('#8E8E93')
  .onClick(() => {
    this.resetGame();
  })

条件渲染的坑:ArkTS 不能像 React 那样用 condition && element,必须用原生 if 语句!

五、配置文件解读

main_pages.json

页面路由配置,所有页面都要在这里注册:

{
  "src": [
    "pages/Index"
  ]
}

module.json5

模块配置,相当于 Android 的 AndroidManifest.xml:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": ["phone"],
    "abilities": [{
      "name": "EntryAbility",
      "label": "$string:EntryAbility_label",
      "icon": "$media:layered_image"
    }]
  }
}

资源引用格式$type:name,比如 $string:module_desc

六、运行效果

在这里插入图片描述

游戏流程演示

  1. 初始界面:显示默认图标,提示选择出拳

  2. 点击出拳:玩家和电脑同时显示选择,结果即时更新

  3. 多局对战:分数持续累计,统计信息实时显示

  4. 重新开始:一键清空所有数据

七、开发过程中遇到的坑

坑 1:类型推断不可靠

// ❌ 可能有问题
@State playerScore = 0;

// ✅ 显式声明类型
@State playerScore: number = 0;

坑 2:事件绑定写错

// ❌ 直接执行了函数
.onClick(this.play(choice.value))

// ✅ 用箭头函数包装
.onClick(() => { this.play(choice.value); })

坑 3:条件渲染语法

// ❌ ArkTS 不支持这种写法
this.roundCount > 0 && Text('显示')

// ✅ 必须用 if 语句
if (this.roundCount > 0) {
  Text('显示')
}

坑 4:layoutWeight 理解偏差

layoutWeight 不是百分比,而是按比例分配剩余空间:

Row() {
  Column().layoutWeight(1)    // 占 1 份
  Column().layoutWeight(0.5)  // 占 0.5 份
  Column().layoutWeight(1)    // 占 1 份
}
// 总共 2.5 份

坑 5:资源引用格式

// ❌ 错误格式(有空格)
"description": "$string : module_desc"

// ✅ 正确格式
"description": "$string:module_desc"

八、总结与展望

这个项目学到了什么?

知识点 说明
@State 响应式状态管理
Column/Row 线性布局
layoutWeight 权重分配
ForEach 循环渲染
条件渲染 if 语句
事件绑定 箭头函数

可以怎么扩展?

  1. 保存历史记录:用 Preferences 存储最高分和战绩
  2. 添加动画:出拳时的翻转效果、胜负时的庆祝动画
  3. 加音效:点击音效、胜负音效
  4. 多页面:添加设置页、历史记录页
  5. 多人模式:支持双人本地对战或在线对战

感想

ArkTS 的声明式 UI 真的很舒服,写起来比传统 Android 简洁多了。状态驱动 UI 的理念,让代码逻辑更清晰。DevEco Studio 的实时预览功能也大大提升了开发效率。

总之,HarmonyOS NEXT 的开发体验还是很不错的,值得深入学习和实践!


项目信息:

  • SDK 版本:API 23 (最低) / API 24 (目标)
  • 开发工具:DevEco Studio 5.0+

Logo

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

更多推荐