鸿蒙游戏开发实战从0到1:手把手带你搭建完整小游戏系统
本文从一个完整的鸿蒙小游戏实战项目出发,详细拆解了从项目结构、数据定义、状态管理、业务逻辑到UI组件和AI加入的全过程。文章不仅保留了原始代码骨架和核心思想,还补充了术语解释、同类架构对比、性能注意事项、专家建议以及未来多设备、AI NPC、社交化等扩展趋势。适合鸿蒙初学者快速掌握“State+UI+Service”的可扩展游戏设计方法。

“能不能从 0 带我做一个完整的鸿蒙小游戏?”
很简单:
一个角色左右移动 点击得分 简单 AI 敌人
同时,我们会遵循 HarmonyOS 推荐的结构:
State + UI + Service +(可选)Agent
扩展:什么是State+UI+Service架构?
这种模式将界面、数据和业务逻辑分离。State(状态)是单一数据源,UI(界面)只做展示和用户交互触发,Service(服务)处理所有游戏规则(移动、碰撞、得分等)。Agent(代理)可选,用于复杂AI或网络请求。对比传统的MVC模式:MVC中Controller容易臃肿,而鸿蒙的State+UI+Service更轻量,且天然适配ArkUI的响应式更新机制。
同类架构对比表
| 架构模式 | 数据流向 | UI更新方式 | 适用场景 | 鸿蒙推荐度 |
|---|---|---|---|---|
| MVC | 双向 | 手动/观察者 | 传统Web/桌面 | ⭐⭐ |
| MVVM | 双向绑定 | 自动 | 复杂表单/CRUD | ⭐⭐⭐⭐ |
| State+UI+Service | 单向 | 状态订阅自动 | 游戏/实时交互 | ⭐⭐⭐⭐⭐ |
专家建议:对于小游戏,优先使用State+UI+Service。如果后续加入网络对战,再考虑引入Agent层处理消息收发。
一、项目结构(先搭骨架)
先不要写代码,先把结构定好:
entry
├─ pages
│ └─ GamePage.ets
│
├─ components
│ └─ Player.ets
│
├─ services
│ └─ GameService.ets
│
├─ models
│ └─ GameModel.ets
│
└─ store
└─ GameStore.ets
核心思想:
UI / 逻辑 / 状态 分离
扩展:目录结构详解
- entry/src/main/ets/ 是鸿蒙应用的主代码目录。
- pages/ 存放页面级组件,例如游戏主界面、排行榜页面。
- components/ 存放可复用的UI组件,比如角色头像、血条、按钮等。
- game/ 是游戏核心逻辑包:state/ 定义所有状态(玩家位置、分数、敌人数组),service/ 实现移动、碰撞检测、得分计算,ai/ 存放敌人AI行为树或简单追逐算法。
- utils/ 放工具函数,比如随机数生成、坐标转换。
注意事项:不要在pages里写游戏逻辑,否则后续增加关卡或多人模式时,代码会变得难以维护。保持pages只做“组装”工作。
二、定义游戏数据
// models/GameModel.ets
export interface Player {
x: number
y: number
}
export interface Enemy {
x: number
y: number
}
export interface GameState {
player: Player
enemy: Enemy
score: number
}
先把“世界”定义出来。
扩展:数据结构设计要点
在鸿蒙中,游戏数据通常定义为一个@Observed或@State装饰的类。例如玩家数据应包含坐标(x, y)、生命值、得分、携带道具等。敌人数据至少包含坐标、类型、移动速度。
术语解释:
- @Observed:让类的属性变化可被观察到。
- @State:组件内部可变状态,变化会触发UI刷新。
- @ObjectLink:用于引用被@Observed装饰的类实例,避免深拷贝开销。
专家建议:将游戏世界数据设计为不可变快照(Immutable Snapshot)模式?对鸿蒙小游戏来说不必过度设计。但建议区分“全局只读配置”(如重力参数、地图大小)和“运行时可变状态”(玩家位置、敌人列表),可变状态放在一个GameState类中统一管理。
三、状态管理
// store/GameStore.ets
import { GameState } from '../models/GameModel'
export class GameStore {
state: GameState = {
player: { x: 100, y: 300 },
enemy: { x: 200, y: 100 },
score: 0
}
update(partial: Partial<GameState>) {
this.state = { ...this.state, ...partial }
}
}
export const gameStore = new GameStore()
所有数据,都走这里。
扩展:状态管理的最佳实践
在鸿蒙ArkUI中,状态管理常用装饰器有@State、@Prop、@Link、@Provide/@Consume。对于小游戏:
- 游戏主页面使用@State gameState: GameState持有全局状态。
- 子组件(如玩家角色、敌人列表)使用@ObjectLink接收部分状态,避免全量传递。
- 如果需要跨页面共享得分或关卡,可使用@Provide/@Consume或全局单例Store。
注意事项:频繁修改@State变量可能导致UI过度重绘。例如在游戏循环中每帧更新100个敌人的坐标,会触发100次UI刷新。解决方案:使用@Watch监听关键变化(如玩家生命值),而对非UI渲染的数据(如路径计算中间值)不要放在@State中。或者采用“手动帧同步”:在requestAnimationFrame回调中批量更新状态后一次性刷新。
未来趋势:鸿蒙Next版本将推出更高效的“原子化状态”和“差分同步”,类似React的useTransition,开发者可关注。
四、业务逻辑
// services/GameService.ets
import { gameStore } from '../store/GameStore'
export class GameService {
moveLeft() {
gameStore.update({
player: {
...gameStore.state.player,
x: gameStore.state.player.x - 10
}
})
}
moveRight() {
gameStore.update({
player: {
...gameStore.state.player,
x: gameStore.state.player.x + 10
}
})
}
addScore() {
gameStore.update({
score: gameStore.state.score + 1
})
}
}
export const gameService = new GameService()
页面不直接改数据,只调用 Service。
扩展:Service层设计模式
Service应该是纯逻辑层,不持有UI引用。典型方法:
- movePlayer(direction):修改玩家坐标,边界检测,触发碰撞检测。
- updateEnemies():遍历敌人,调用AI决策,更新坐标。
- checkCollisions():检测玩家与敌人、物品的碰撞,返回事件列表。
专家建议:Service内部可以进一步拆分为GamePhysicsService(物理移动、碰撞)、ScoreService(计分、连击)、AudioService(音效触发)。这样单元测试更容易。另外,Service方法返回值建议只返回“发生了什么事件”(如碰撞类型、新分数),而不是直接修改UI。UI通过订阅状态变化自动响应。
同类对比:
- 传统Unity开发:MonoBehavior脚本直接操作Transform,逻辑与组件绑定。
- 鸿蒙Service:纯TypeScript类,不依赖UI框架,可移植到Node.js做服务端验证。
五、UI 组件
// components/Player.ets
@Component
export struct Player {
@Prop x: number
@Prop y: number
build() {
Image("player.png")
.width(50)
.height(50)
.position({
x: this.x,
y: this.y
})
}
}
UI 只负责展示。
扩展:UI组件设计原则
每个组件应接收必要的数据(通过@Prop或@ObjectLink)和回调函数(通过@BuilderParam或箭头函数)。例如:
- PlayerComponent:接收玩家坐标和血量,点击时调用onTap回调。
- EnemyComponent:接收敌人类型和位置,动画状态由敌人状态(移动/攻击)决定。
注意事项:避免在UI组件中做复杂计算或网络请求。例如不要在每个渲染帧中遍历所有敌人计算可见性,应该在Service中预先计算好“可见敌人列表”再传递给UI。
优化技巧:使用@Builder装饰器提取可复用布局片段。对于频繁刷新的游戏画布,也可以考虑使用Canvas组件直接绘制,绕过ArkUI的组件树重绘开销。Canvas更适合做实时动作游戏。
六、主页面
// pages/GamePage.ets
import { gameStore } from '../store/GameStore'
import { gameService } from '../services/GameService'
import { Player } from '../components/Player'
@Entry
@Component
struct GamePage {
@State state = gameStore.state
aboutToAppear() {
this.loop()
}
loop() {
setInterval(() => {
this.enemyMove()
}, 500)
}
enemyMove() {
const newX = this.state.enemy.x + (Math.random() * 20 - 10)
gameStore.update({
enemy: {
...this.state.enemy,
x: newX
}
})
this.state = gameStore.state
}
build() {
Column() {
// 玩家
Player({
x: this.state.player.x,
y: this.state.player.y
})
// 敌人
Image("enemy.png")
.width(50)
.height(50)
.position({
x: this.state.enemy.x,
y: this.state.enemy.y
})
// 分数
Text(`Score: ${this.state.score}`)
.fontSize(20)
// 操作按钮
Row() {
Button("←").onClick(() => gameService.moveLeft())
Button("→").onClick(() => gameService.moveRight())
Button("+1").onClick(() => gameService.addScore())
}
}
.width('100%')
.height('100%')
}
}
到这里,一个完整小游戏已经跑起来了。
扩展:主页面生命周期
在aboutToAppear()中初始化游戏Service,注册键盘或触摸事件。在aboutToDisappear()中清理定时器(如游戏循环的requestAnimationFrame ID)。
注意事项:如果游戏需要后台暂停,监听onPageHide和onPageShow。鸿蒙系统会冻结不可见页面的定时器,但最好主动暂停游戏循环以节省资源。
示例代码片段(保留占位符,实际不可运行,仅示意):
aboutToAppear() {
this.gameLoopId = setInterval(() => {
this.gameService.update(); // 更新逻辑
this.gameState = this.gameService.getState(); // 触发UI更新
}, 16); // 约60fps
}七、加入简单 AI
我们让敌人“聪明一点”。
// services/EnemyAI.ets
import { gameStore } from '../store/GameStore'
export class EnemyAI {
decide() {
const { player, enemy } = gameStore.state
if (player.x > enemy.x) {
return 'right'
} else {
return 'left'
}
}
act() {
const action = this.decide()
const delta = action === 'right' ? 5 : -5
gameStore.update({
enemy: {
...gameStore.state.enemy,
x: gameStore.state.enemy.x + delta
}
})
}
}页面中替换:
enemyAI.act()
现在敌人会“追你”。
扩展:AI行为详解 上面实现的追逐AI本质是:每一帧计算敌人到玩家的向量,归一化后乘以速度。这种AI非常简单,但可能导致所有敌人重叠追玩家。
改进方向: - 加入随机偏移,避免路径重叠。
实现“群组AI”:敌人之间保持最小距离(类似群聚算法Boids)。
状态机AI:敌人有“巡逻”、“追击”、“攻击”、“逃跑”状态,根据玩家距离切换。
术语解释:
向量归一化:将方向向量长度变为1,只保留方向。
欧几里得距离:sqrt(dx²+dy²),判断是否进入攻击范围。
专家建议:对于鸿蒙小游戏,AI逻辑放在Service中,但复杂的AI计算可能造成卡顿。可以使用Worker线程单独计算AI决策,再回传主线程更新坐标。
八、优化结构
当前问题
this.state = gameStore.state
手动同步。
优化方向
- 使用状态订阅
- 自动更新 UI
扩展:状态订阅的实现方式 在鸿蒙中,可以利用@Observed和@ObjectLink实现自动订阅。另一种方式是使用AppStorage或LocalStorage进行跨组件同步。对于游戏,推荐将GameState类标记为@Observed,然后在页面中用@State包装,任何Service修改GameState的属性都会自动触发UI刷新,无需手动调用setState。
注意事项:
如果GameState嵌套层次很深(如player.inventory.items[0].count),修改深层属性可能不会被观测到。
解决:使用@Track装饰器或重新赋值整个对象(不推荐)。
更好的做法:保持状态扁平化,或使用不可变数据模式(每次修改返回新对象)。
性能对比表
| 更新方式 | 代码复杂度 | UI响应延迟 | 适用游戏类型 |
|---|---|---|---|
| 手动setState | 高,易遗漏 | 低(可控) | 小型休闲游戏 |
| 状态订阅自动 | 低 | 中(框架批处理) | 大部分棋牌、回合制 |
| 手动Canvas绘制 | 中 | 最低 | 动作、射击游戏 |
九、扩展方向
1、加入碰撞检测
if (distance(player, enemy) < 50) {
gameOver()
}扩展:
碰撞检测算法 对于矩形碰撞,使用AABB(Axis-Aligned Bounding Box)
检测:if (rect1.x < rect2.x+rect2.w && ...)。对于圆形,检测圆心距离。
注意事项:每帧对所有敌人-玩家进行O(N)检测,敌人数量过多(>100)时可能掉帧。可使用空间划分(网格或四叉树)优化。
2、加入音效
audio.play("hit.mp3")扩展:音效管理最佳实践
使用@ohos.multimedia.audio或@system.audio。建议预加载所有音效资源(如new Audio('sound.mp3')),避免游戏过程中IO延迟。对于背景音乐,使用长循环播放;对于短促音效(跳跃、碰撞),使用轻量级播放器。注意设置音量类别(媒体/通知),避免与系统音冲突。
3、加入排行榜
leaderboard.submit(score)
扩展:排行榜的云存储方案
鸿蒙提供了@ohos.data.cloudData或集成华为云服务。如果仅本地存储,使用Preferences或分布式数据库。分布式数据库可以在多个鸿蒙设备间同步排行榜,例如手机和平板查看同一份数据。
专家建议:排行榜设计时考虑防作弊:对分数进行加密签名,或者采用服务端验证。小游戏可先实现简单的“历史最高分”本地存储。
4、加入 AI NPC
npcAgent.decide(state)
扩展:NPC行为树
简单的追逐AI是行为树的一个叶子节点。完整的AI NPC可以具备“对话”、“交易”、“发布任务”等。行为树由Selector、Sequence、Condition等节点组成。鸿蒙中可以使用第三方库btjs或自己实现轻量级状态机。
5、多设备扩展
手机控制 TV 显示
扩展:多设备协同实战
鸿蒙的分布式能力允许手机作为手柄,电视显示游戏画面。实现思路:
- 在手机端编写一个“控制器”Ability,通过DistributedDataKit同步玩家输入。
- 电视端接收输入并更新游戏状态。
注意事项:需要处理设备间的网络延迟,一般使用预测回滚(Client-side prediction)技术。
未来趋势:鸿蒙Next将推出“一次开发,多端自适应”的声明式布局,游戏UI可自动适配手机、折叠屏、平板的不同宽高比。
十、你刚刚做了什么?
很多人做完 Demo 会觉得:
“好像挺简单?”
但其实你已经完成了一件很重要的事情:
从:
写 UI
到:
设计系统: State Service Component AI
这就是鸿蒙开发的核心。
扩展:思维转变的意义
从“写一次性代码”到“搭建可扩展系统”,意味着你的项目可以:
- 轻松加入新功能而不破坏原有逻辑(例如增加新敌人类型,只需扩展Service和State)。
- 多人协作时,UI工程师和逻辑工程师可并行工作。
- 单元测试可以独立测试Service,无需启动UI。
专家建议:定期重构状态和Service的接口,保持单向数据流。避免在Service中持有UI相关的Context或Resource对象。
总结
我们从 0 做了一个完整鸿蒙小游戏,核心结构是:
Model(数据) Store(状态) Service(逻辑) Component(UI)
并且扩展了:
AI(Enemy)
如果用一句话总结这次实战:
你不是在“写一个小游戏”,而是在“搭一个可扩展的游戏系统”。
扩展:最终检查清单
- [ ] 状态是否集中在GameState中,没有散落在各组件?
- [ ] Service方法是否纯逻辑,不调用UI组件?
- [ ] 碰撞检测、音效、排行榜等扩展是否按需加入?
- [ ] 游戏循环是否在页面销毁时正确清理?
- [ ] 是否测试过在折叠屏或不同分辨率下的显示?
最后给你一个建议:不要停在这个 Demo。
你可以继续把它升级为:
- 多端游戏
- AI 游戏
- 社交游戏
在 HarmonyOS 上,这样的项目,才刚刚开始有意思。
未来展望:
随着鸿蒙生态的发展,AI能力(如Maxtrix API)、分布式计算、元服务卡片等新技术将深度整合进游戏开发。例如,利用卡片在桌面显示游戏状态,无需打开游戏即可查看资源产出;利用AI推理优化NPC行为,使敌人更“聪明”。建议开发者关注鸿蒙官方每季度的技术沙龙和开发者文档更新。
到此这篇关于鸿蒙游戏开发从0到1:手把手带你搭建完整小游戏系统的文章就介绍到这了,更多相关鸿蒙游戏开发从0到1内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
- 鸿蒙HarmonyOS开发:Navigation路由导航功能和实践
- 鸿蒙(HarmonyOS)实现隐私政策弹窗效果
- DevEco Studio 2.0开发鸿蒙HarmonyOS应用初体验全面测评(推荐)
- 鸿蒙HarmonyOS中的ArkUI组件库特性与常用组件实例演示
- 鸿蒙中@State的原理使用详解(HarmonyOS 5)
- 鸿蒙HarmonyOS App开发造轮子之自定义圆形图片组件的实例代码
- 浅析鸿蒙基础之Permanent 持久性内存对象(HarmonyOS鸿蒙开发基础知识)
- 鸿蒙开发之处理图片位图操作的方法详解(HarmonyOS鸿蒙开发基础知识)
- HarmonyOS开发基础知识之Component和ComponentContainer区别(鸿蒙教程)
- 鸿蒙开发之Button按钮类型及如何通过代码设置(HarmonyOS鸿蒙开发基础知识)
- 鸿蒙HarmonyOS 分布式任务调度的实现
- 鸿蒙HarmonyOS剪切板的实现
- 鸿蒙HarmonyOS视频播放的实现
- HarmonyOS鸿蒙基本控件的实现
- HarmonyOS鸿蒙实现HelloWorld应用开发E2E体验
相关文章
Android报错Didn‘t find class “android.view.x“问题解决原理剖析
这篇文章主要为大家介绍了Android报错Didn‘t find class “android.view.x“问题解决及原理剖析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2023-03-03
Android手把手教大家制作APP首页(下拉刷新、自动加载)
这篇文章主要为大家详细介绍了Android手把手教大家制作APP首页,实现下拉刷新、自动加载功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2017-01-01
Android学习笔记--使用剪切板在Activity中传值示例代码
相对于getText和setText而言,利用ClipData对象来传递数据,更符合面向对象的思想,而且所能传递的数据类型也多样化了2013-06-06


最新评论