news 2026/7/25 7:15:39

【OpenHarmony/HarmonyOS】从开始到结算:ArkUI 游戏页面的暂停、重开与状态机治理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【OpenHarmony/HarmonyOS】从开始到结算:ArkUI 游戏页面的暂停、重开与状态机治理

【OpenHarmony/HarmonyOS】从开始到结算:ArkUI 游戏页面的暂停、重开与状态机治理

声明式 UI 很擅长“状态决定界面”,但当页面同时拥有currentPageisGameRunningisPausedisGameOverisLoadingisGameInitialized等多个变量时,状态组合会快速膨胀。某个按钮少改一个字段,就可能出现游戏循环在跑但暂停层仍显示、结算层和操作摇杆同时存在、重开后难度丢失等问题。本篇结合 ArkUI 游戏主页,分析现有布尔状态协作方式,并给出渐进演进为显式状态机的方案。🎮

一、页面同时管理了哪些状态

Index.ets不只是游戏画布,还承载主页、难度页、战斗、资料、HUD、暂停和结算。与游戏流程直接相关的字段包括:

@StatecurrentPage:'home'|'game'|'difficulty'='home';@StateisGameRunning: boolean = false;@StateisPaused: boolean = false;@StateisGameOver: boolean = false;@StateisLoading: boolean = false;@StateisGameInitialized: boolean = false;@StategameResult:'win'|'lose'='lose';@StategameMode: GameModeType ='pve';@StategameDifficulty:'easy'|'normal'|'nightmare'='normal';
字段表达的问题
currentPage当前显示主页、难度选择还是游戏
isGameRunning页面是否认为游戏会话正在进行
isPaused是否显示暂停层并停止循环
isGameOver是否显示结算层
isLoading是否显示进入游戏前的加载反馈
isGameInitializedCanvas 尺寸是否已经交给引擎初始化

每个字段单看都合理,但 5 个布尔值理论上有 32 种组合,其中大多数没有业务意义。例如currentPage='home' && isPaused=trueisPaused=true && isGameOver=true都属于应该避免的组合。

二、当前页面其实已经有“隐式状态机”

虽然代码没有定义enum GamePhase,但方法之间已经形成状态迁移:

stateDiagram-v2 [*] --> Home Home-->Difficulty: 选择 PvE Home-->Loading: 选择其他模式 Difficulty-->Loading: 选择难度并支付 Loading-->Playing: Canvas 尺寸就绪并初始化 Playing-->Paused: 点击菜单 Paused-->Playing: 继续 Playing-->Settling: 引擎触发 onGameEnd Settling-->GameOver: 同步统计并显示弹窗 Paused-->Home: 退出 GameOver-->Loading: 再来一局 GameOver-->Home: 返回主页

问题不在于“没有状态机就一定错”,而在于状态被分散写入多个方法和异步回调,迁移规则只能靠阅读者脑中拼接。

三、startGame:一次迁移包含同步和异步两部分

进入游戏时先打开 loading,再用 50ms 延迟让 ArkUI 有机会渲染,然后批量重置会话字段:

