# 鸿蒙NEXT ArkTS 猜拳游戏开发实战 —— 从零搭建你的第一个 HarmonyOS 应用




前言
2024 年,华为正式发布了 HarmonyOS NEXT(鸿蒙星河版),这是一个完全剥离 Android 代码、基于 OpenHarmony 深度演进的全场景智能操作系统。它采用全新的鸿蒙内核,不再兼容 Android 应用,标志着中国操作系统进入独立自研的新纪元。作为开发者,掌握 HarmonyOS NEXT 的应用开发技能,正变得前所未有的重要。
本文将带领读者从零开始,使用 HarmonyOS NEXT 6.1.1(API 24)和 ArkTS 语言,完整实现一个经典的"猜拳游戏"(石头剪刀布)应用。文章会涵盖从环境搭建、项目创建、UI 布局、状态管理到业务逻辑实现的全部流程,并附上完整的可运行代码。无论你是刚接触鸿蒙开发的新手,还是想了解 ArkTS 语法的跨平台开发者,本文都能为你提供一个扎实的实战起点。
第一章 认识 HarmonyOS NEXT 与 ArkTS
1.1 HarmonyOS NEXT 的技术定位
HarmonyOS NEXT 是华为面向万物互联时代打造的分布式操作系统。与传统的 Android 或 iOS 不同,它具备以下几个核心特征:
分布式架构:通过分布式软总线,实现多设备之间的硬件能力互助共享。例如,手机上运行的应用可以无缝调用平板的屏幕、手表的传感器或智慧屏的扬声器,开发者无需为每种设备单独适配。
一次开发,多端部署:同一套代码可以运行在手机、平板、手表、车机、智慧屏等多种设备上。ArkTS 提供了自适应布局能力,UI 组件会根据屏幕尺寸自动调整排列方式,真正做到"一套代码,全场景覆盖"。
原生安全:HarmonyOS NEXT 从系统层面对数据访问进行管控。应用必须通过安全访问框架申请权限,系统在授予前会对权限的合理性进行校验。用户的数据默认经过加密存储,应用无法随意读取其他应用的数据。
ArkTS 声明式 UI:ArkTS 借鉴了 SwiftUI 和 Jetpack Compose 的设计理念,使用 TypeScript 语法扩展,提供了声明式、状态驱动的 UI 开发范式。开发者只需描述"UI 应该长什么样",框架自动处理"状态变化时如何更新视图"。
1.2 ArkTS 语言简介
ArkTS 是鸿蒙生态的主力开发语言,它在 TypeScript 的基础上做了针对性的精简和增强:
类型安全:ArkTS 继承 TypeScript 的静态类型检查,所有变量、函数参数和返回值都要求在编译时明确类型,这样可以在编码阶段就发现大量潜在的类型错误,减少运行时崩溃。
装饰器语法:使用 @Component、@State、@Entry、@Builder 等装饰器来定义组件和响应式数据。装饰器是 ArkTS 声明式编程的核心机制,它们标记出哪些数据需要被框架观测、哪些类是 UI 组件。
声明式 UI:通过在 build() 方法中声明界面结构,描述"UI 应该是什么样子",而不是"如何一步步创建 UI"。当状态数据变化时,框架会自动计算出最小的 UI 更新范围并执行重绘,开发者完全不需要手动操作 DOM 或视图节点。
不支持的特性:为了运行性能考虑,ArkTS 不支持以下 TypeScript / JavaScript 特性:
any类型 —— 所有类型必须明确,禁用动态类型eval()动态代码执行- 原型链操作
- 运行时类型修改
- 部分 ES6+ 动态语法
这些限制确保了 ArkTS 代码可以在鸿蒙的方舟编译器(ArkCompiler)下获得最优的编译执行性能。
1.3 开发环境要求
在开始编码之前,请确保你的开发机满足以下条件:
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows 10/11(64位)或 macOS Ventura 及以上 |
| DevEco Studio | 5.0.3 Release 及以上版本 |
| SDK 版本 | HarmonyOS NEXT 6.1.1(API 24) |
| Node.js | 18.x LTS |
| 内存 | 建议 16GB 以上,最低 8GB |
| 磁盘 | 至少 30GB 可用空间 |
| 分辨率 | 建议 1920×1080 及以上 |
第二章 项目创建与目录结构
2.1 创建新项目
打开 DevEco Studio,按以下步骤操作:
- 点击欢迎页的 Create Project(或菜单 File > New > Create Project)。
- 在模板选择页,选择 Empty Ability(空模板,适合从零开始)。
- 填写项目信息:
- Project Name:MyApplication1
- Bundle Name:com.example.myapplication1
- Save Location:选择你希望的磁盘路径
- Compatible SDK:选择 API 24
- Language:ArkTS
- Device:Phone
- 点击 Finish,DevEco Studio 会自动初始化项目并通过 ohpm 下载依赖。
初次创建会耗时 1~3 分钟,期间 IDE 底部状态栏会显示 Gradle 和 ohpm 的同步进度。
2.2 项目目录结构解析
创建完成后,项目的目录结构如下:
MyApplication1/
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用级信息(包名、版本号、图标等)
│ └── resources/
│ └── base/
│ ├── element/ # 颜色、字体等基础资源
│ └── media/ # 图片资源
├── entry/ # entry 类型模块(可独立安装的应用)
│ ├── src/
│ │ └── main/
│ │ ├── ets/ # ArkTS 源码目录
│ │ │ ├── entryability/ # Ability 入口
│ │ │ │ └── EntryAbility.ets
│ │ │ ├── entrybackupability/
│ │ │ │ └── EntryBackupAbility.ets
│ │ │ └── pages/
│ │ │ └── Index.ets # 主页(核心编辑文件)
│ │ ├── module.json5 # 模块配置
│ │ └── resources/ # 模块级资源
│ ├── build-profile.json5 # 模块构建配置
│ └── oh-package.json5 # 模块依赖声明
├── hvigor/ # 构建管线配置
├── build-profile.json5 # 项目级构建配置
├── hvigorfile.ts # 项目级构建脚本
├── oh-package.json5 # 项目级依赖
├── oh-package-lock.json5 # 依赖锁定文件
└── local.properties # 本地 SDK 路径配置(自动生成)
重点文件说明:
entry/src/main/ets/pages/Index.ets—— 这是应用的首页,也是我们本次开发的核心文件。猜拳游戏的全部代码都写在这个文件中。AppScope/app.json5—— 应用级的元信息配置,包括包名、版本号、应用名称等。entry/src/main/module.json5—— 模块级配置,注册 Ability、配置权限、声明设备类型等。
2.3 关键配置文件详解
app.json5 —— 应用级配置
{
"app": {
"bundleName": "com.example.myapplication1",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"buildVersion": "1",
"icon": "$media:layered_image",
"label": "$string:app_name"
}
}
各字段含义:
bundleName:应用包名,格式通常为反向域名,在华为应用市场上唯一标识一个应用。vendor:应用开发商名称。versionCode:版本号(整型),用于市场版本升级判断,新版本必须大于旧版本。versionName:版本显示名称,如 “1.0.0”,供用户查看。icon和label:分别引用资源文件中定义的图标和应用名称。
module.json5 —— 模块配置
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false
}
]
}
}
重点字段:
mainElement:应用启动时默认加载的 Ability 名称。abilities:注册应用中所有的 Ability,每个 Ability 代表一个功能入口。abilities[].skills+actions:声明该 Ability 可以响应桌面启动意图,即用户从桌面点击图标时触发。extensionAbilities:注册 Extension Ability,例如这里的backup类型用于数据备份恢复。
第三章 ArkTS 核心概念速览
在开始写游戏之前,我们需要快速掌握 ArkTS 中最核心的几个概念。这些知识点将在后续的代码实现中反复用到。
3.1 @Component 装饰器
@Component 将一个自定义类标记为 UI 组件。被装饰的类必须实现 build() 方法,返回组件的 UI 树描述。
@Component
struct MyComponent {
build() {
// 在这里声明此组件的 UI 结构
Text('Hello ArkTS')
.fontSize(20);
}
}
注意:ArkTS 中推荐使用 struct(结构体)而非 class 来定义组件。struct 是值类型,分配在栈上,相比 class(引用类型,分配在堆上)具有更好的内存布局和访问性能。在频繁创建销毁 UI 组件的场景下,这种性能优势尤为明显。
3.2 @Entry 装饰器
@Entry 标记一个组件为页面的入口点。它通常与 @Component 配合使用,表示该组件是一个完整的页面,可以被路由导航到。
@Entry
@Component
struct Index {
build() {
// 页面代码
}
}
一个应用中可以有多个 @Entry 组件(多页面应用),每个 @Entry 对应一个可路由的页面。我们猜拳游戏的 Index.ets 就是被 @Entry 修饰的首页。
3.3 @State 装饰器 —— 响应式状态
@State 是 ArkTS 中最常用的状态装饰器。被 @State 修饰的变量发生变化时,框架会自动触发 UI 重新渲染,开发者完全不需要手动编写更新逻辑。
@Component
struct Demo {
@State count: number = 0;
build() {
Column() {
Text(`计数: ${this.count}`)
.fontSize(24);
Button('加一')
.onClick(() => {
this.count++; // 修改状态,UI 自动更新
});
}
}
}
关键特性:
@State变量必须是组件实例的局部变量,不能是静态变量或全局变量。- 支持的类型:
number、string、boolean、enum、Array、Map、Set以及这些类型的联合类型。 - 对于嵌套对象的属性变更,
@State只能观测到第一层赋值(this.obj = newObj),无法观测到深层次属性修改(this.obj.prop = value)。如需观测深层变化,需配合使用@Observed和@ObjectLink装饰器。
3.4 基础 UI 组件
ArkTS 提供了丰富的内置基础组件,我们本次猜拳游戏会用到的有:
| 组件 | 说明 | 常用属性 |
|---|---|---|
Column |
垂直方向 Flex 布局容器 | width, height, padding, backgroundColor, borderRadius, alignItems |
Row |
水平方向 Flex 布局容器 | justifyContent, alignItems, layoutWeight |
Text |
文本显示组件 | fontSize, fontColor, fontWeight, textAlign |
Button |
按钮组件(可定制子组件) | width, height, backgroundColor, borderRadius, onClick |
ForEach |
循环渲染指令 | 基于数据源数组动态生成 N 个子组件 |
3.5 链式属性设置
ArkTS 中,组件的属性设置采用 链式调用(Chained Calls) 风格。每个属性方法都返回组件对象本身,因此可以连续调用,代码非常简洁且可读性强。
Text('Hello')
.fontSize(20)
.fontColor('#ff0000')
.fontWeight(FontWeight.Bold)
.margin({ top: 10, bottom: 5, left: 16, right: 16 })
.padding(8)
.borderRadius(4)
.onClick(() => {
console.log('Hello clicked');
});
这种风格与 SwiftUI 和 Flutter 的组件嵌套写法不同,它更像是"流水线式"的设置过程,每个方法改变组件的某一个属性。开发者可以按任意顺序设置属性,最终效果等价于同时设置所有属性。
3.6 Flex 布局
HarmonyOS NEXT 的 UI 布局系统基于 Flexbox 模型。Column 相当于 flex-direction: column(主轴为垂直方向),Row 相当于 flex-direction: row(主轴为水平方向)。
常用布局属性:
Column() {
// 子组件
}
.alignItems(HorizontalAlign.Center) // 交叉轴(水平)居中
.justifyContent(FlexAlign.SpaceEvenly); // 主轴(垂直)均匀分布
Row() {
// 子组件
}
.alignItems(VerticalAlign.Center) // 交叉轴(垂直)居中
.justifyContent(FlexAlign.Center); // 主轴(水平)居中
layoutWeight(value: number) 是特别有用的属性,它按照权重分配容器内的剩余空间,相当于 CSS Flex 中的 flex: value。所有设置了 layoutWeight 的子组件会根据权重比例瓜分剩余空间。
第四章 猜拳游戏需求分析与设计
4.1 功能需求
我们要实现的猜拳游戏需要满足以下六项功能:
- 三种出拳选项:玩家可以选择石头(Rock)、剪刀(Scissors)或布(Paper)。
- 玩家出拳交互:点击对应按钮后,界面立即反馈玩家的选择。
- 电脑自动出拳:玩家出拳后,电脑程序随机选择一种出拳。
- 即时胜负判定:根据经典猜拳规则判定本局胜负,并显示结果。
- 累计比分记录:持续记录玩家和电脑各自获胜的总局数。
- 一键重置:任何时候点击重置按钮,清空比分和出拳状态,重新开始。
4.2 猜拳规则
猜拳游戏的经典规则非常简单:
- 石头 胜 剪刀
- 剪刀 胜 布
- 布 胜 石头
- 相同出拳则为 平局
我们的代码实现需要将这个规则转化为计算机可以执行的条件判断。虽然可以使用 if-else 枚举所有 3×3 = 9 种组合,但更优雅的方式是利用模运算(Modular Arithmetic)来简化判定。
模运算判定法:
将三种出拳分别映射为数字:
- 石头(Rock)→ 0
- 剪刀(Scissors)→ 1
- 布(Paper)→ 2
这 0→1→2→0 构成一个环。在模 3 的体系中,"差值"决定了胜负方向:
diff = (playerValue - computerValue + 3) % 3
diff = 0 → 平局(相同出拳)
diff = 1 → 电脑胜(玩家在环上"后面"一位)
diff = 2 → 玩家胜(玩家在环上"前面"一位)
验证:
| 玩家 | 电脑 | 玩家值 | 电脑值 | 差值 | 结果 |
|---|---|---|---|---|---|
| 石头 | 剪刀 | 0 | 1 | (0−1+3)%3 = 2 | 玩家胜 ✓ |
| 石头 | 布 | 0 | 2 | (0−2+3)%3 = 1 | 电脑胜 ✓ |
| 剪刀 | 石头 | 1 | 0 | (1−0+3)%3 = 1 | 电脑胜 ✓ |
| 剪刀 | 布 | 1 | 2 | (1−2+3)%3 = 2 | 玩家胜 ✓ |
| 布 | 石头 | 2 | 0 | (2−0+3)%3 = 2 | 玩家胜 ✓ |
| 布 | 剪刀 | 2 | 1 | (2−1+3)%3 = 1 | 电脑胜 ✓ |
这个判定方法只用一行算术表达式就覆盖了全部 9 种情况,代码简洁且性能极好(没有分支预测失败的开销)。
4.3 UI 设计
考虑到猜拳游戏的交互流程简单明确,我们采用 单页垂直布局 的设计方案。整个界面从上到下依次为:
┌─────────────────────────────┐
│ 猜拳游戏 │ ← 标题区
├─────────────────────────────┤
│ 玩家: 3 │ VS │ 电脑: 2 │ ← 比分板
├─────────────────────────────┤
│ 你出了 🪨 │ 🆚 │ 电脑出 ✂️ │ ← 出拳展示区
├─────────────────────────────┤
│ 🎉 你赢了! │ ← 结果提示
├─────────────────────────────┤
│ 请出拳: │
│ [🪨石头] [✂️剪刀] [📄布] │ ← 操作按钮
├─────────────────────────────┤
│ [ 重新开始 ] │ ← 重置按钮
└─────────────────────────────┘
设计思路:
- 采用 Card 式卡片风格,每个区块有浅灰背景和圆角分割,视觉层次清晰。
- 比分板中玩家分数用蓝色、电脑分数用红色,便于快速区分。
- 出拳区使用 Emoji 表达,直观且无需额外图片资源。
- 结果文字颜色随胜负平动态变化(绿色=赢,红色=输,黄色=平)。
- 底部重置按钮独立存在,避免游戏过程中误触。
第五章 完整代码实现
本章是文章的核心部分。我们将从数据模型定义开始,逐步构建猜拳游戏的完整代码。所有代码都写在 entry/src/main/ets/pages/Index.ets 中。
5.1 定义数据模型
首先,我们定义一个 Choice 接口来描述每种出拳的属性和行为:
interface Choice {
name: string; // 中文名称:石头、剪刀、布
emoji: string; // Emoji 图标:🪨 ✂️ 📄
value: number; // 数值编号:0、1、2
}
选择使用 interface(接口)而不是 class(类),是因为 Choice 仅仅是纯粹的数据载体,不需要任何方法。ArkTS 中的 interface 在编译期会被完全擦除,不产生任何运行时内存占用。
5.2 组件状态设计
在 @Component struct Index 内部,我们声明以下状态变量:
@State playerChoice: string = ''; // 玩家出拳的 emoji
@State computerChoice: string = ''; // 电脑出拳的 emoji
@State result: string = '点击按钮开始猜拳'; // 结果文本
@State playerScore: number = 0; // 玩家获胜次数
@State computerScore: number = 0; // 电脑获胜次数
@State round: number = 0; // 总局数计数器
每个变量都被 @State 装饰器修饰,这意味着:
- 它们属于组件实例,不是共享的全局状态。
- 当这些变量的值发生变化时,引用了它们的 UI 部分会自动重绘。
- 例如,
this.playerScore++执行后,比分板上显示玩家分数的Text组件会自动更新。
另外定义一组固定数据(不需要 @State,因为永不变化):
readonly choices: Choice[] = [
{ name: '石头', emoji: '🪨', value: 0 },
{ name: '剪刀', emoji: '✂️', value: 1 },
{ name: '布', emoji: '📄', value: 2 },
];
readonly 关键字确保数组和其元素不会被意外修改。三种出拳的 value 分别是 0、1、2,对应我们在第四章中建立的猜拳判定环。
5.3 build() 方法 —— UI 构建
build() 方法返回组件的界面树。它是组件的"蓝图",ArkTS 框架会根据这个方法生成实际的界面。我们逐层分析每段代码。
5.3.1 根容器
build() {
Column() {
// 所有子组件
}
.width('100%')
.height('100%')
.backgroundColor('#fff');
}
最外层使用 Column 垂直布局容器,宽度和高度都设置为 '100%'(占满整个屏幕),背景色为纯白色。
5.3.2 标题
Text('猜拳游戏')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 10 });
使用 FontWeight.Bold 枚举值设置粗体。ArkTS 提供了完整的字体权重枚举:FontWeight.Lighter、FontWeight.Normal、FontWeight.Medium、FontWeight.Bold、FontWeight.Bolder 等,比直接使用数字更语义化。
5.3.3 比分板
比分板是游戏中最重要的信息展示区,我们采用水平三栏布局:
Row() {
// 玩家分数区域
Column() {
Text('玩家')
.fontSize(14)
.fontColor('#666');
Text(`${this.playerScore}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#1890ff');
}
.layoutWeight(1) // 占据 1 份剩余空间
// VS 分隔
Text('VS')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#999');
// 电脑分数区域
Column() {
Text('电脑')
.fontSize(14)
.fontColor('#666');
Text(`${this.computerScore}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#ff4d4f');
}
.layoutWeight(1) // 占据 1 份剩余空间
}
.margin({ top: 10, bottom: 20 })
.padding(16)
.width('90%')
.backgroundColor('#f5f5f5')
.borderRadius(12);
关键设计:
Row容器内包含三个子元素:左 Column(玩家分)、Text(VS 分隔符)、右 Column(电脑分)。- 左右两个
Column都设置了layoutWeight(1),它们会等分除去 VS 文本宽度后的所有剩余空间。 backgroundColor('#f5f5f5')和borderRadius(12)组合产生浅灰卡片效果。- 分数数字使用大号字体(36)和醒目的颜色(玩家蓝 #1890ff,电脑红 #ff4d4f),让比分一目了然。
5.3.4 出拳展示区
出拳展示区在玩家出拳后动态显示双方的选择:
Row() {
// 玩家出拳
Column() {
Text('你出了')
.fontSize(14)
.fontColor('#666');
Text(this.playerChoice || '❓')
.fontSize(48)
.margin(8);
}
.layoutWeight(1)
// 对战图标
Text('🆚')
.fontSize(28);
// 电脑出拳
Column() {
Text('电脑出了')
.fontSize(14)
.fontColor('#666');
Text(this.computerChoice || '❓')
.fontSize(48)
.margin(8);
}
.layoutWeight(1)
}
.padding(20)
.width('90%')
.backgroundColor('#fafafa')
.borderRadius(12);
这里使用 || 运算符为初始状态提供默认值:当玩家尚未出拳时,this.playerChoice 为空字符串 '',逻辑或运算符会取后面的 '❓' 作为回退值。同样的逻辑也适用于电脑的出拳展示。
5.3.5 结果文本
Text(this.result)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(this.getResultColor())
.margin(16)
.height(50);
结果文本的颜色不固定,而是通过 getResultColor() 方法根据当前结果动态计算:
getResultColor(): ResourceColor {
if (this.result.includes('赢')) {
return '#52c41a'; // 绿色 —— 赢
} else if (this.result.includes('输')) {
return '#ff4d4f'; // 红色 —— 输
} else if (this.result.includes('平')) {
return '#faad14'; // 黄色 —— 平局
}
return '#999'; // 灰色 —— 初始状态
}
这个方法利用字符串包含判断来区分三种结果,返回对应的颜色值。注意返回值类型声明为 ResourceColor,这是 ArkTS 中表示颜色的内置类型,可以直接接受十六进制颜色字符串。
5.3.6 出拳操作按钮
这是玩家与游戏交互的核心区域。三个按钮分别对应石头、剪刀、布:
Text('请出拳:')
.fontSize(16)
.fontColor('#888')
.margin({ bottom: 12 });
Row() {
ForEach(this.choices, (item: Choice, index: number) => {
Button() {
Column() {
Text(item.emoji)
.fontSize(36);
Text(item.name)
.fontSize(14)
.margin({ top: 4 });
}
}
.width(90)
.height(90)
.margin({ left: 8, right: 8 })
.backgroundColor(this.playerChoice === item.name ? '#1890ff' : '#e8e8e8')
.borderRadius(16)
.onClick(() => {
this.playGame(item);
})
}, (item: Choice) => item.name)
}
.width('100%')
.justifyContent(FlexAlign.Center);
ForEach 详解:
ForEach 是 ArkTS 中的循环渲染指令,它的签名如下:
ForEach(
dataSource: any[], // 数据源数组
itemGenerator: (item, index) => void, // 生成子组件的闭包
keyGenerator?: (item) => string // 可选的键值生成器
)
在代码中:
- 数据源是
this.choices(三个 Choice 对象)。 - 闭包接收
item(当前元素)和index(当前索引),返回 Button 组件。 - 键值生成器
(item: Choice) => item.name使用name字段作为每个 Button 的稳定标识,帮助 ArkTS 框架在列表更新时准确识别哪些元素发生了变化,从而最小化 DOM 更新范围。
动态背景色:
this.playerChoice === item.name ? '#1890ff' : '#e8e8e8' —— 每次 UI 渲染时,遍历三个按钮,如果按钮对应的出拳名称与 playerChoice 匹配(即玩家刚刚点击了这个按钮),则该按钮高亮为蓝色;否则显示浅灰色。这种"选中高亮"的交互反馈让玩家确认自己的选择已被记录。
5.3.7 重置按钮
Button('重新开始')
.width('50%')
.margin({ top: 20 })
.backgroundColor('#ff4d4f')
.borderRadius(20)
.onClick(() => {
this.resetGame();
});
红色按钮,宽度为父容器的 50%,点击后调用 resetGame() 方法重置所有游戏状态。
5.4 playGame() —— 核心游戏逻辑
playGame 方法是猜拳游戏的大脑,它整合了电脑随机出拳、结果判定和分数更新。完整的代码如下:
playGame(player: Choice): void {
// 电脑随机出拳 —— 生成 0、1、2 的随机整数
const computerValue = Math.floor(Math.random() * 3);
const computer = this.choices[computerValue];
// 更新出拳展示(触发 UI 自动刷新)
this.playerChoice = player.emoji;
this.computerChoice = computer.emoji;
this.round++;
// 模运算判定胜负
// diff = 0 → 平局 | diff = 1 → 电脑胜 | diff = 2 → 玩家胜
const diff = (player.value - computer.value + 3) % 3;
if (diff === 0) {
this.result = '🤝 平局!';
} else if (diff === 2) {
this.result = '🎉 你赢了!';
this.playerScore++;
} else {
this.result = '😢 你输了!';
this.computerScore++;
}
}
逐行分析:
电脑随机出拳:
const computerValue = Math.floor(Math.random() * 3);
Math.random() 返回 [0, 1) 的浮点数,乘以 3 后范围是 [0, 3),Math.floor 取整得到 0、1、2 三个整数值之一。这是一种高效且均匀分布的随机数生成方式。
UI 状态更新:
this.playerChoice = player.emoji;
this.computerChoice = computer.emoji;
赋值操作的副作用:由于这些变量被 @State 修饰,赋值动作会触发 ArkTS 框架的变更检测,自动找出所有依赖这些变量的 UI 节点并执行最小化重绘。
胜负判定:
const diff = (player.value - computer.value + 3) % 3;
这是判定逻辑的核心。我们在第四章已经详细推导过这个公式。这里只给出结论:
diff = 0→ 平局diff = 1→ 电脑胜diff = 2→ 玩家胜
注意为什么要 + 3:在 JavaScript/ArkTS 中,% 运算符对负数返回负值(例如 -1 % 3 = -1)。+3 能保证结果始终落在 [0, 2] 的正数范围内,使判定逻辑正确。
5.5 resetGame() —— 重置游戏
resetGame(): void {
this.playerChoice = '';
this.computerChoice = '';
this.result = '点击按钮开始猜拳';
this.playerScore = 0;
this.computerScore = 0;
this.round = 0;
}
将所有状态变量恢复为初始值。所有 @State 变量重置后,UI 会自动回到初始状态——出拳区显示两个问号、比分归零、结果文本恢复为引导文字。
5.6 完整代码总览
将以上所有代码片段整合在一起,得到 Index.ets 的完整内容:
@Entry
@Component
struct Index {
@State playerChoice: string = '';
@State computerChoice: string = '';
@State result: string = '点击按钮开始猜拳';
@State playerScore: number = 0;
@State computerScore: number = 0;
@State round: number = 0;
readonly choices: Choice[] = [
{ name: '石头', emoji: '🪨', value: 0 },
{ name: '剪刀', emoji: '✂️', value: 1 },
{ name: '布', emoji: '📄', value: 2 },
];
build() {
Column() {
// --- 标题 ---
Text('猜拳游戏')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 10 });
// --- 比分板 ---
Row() {
Column() {
Text('玩家').fontSize(14).fontColor('#666');
Text(`${this.playerScore}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#1890ff');
}.layoutWeight(1)
Text('VS')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#999');
Column() {
Text('电脑').fontSize(14).fontColor('#666');
Text(`${this.computerScore}`)
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#ff4d4f');
}.layoutWeight(1)
}
.margin({ top: 10, bottom: 20 })
.padding(16)
.width('90%')
.backgroundColor('#f5f5f5')
.borderRadius(12);
// --- 出拳展示区 ---
Row() {
Column() {
Text('你出了').fontSize(14).fontColor('#666');
Text(this.playerChoice || '❓')
.fontSize(48)
.margin(8);
}.layoutWeight(1)
Text('🆚')
.fontSize(28);
Column() {
Text('电脑出了').fontSize(14).fontColor('#666');
Text(this.computerChoice || '❓')
.fontSize(48)
.margin(8);
}.layoutWeight(1)
}
.padding(20)
.width('90%')
.backgroundColor('#fafafa')
.borderRadius(12);
// --- 结果提示 ---
Text(this.result)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(this.getResultColor())
.margin(16)
.height(50);
// --- 出拳操作 ---
Text('请出拳:')
.fontSize(16)
.fontColor('#888')
.margin({ bottom: 12 });
Row() {
ForEach(this.choices, (item: Choice) => {
Button() {
Column() {
Text(item.emoji).fontSize(36);
Text(item.name).fontSize(14).margin({ top: 4 });
}
}
.width(90)
.height(90)
.margin({ left: 8, right: 8 })
.backgroundColor(this.playerChoice === item.name ? '#1890ff' : '#e8e8e8')
.borderRadius(16)
.onClick(() => {
this.playGame(item);
})
}, (item: Choice) => item.name)
}
.width('100%')
.justifyContent(FlexAlign.Center);
// --- 重置按钮 ---
Button('重新开始')
.width('50%')
.margin({ top: 20 })
.backgroundColor('#ff4d4f')
.borderRadius(20)
.onClick(() => {
this.resetGame();
});
}
.width('100%')
.height('100%')
.backgroundColor('#fff');
}
playGame(player: Choice): void {
const computerValue = Math.floor(Math.random() * 3);
const computer = this.choices[computerValue];
this.playerChoice = player.emoji;
this.computerChoice = computer.emoji;
this.round++;
const diff = (player.value - computer.value + 3) % 3;
if (diff === 0) {
this.result = '🤝 平局!';
} else if (diff === 2) {
this.result = '🎉 你赢了!';
this.playerScore++;
} else {
this.result = '😢 你输了!';
this.computerScore++;
}
}
getResultColor(): ResourceColor {
if (this.result.includes('赢')) {
return '#52c41a';
} else if (this.result.includes('输')) {
return '#ff4d4f';
} else if (this.result.includes('平')) {
return '#faad14';
}
return '#999';
}
resetGame(): void {
this.playerChoice = '';
this.computerChoice = '';
this.result = '点击按钮开始猜拳';
this.playerScore = 0;
this.computerScore = 0;
this.round = 0;
}
}
interface Choice {
name: string;
emoji: string;
value: number;
}
第六章 构建与运行
6.1 项目构建
代码编写完成后,需要进行构建以验证语法正确性并生成 HAP 安装包。
在 DevEco Studio 中,可以点击菜单栏 Build > Build HAP(s) 进行构建。如果使用命令行,可以在项目根目录执行:
cd E:\MyApplication1
hvigorw assembleHap --mode module -p module=entry --no-daemon
如果一切正常,终端输出末尾会显示:
> hvigor BUILD SUCCESSFUL in 15 s
构建成功后,HAP 安装包生成在 entry/build/default/outputs/ 目录中。
如果构建报错,常见原因及解决方案:
| 错误类型 | 可能原因 | 解决方案 |
|---|---|---|
编译报错 XXX is not defined |
变量名拼写错误或未声明 | 检查变量名与 @State 声明是否一致 |
| 类型不匹配 | @State 变量被赋了错误类型 |
确保 number 类型变量不赋值字符串 |
Module not found |
文件路径引用错误 | 检查 module.json5 中的 srcEntry 路径 |
| SDK 版本不兼容 | DevEco Studio 或 SDK 版本过低 | 升级到 API 24 对应的 SDK 版本 |
6.2 运行到真机或模拟器
运行到模拟器:
- 在 DevEco Studio 的 Device Manager 中创建一个 Phone 模拟器。
- 确保模拟器已启动并处于运行状态。
- 点击工具栏的 Run 按钮(绿色三角形),选择目标设备为模拟器。
- 等待安装和启动,应用将在模拟器中运行。
运行到真机:
- 在手机上开启 开发者模式(设置 > 关于手机 > 连续点击版本号 7 次)。
- 开启 USB 调试 和 通过 USB 安装应用。
- 使用 USB 数据线连接手机到电脑。
- 在 DevEco Studio 中点击 Run,选择已连接的真机设备。
- 如果是首次在真机运行,需要配置签名证书(DevEco Studio 会自动引导生成调试证书)。
6.3 运行效果验证
应用启动后,你应该能看到:
- 顶部显示"猜拳游戏"标题。
- 比分板显示"玩家 0 | VS | 电脑 0"。
- 出拳展示区显示两个问号 ❓ ❓。
- 中间文字显示"点击按钮开始猜拳"。
- 三个按钮分别显示 🪨石头、✂️剪刀、📄布。
- 底部有红色"重新开始"按钮。
点击任意出拳按钮后:
- 出拳展示区显示玩家的选择和电脑的随机选择(Emoji 形式)。
- 结果文字变为"🎉 你赢了!"、“😢 你输了!” 或 “🤝 平局!”,颜色对应绿、红、黄。
- 比分板的数字相应更新。
第七章 扩展与优化方向
我们的猜拳游戏已经是一个完整可用的应用,但这个项目还有很多可以延伸优化的方向。以下是几个值得尝试的扩展思路:
7.1 添加游戏动画
当前版本中,电脑出拳是瞬间显示的,缺少悬念感。可以添加一个"翻牌"动画:玩家出拳后,电脑先显示一个随机问号旋转动画,0.5 秒后再展示真正的出拳结果。ArkTS 提供了 animateTo 方法和过渡动画 API 来实现这类效果:
animateTo({ duration: 500, curve: Curve.EaseOut }, () => {
// 在闭包中修改状态,框架自动生成动画过渡
this.computerChoice = computer.emoji;
});
7.2 历史对局记录
可以使用 @State 数组记录最近若干局的历史,并在界面底部添加一个展开式列表,展示每局玩家出了什么、电脑出了什么、结果如何。这有助于玩家复盘自己的出拳策略。
7.3 最佳 N 局决胜模式
添加一个设置入口,让玩家选择"三局两胜"或"五局三胜"模式。先达到胜场数的一方获胜,游戏自动弹窗宣布最终胜利者并重置。
7.4 AI 策略优化
当前的电脑出拳是完全随机的(Math.random() 均匀分布)。可以引入简单的策略:记录玩家的出拳历史,统计玩家最常出的拳种,然后电脑选择能克制该拳种的出拳。例如:
// 简易 AI:如果玩家最近最爱出"石头",电脑就多出"布"
function aiChoice(playerHistory: number[]): number {
// 统计玩家历史出拳的分布
const counts = [0, 0, 0];
playerHistory.forEach(v => counts[v]++);
const mostFrequent = counts.indexOf(Math.max(...counts));
// 返回克制玩家最常见出拳的选项
return (mostFrequent + 2) % 3; // 环上后退一步
}
7.5 多页面与路由
现在所有代码都在一个页面中。如果添加设置页面、历史记录页面或关于页面,需要学习 ArkTS 的路由系统(router API)和页面间传参。
第八章 常见问题解答
Q1: DevEco Studio 提示 SDK 未安装怎么办?
打开 DevEco Studio 的 Settings > SDK Manager,检查 API 24 是否已安装。如果未安装,勾选并点击 Apply 下载。下载完成后重启 IDE。
Q2: 构建时报 “ohpm install failed”?
可能是网络问题导致的依赖下载失败。检查网络连接后,在项目根目录手动执行:
ohpm install
如果持续失败,可以配置 ohpm 镜像源(在 ~/.ohpmrc 中写入 registry=https://repo.harmonyos.com/ohpm/)。
Q3: 真机运行提示 “signing certificate error”?
鸿蒙应用在真机运行时需要签名。在 DevEco Studio 中,按照提示进入 Build > Generate Key and CSR 生成调试证书,或在 Project Structure > Project > Signing Configs 中配置自动签名。
Q4: 界面在不同尺寸设备上显示异常怎么办?
使用 ArkTS 的响应式布局能力替代硬编码的 px 值。例如使用 .width('90%') 而不是 .width(360),使用 .layoutWeight(1) 实现自适应比例分配。对于更复杂的场景,可以监听屏幕尺寸变化并调整布局参数。
Q5: @State 装饰的数组修改后 UI 不更新怎么办?
使用 @State 装饰的数组,如果只是通过索引修改元素(如 this.arr[0] = newValue),ArkTS 无法检测到变化。应当使用不可变方式赋值:
// 错误 —— UI 不会更新
this.choices[0] = newItem;
// 正确 —— 创建新数组触发更新
this.choices = [...this.choices.slice(0, 0), newItem, ...this.choices.slice(1)];
结语
本文从 HarmonyOS NEXT 的技术背景出发,详细介绍了 ArkTS 的核心概念,并带领大家一步步实现了一个完整的猜拳游戏应用。在这个过程中,我们覆盖了:
- ArkTS 声明式编程:
@Component、@Entry、@State等装饰器的使用 - UI 构建:
Column、Row、Text、Button、ForEach等组件的组合与布局 - 状态管理:响应式数据驱动 UI 更新的机制
- 业务逻辑:猜拳规则的数学建模与代码实现
- 工程实践:项目构建、真机调试和常见问题排查
猜拳游戏虽然简单,但它涵盖了应用开发中最核心的"UI × 状态 × 逻辑"三角关系。理解了这个基本模式,你就可以将其扩展到更复杂的应用场景中。
HarmonyOS NEXT 作为一个全新的操作系统生态,正在迅速发展。掌握 ArkTS 开发,不仅是掌握一门语言,更是为未来的全场景智能化应用开发做好准备。希望本文能成为你鸿蒙开发旅程中坚实的第一步。
Happy coding!
本文基于 HarmonyOS NEXT 6.1.1(API 24)和 DevEco Studio 5.0.3 编写。源码可在项目目录 entry/src/main/ets/pages/Index.ets 中找到。
更多推荐




所有评论(0)