前言游戏和静态 UI 的本质区别是——UI 是事件驱动的用户点击才更新游戏是时间驱动的每帧更新物理无论用户是否交互。ArkUI 没有内置「游戏循环」需要开发者用setInterval自己搭主循环——每隔固定周期触发一次物理更新。本篇以「猫猫大作战」100ms 主循环为锚点把setInterval 定时器、100ms 物理周期、主循环与 ArkUI 帧的协同、暂停时短路返回四大要点讲透。提示本系列不讲 ArkTS 础语法与环境搭建假设你已跟完第 1–32 篇。本篇是阶段二第三篇。一、场景拆解游戏主循环回顾「猫猫大作战」主循环第 31 篇// 来源entry/src/main/ets/pages/Index.ets startGame() this.gameLoopTimer setInterval(() { if (this.gameState ! GameState.PLAYING) return; // ← 暂停时短路返回 this.cats this.gameEngine.updateCats(); // 物理更新 this.score this.gameEngine.getScore(); // 同步得分 this.combo this.gameEngine.getCombo(); // 同步连击 if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100); // ← 100ms 周期核心问题为什么用setInterval而不是 ArkUI 的requestAnimationFrame为什么是 100ms改成 16ms60 FPS会怎样暂停时定时器还在跑怎么处理二、setInterval 定时器2.1 基本用法const timerId: number setInterval(() { console.info(每 100ms 触发一次); }, 100); // 停止 clearInterval(timerId);API 说明API作用setInterval(callback, delay)周期性触发回调返回 timer idclearInterval(timerId)停止定时器2.2 setInterval vs setTimeoutAPI触发适合setInterval(cb, 100)每 100ms 一次周期游戏主循环setTimeout(cb, 100)100ms 后一次单次倒计时、延迟动画实战经验主循环用 setInterval一次性延迟用 setTimeout。本系列第 35 篇会讲 spawnTimer也用 setInterval因为要周期生成猫咪。2.3 setInterval 的误差setInterval(callback, 100)的100ms是「最短间隔」不是「精确间隔」——如果回调执行耗 20ms下一帧间隔就是 120ms 而非 100ms。回调耗时实际周期误差1ms101ms1%20ms120ms20%100ms200ms100%关键经验回调内别做重计算——耗时超过周期会拖慢整个循环。本系列第 144 篇会专讲 TaskPool 并发把重计算丢到后台线程。三、100ms 物理周期的选择3.1 为什么是 100ms游戏主循环周期决定物理粒度——周期越短猫咪下落越平滑但 CPU 开销越大。周期帧率猫咪下落视觉CPU 开销适合16ms60 FPS极平滑高FPS 游戏33ms30 FPS平滑中ARPG100ms10 FPS格子跳跃本项目低消除/回合200ms5 FPS明显卡顿极低慢节奏策略本项目选 100ms 的原因消除类游戏节奏慢——猫咪每 100ms 下落一格玩家来得及反应。格子跳跃即原生视觉——猫咪本就是按格子摆放不需要 60 FPS 平滑过渡第 16 篇 position 动画做了 100ms 过渡补偿。CPU 开销低——10 FPS 更新引擎省电。3.2 改成 60 FPS 会怎样// 假设改成 16ms60 FPS this.gameLoopTimer setInterval(() { this.cats this.gameEngine.updateCats(); // 每 16ms 调一次 // ... }, 16);问题引擎按格子算物理——updateCats()每次cat.y 116ms 调一次 60 格/秒下落太快玩家看不清。CPU 浪费——引擎内部物理只支持「一格一格跳」60 FPS 调用 60 次但只产生 10 次实际位移50 次空跑。关键经验主循环周期要匹配物理粒度——格子游戏用 100ms连续物理用 16ms。3.3 position 动画补偿// 来源entry/src/main/ets/pages/Index.ets GameView() 猫咪层 ForEach(this.cats, (cat: Cat) { Column() { /* ... */ } .position({ x: ..., y: ... }) .animation({ duration: 100, curve: Curve.Linear }) // ← 100ms 动画 }, (cat: Cat) cat.id)协同机制主循环 100ms 改cat.y→ position 重新计算。.animation({ duration: 100 })让 position 从旧值平滑过渡到新值耗时 100ms。下一帧主循环再改cat.y→ 新的 100ms 动画开始。视觉上猫咪每 100ms 平滑下落一格而不是「瞬移」。关键经验主循环周期 position 动画时长——视觉最连贯。本系列第 55、56 篇会专讲 animateTo 与 animation 属性动画。四、暂停时短路返回4.1 短路守卫this.gameLoopTimer setInterval(() { if (this.gameState ! GameState.PLAYING) return; // ← 短路返回 this.cats this.gameEngine.updateCats(); this.score this.gameEngine.getScore(); this.combo this.gameEngine.getCombo(); if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100);为什么不用 clearInterval 暂停方案优点缺点短路返回本项目续期无缝、代码简单定时器一直跑微量 CPUclearInterval 暂停CPU 零开销续期要重建定时器代码复杂实战经验短停暂停用短路返回长停结束用 clearInterval——暂停要能无缝续期结束要彻底清理。4.2 三种状态的短路行为gameState主循环行为spawnTimer 行为timeTimer 行为IDLE短路返回短路返回短路返回PLAYING正常执行正常执行正常执行PAUSED短路返回短路返回短路返回不计时间GAME_OVER短路返回短路返回短路返回关键经验所有定时器都先检查gameState PLAYING——暂停/结束时统一短路避免状态错乱。4.3 暂停后恢复pauseGame() { if (this.gameState GameState.PLAYING) { this.gameState GameState.PAUSED; } } resumeGame() { if (this.gameState GameState.PAUSED) { this.gameState GameState.PLAYING; } } **恢复流程** 1. resumeGame() 把 gameState 切回 PLAYING。 2. 下一个 100ms 主循环触发短路守卫通过**正常执行物理更新**。 3. **无需重建定时器**——定时器一直在跑只是暂停期被短路。 ## 五、完整主循环代码 ts // 来源entry/src/main/ets/pages/Index.ets Entry Component struct Index { State gameState: GameState GameState.IDLE; State score: number 0; State cats: Cat[] []; State combo: ComboInfo { count: 0, multiplier: 1, lastMergeTime: 0 }; State nextCatLevel: CatLevel CatLevel.SMALL; State highScore: number 0; State gameTime: number 0; private gameEngine: GameEngine new GameEngine(); private gameLoopTimer: number -1; private spawnTimer: number -1; private timeTimer: number -1; startGame() { this.clearTimers(); this.gameEngine.reset(); this.gameState GameState.PLAYING; this.score 0; this.cats []; this.gameTime 0; this.combo { count: 0, multiplier: 1, lastMergeTime: 0 }; this.nextCatLevel this.gameEngine.getNextCatLevel(); // 游戏主循环 - 100ms 更新物理本篇重点 this.gameLoopTimer setInterval(() { if (this.gameState ! GameState.PLAYING) return; // 短路守卫 this.cats this.gameEngine.updateCats(); // 物理更新 this.score this.gameEngine.getScore(); // 同步得分 this.combo this.gameEngine.getCombo(); // 同步连击 if (this.gameEngine.isGameOver()) { this.endGame(); } }, 100); // 自动生成猫咪第 35 篇专讲 this.spawnTimer setInterval(() { if (this.gameState ! GameState.PLAYING) return; if (this.gameEngine.getCatCount() GameConfig.MAX_CATS) { this.gameEngine.autoSpawnCat(); this.cats this.gameEngine.getAllCats(); this.nextCatLevel this.gameEngine.getNextCatLevel(); } }, GameConfig.SPAWN_RATE); // 计时器第 34 篇专讲 this.timeTimer setInterval(() { if (this.gameState GameState.PLAYING) { this.gameTime; } }, 1000); } pauseGame() { if (this.gameState GameState.PLAYING) { this.gameState GameState.PAUSED; } } resumeGame() { if (this.gameState GameState.PAUSED) { this.gameState GameState.PLAYING; } } endGame() { this.gameState GameState.GAME_OVER; this.maxCombo this.gameEngine.getMaxCombo(); this.mergeCount this.gameEngine.getMergeCount(); this.highestLevel this.gameEngine.getHighestLevel(); if (this.score this.highScore) { this.highScore this.score; } this.clearTimers(); } clearTimers() { if (this.gameLoopTimer ! -1) { clearInterval(this.gameLoopTimer); this.gameLoopTimer -1; } if (this.spawnTimer ! -1) { clearInterval(this.spawnTimer); this.spawnTimer -1; } if (this.timeTimer ! -1) { clearInterval(this.timeTimer); this.timeTimer -1; } } }六、踩坑提示6.1 忘记 clearTimers 导致定时器泄漏// ❌ 错误重新开始没清旧定时器 Button(重新开始).onClick(() { this.startGame(); // 旧的 gameLoopTimer 还在跑 }) // ✅ 正确startGame 内首行 clearTimers startGame() { this.clearTimers(); // 先清旧定时器 /* ... */ }后果不清旧定时器新游戏开始后有两套主循环并行——猫「闪现」两个循环都改 cats、得分「跳变」两个都改 score。6.2 timer id 用 string 而非 number// ❌ 错误setInterval 返回 number却声明 string private gameLoopTimer: string ; // ✅ 正确声明 number初始 -1 表示无定时器 private gameLoopTimer: number -1;6.3 暂停时 clearInterval 而非短路// ⚠️ 可以但繁琐暂停就 clearInterval恢复再 setInterval pauseGame() { clearInterval(this.gameLoopTimer); this.gameLoopTimer -1; } resumeGame() { this.gameLoopTimer setInterval(() { /* ... */ }, 100); // 要重建定时器 } // ✅ 推荐短路返回定时器一直在跑 pauseGame() { this.gameState GameState.PAUSED; // 主循环内 if 短路 } resumeGame() { this.gameState GameState.PLAYING; // 主循环自动续期 }6.4 回调内改 state 丢 this// ❌ 错误普通函数 this 不指向组件 setInterval(function () { this.cats this.gameEngine.updateCats(); // this 不是 Index }, 100); // ✅ 正确箭头函数保留外层 this setInterval(() { this.cats this.gameEngine.updateCats(); }, 100);本系列第 38 篇会专讲箭头函数 this 绑定。七、调试技巧console.info打周期主循环里 logDate.now()两次 log 差就是实际周期看误差。定时器泄漏排查DevEco Profiler 看 timer 数量正常游戏时 3 个重新开始后还是 3 个不是 6 个。暂停无效排查检查gameState是否真的切到PAUSED检查主循环短路守卫。猫闪现排查DevEco Profiler 看 timer 数量闪现说明有重复定时器。八、性能与最佳实践主循环周期匹配物理粒度——格子游戏 100ms连续物理 16ms。暂停用短路返回——比 clearInterval 重建更简洁无缝续期。结束用 clearInterval——彻底清理避免泄漏。startGame 首行 clearTimers——避免新旧定时器并行。回调内别做重计算——耗时超周期会拖慢循环重计算丢 TaskPool第 144 篇。回调用箭头函数保留 this——普通函数 this 不指向组件。总结本篇我们从 setInterval 主循环切入掌握了setInterval 定时器 API、100ms 物理周期的选择依据、主循环与 position 动画的协同、暂停时短路返回四大要点并给出了完整主循环代码。核心要点周期匹配物理粒度暂停短路不 clearIntervalstartGame 首行 clearTimers 防泄漏。下一篇我们将拆解计时器——秒级 gameTime 递增。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.etssetInterval/clearInterval 官方文档HarmonyOS 定时器 API 参考ArkUI 动画与定时器协同最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md