startGame( mode: GameModeType, difficulty:'easy'|'normal'|'nightmare'='normal', multiplayerConfig: string ='{}'): void {this.isLoading =true; setTimeout(() => {this.currentPage ='game';this.isGameRunning =true;this.isPaused =false;this.isGameOver =false;this.gameMode = mode;this.gameDifficulty = difficulty;this.pendingMultiplayerConfig = multiplayerConfig;this.isGameInitialized =false;this.currentWave =1;this.survivalTime =0;this.sessionCoins =0;// 根据 Canvas 尺寸决定立即初始化或等待 onAreaChange},50); }

这段逻辑解决了真实的声明式 UI 时序问题:如果在主页树还没切换到 Canvas 时就执行重初始化,宽高可能为 0。先改变页面,再等待onAreaChange,能用真实布局尺寸初始化引擎。

但“固定等待 50ms”并不是布局完成的可靠契约。设备负载、动画和系统调度都可能变化。当前代码已经有Canvas.onReady()onAreaChange(),更稳健的设计应以事件是否到达作为条件,而不是把毫秒数当成状态。

四、Canvas 初始化是另一个子状态

Canvas 就绪只设置上下文,不立即初始化:

Canvas(this.context) .onReady(() => {this.gameEngine?.setContext(this.context); }) .onAreaChange((_oldArea, newArea) => {constwidth = newArea.widthasnumber;constheight = newArea.heightasnumber;if(width <=0|| height <=0)return;this.screenWidth = width;this.screenHeight = height;if(this.isGameRunning &&this.gameEngine) {if(this.isGameInitialized) {this.gameEngine.updateScreenSize(width, height); }else{this.gameEngine.initGame( width, height,this.gameMode,this.gameDifficulty,this.pendingMultiplayerConfig );this.isGameInitialized =true; } } })

isGameInitialized防止折叠屏变化或普通尺寸回调重复创建整局游戏。第一次走initGame(),后续只更新屏幕尺寸。

这个布尔值表达的其实是waiting_canvasplaying两个状态。如果初始化抛异常,当前代码仍可能在调用后把它设为true,页面认为成功但引擎未必可用。更好的接口是await engine.initGame()或返回显式结果,再进入 Playing。

五、暂停:UI 状态和引擎命令必须成对

暂停函数同时修改 ArkUI 状态和游戏循环:

togglePause(): void {this.isPaused = !this.isPaused;if(this.isPaused) {this.gameEngine?.stopGameLoop(); }else{this.gameEngine?.startGameLoop(); } }

页面通过if (this.isPaused)显示遮罩,通过if (!this.isPaused && !this.isGameOver)隐藏摇杆与开火按钮。也就是说,一个状态同时控制视觉、输入和引擎时间,闭环较完整。

风险在于toggle依赖当前值。异步回调、快速连点或其他方法提前修改isPaused后,再调用 toggle 可能走向相反状态。命令式接口通常更安全:

pauseGame(): void {if(this.phase !=='playing')return;this.phase ='paused';this.gameEngine?.stopGameLoop(); } resumeGame(): void {if(this.phase !=='paused')return;this.phase ='playing';this.gameEngine?.startGameLoop(); }

这是演进方案。明确的 pause/resume 具有幂等性,多次调用也不会反复翻转。

六、暂停菜单的重开时序值得警惕 ⚠️

暂停菜单当前重开动作是:

{ text:$r('app.string.btn_restart'), action:():void=>{ this.restartGame(); this.togglePause(); } }

restartGame()同步执行:

this.gameEngine?.stopGameLoop();this.isGameOver =false;this.isPaused =false;this.isGameInitialized =false; setTimeout(() => {this.startGame(this.gameMode); },10);

restartGame()已经把isPaused设为 false,随后togglePause()又会把它改成 true 并执行 stop。10ms 后startGame()最终又会重设 false。多数情况下最后状态正确,但中间存在无意义翻转,逻辑依赖多个定时器先后到达。

结算弹窗的重开只调用restartGame(),暂停菜单却多调用一次 toggle,两个入口不一致。更清晰的做法是让restartGame()独自完成全部迁移,调用者不再补状态。

七、重开会丢失哪些会话参数

restartGame()最终调用:

this.startGame(this.gameMode);

只传了模式,难度回退为默认normal,多人配置回退为'{}'。所以从 Nightmare 重开可能变成 Normal,从自定义多人局重开也可能丢失槽位配置。

页面明明已经保存gameDifficultypendingMultiplayerConfig,应该完整传递:

this.startGame(this.gameMode,this.gameDifficulty,this.pendingMultiplayerConfig );

这段是直接可行的演进思路,但本文不修改业务源码。它说明状态机不只管理“处于哪一阶段”,还要携带该阶段的上下文。

八、结束回调:从引擎状态进入 UI 结算

GameEngine 通过onGameEnd将结果上抛,页面同步统计并切换 UI:

this.gameEngine.onGameEnd = (result) => {this.gameResult = result;this.gameStats = new GameStats();this.gameStats.targetsDestroyed =this.gameEngine!.gameStats.targetsDestroyed;this.gameStats.survivalTime =this.gameEngine!.gameStats.survivalTime;this.gameStats.score =this.gameEngine!.gameStats.score;this.isGameOver =true;this.isGameRunning =false;// 保存排行榜与用户统计,刷新资产和资料};

isGameOver=true显示结算弹窗,isGameRunning=false停止页面的 HUD 轮询分支。引擎内部在调用前把gameState设为game_over,因此世界更新也会停止主要逻辑。

但页面没有在回调中显式stopGameLoop()。引擎循环可能仍执行帧回调,只是在 update 中提前返回,并继续 render。这样能保留最终画面,却会持续占用一定资源。产品可以选择“冻结并继续渲染”或“停止循环保留 Canvas 像素”,但应明确,而不是依赖早退的副作用。

九、结算层和暂停层为什么目前不会同时出现

绘制条件分别是:

if(this.isPaused) {this.OverlayMenu(...); }if(this.isGameOver) { GameOverDialog(...); }

它们没有else互斥。如果状态同时为 true,两个全屏遮罩会叠加。当前正常流程中,结算发生前一般没有暂停;暂停后循环停止,也不会触发新结算,因此实际很少重叠。

但这只是时序上的“通常不会”,不是模型层保证。比如网络回调可能在暂停时通知比赛结束。显式状态枚举天然互斥,可以从根上避免两个主阶段同时成立。

十、退出游戏:页面、经济与循环一起收尾

stopGame()承担三类工作:保存中途退出晶石、恢复主页状态、停止游戏循环。

stopGame(): void {if(this.gameEngine && !this.isGameOver &&this.gameEngine.gameStats.coinsCollected >0) {// 根据模式结算中途收益}this.isGameRunning =false;this.isPaused =false;this.isGameOver =false;this.currentPage ='home';this.refreshCoins();this.gameEngine?.stopGameLoop(); }

这个方法本质上是Playing/Paused/GameOver -> Home的统一出口。问题是结算副作用和 UI 迁移耦合:如果资产保存失败,页面仍立即回主页。长期来看可以先由 SessionController 完成endSession(reason),返回结算结果,再让页面导航。

十一、把状态收敛成“阶段 + 上下文”

无需一次性重构整个页面,可以先定义一个主阶段:

type AppPhase = 'home'|'difficulty'|'loading_game'|'playing'|'paused'|'settling'|'game_over'; interface GameSessionContext { mode: GameModeType; difficulty: 'easy'| 'normal' | 'nightmare';multiplayerConfig: string; initialized: boolean; }

UI 条件变成:

if(this.phase ==='paused') {this.PauseOverlay(); }if(this.phase ==='game_over') { GameOverDialog({ result:this.gameResult, stats:this.gameStats }); }

isGameRunning可以由phase推导,不再单独写入;isPausedisGameOver同理。initialized仍可放在会话上下文,因为它是 Playing 内部与 Canvas 生命周期相关的子状态。

十二、迁移函数必须验证合法来源

状态枚举只是第一步,真正价值来自集中迁移:

privatetransitionTo(next: AppPhase):boolean{constallowed: Record<AppPhase, AppPhase[]> = { home: ['difficulty','loading_game'], difficulty: ['home','loading_game'], loading_game: ['playing','home'], playing: ['paused','settling','home'], paused: ['playing','loading_game','home','settling'], settling: ['game_over','home'], game_over: ['loading_game','home'] };if(!allowed[this.phase].includes(next))returnfalse;this.phase =next;returntrue; }

这是示例方案。生产代码还要在迁移钩子中启动/停止循环、清理输入、取消定时器。关键是非法迁移能够被日志捕获,而不是悄悄形成矛盾布尔组合。

十三、异步操作需要“会话代号”

页面使用多个setTimeout:加载延迟、重开延迟、波次 Banner、引擎结算延迟。用户快速返回主页再开始新局时,旧回调可能晚到并修改新局状态。

可以为每次开局生成递增sessionId

privatesessionVersion:number=0;startGame(...):void{constversion = ++this.sessionVersion;setTimeout(() =>{if(version !==this.sessionVersion)return;// 只允许当前会话继续初始化},50); }stopGame():void{this.sessionVersion++;// 旧异步回调随后会失效}

这种“版本令牌”不能替代清除定时器,但能作为第二层防线,尤其适合无法取消的 Promise 回调。

十四、状态机测试矩阵 🧪

初始阶段事件期望阶段额外断言
Home选普通模式LoadingCanvas 未就绪前不初始化
Loading尺寸有效Playing引擎只初始化一次
Playing点击暂停Paused循环停止、输入隐藏
Paused点击继续Playing循环只启动一次
Paused重开Loading保留原难度与多人配置
PlayingonGameEndGameOver结算只执行一次
GameOver再来一局Loading旧弹窗先移除
任意游戏阶段退出Home循环停止、旧回调失效
Paused网络比赛结束GameOver不出现双遮罩

还应做不变量测试:phase !== 'playing'时不接受开火;只有 Playing/Paused/Settling/GameOver 持有有效会话;Home 不允许游戏循环运行。

十五、总结 ✨

当前页面已经通过startGame()togglePause()restartGame()stopGame()onGameEnd建立了可运行的隐式状态机,也正确考虑了 Canvas 尺寸就绪、声明式 UI 先渲染和循环暂停等实际问题。

它的主要风险来自状态分散:多个布尔值可以组成非法状态,暂停重开存在多余翻转,重开没有完整传递难度与多人配置,固定延时替代了事件契约,旧回调还可能越过会话边界。渐进治理的核心不是立刻重写 1900 行页面,而是先引入唯一主阶段、保留会话上下文、让迁移显式且幂等,再用会话版本和测试保护异步边界。这样 ArkUI 的“状态驱动界面”才能真正与游戏引擎的状态保持一致。🚀


推荐标签:OpenHarmonyHarmonyOSArkTSArkUI状态机Canvas游戏开发生命周期

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/25 7:09:36

WordPress插件选择与代码规范:从性能优化到工程化实践

你是不是也遇到过这种情况:刚建好的 WordPress 站点运行流畅,但随着插件越装越多,页面加载越来越慢,甚至服务器资源时不时被占满?后台操作卡顿,前端访问延迟,查日志发现一堆 W3C 校验错误,但就是不知道从哪里下手优化。 很多人把 WordPress 插件当成“万能药”,看到一…

作者头像 李华
网站建设 2026/7/25 7:07:54

注意力机制演进与优化:从MHA到GQA的实践指南

1. 注意力机制演进全景图在自然语言处理领域&#xff0c;注意力机制的发展就像显微镜的迭代升级——从最初的单镜头观察&#xff08;基础注意力&#xff09;到多镜片复合成像&#xff08;多头注意力&#xff09;&#xff0c;再到可调节焦距的智能显微镜&#xff08;现代变体&am…

作者头像 李华
网站建设 2026/7/25 7:04:27

AI辅助游戏开发:工程化实践与毕业设计高效路径

1. 项目概述&#xff1a;当游戏毕设遇上AI工程化又到了一年一度的毕业季&#xff0c;对于计算机、软件工程甚至数字媒体技术专业的同学来说&#xff0c;毕业设计中的游戏开发项目&#xff0c;往往是一场甜蜜的负担。一方面&#xff0c;它充满创意和乐趣&#xff0c;是展示四年所…

作者头像 李华
网站建设 2026/7/25 7:03:23

强化学习中的安全约束与高效探索算法解析

1. 项目背景与核心挑战这篇论文标题直指强化学习领域一个关键痛点——如何在保证每轮训练安全性的前提下&#xff0c;实现高效学习。拆解来看&#xff0c;"Provably Efficient RL"表明研究目标是可证明的高效强化学习算法&#xff0c;"Episode-Wise Safety"…

作者头像 李华
网站建设 2026/7/25 7:03:12

无需梯子构建AI工作流:用DeepSeek与智能体实现本地化自动化

你有没有过这样的经历&#xff1a;想用某个国外的 AI 工具&#xff0c;比如 Codex&#xff0c;来优化你的工作流&#xff0c;但第一步就被“网络环境”卡住了&#xff1f;不是不想用&#xff0c;而是折腾起来太麻烦&#xff0c;或者干脆就用不了。这感觉就像你面前有一台功能强…

作者头像 李华
网站建设 2026/7/25 7:00:33

Claude Code与MiniMax Hub集成:构建AI创作流提升开发效率

最近在尝试将 AI 编程助手深度集成到我的日常开发工作流中时&#xff0c;发现了一个非常有意思的组合&#xff1a;Claude Code与MiniMax Hub。这个组合不仅仅是简单的工具叠加&#xff0c;而是通过 MiniMax Hub 的“技能库”功能&#xff0c;将 Claude Code 从一个代码助手&…

作者头像 李华