03-游戏引擎
在上一篇文章中,我们完成了像素小鸟游戏的 UI 渲染层搭建。然而,一个游戏真正的灵魂在于其背后的引擎逻辑——状态机如何驱动画面流转、物理公式怎样让小鸟的飞行既真实又可控、管道以何种节奏生成才能既具挑战性又不至于令人绝望。本文将深入剖析这款 HarmonyOS ArkTS 像素小鸟游戏的核心引擎实现,从状态机到物理系统,从碰撞检测到动画反馈,逐层拆解纯代码驱动的游戏逻辑。

游戏状态机设计——READY / PLAYING / GAME_OVER 三态切换
游戏状态机是整个引擎的指挥中心。我们将游戏生命周期抽象为三个互斥状态,由 GameStatus 枚举定义:
export enum GameStatus {
READY = 0,
PLAYING = 1,
GAME_OVER = 2,
}GameEngine 类以 status 字段持有当前状态,并在 update() 主循环中根据状态执行截然不同的逻辑分支:
- READY:小鸟在屏幕中央做正弦波悬停动画,仅响应点击事件触发
startGame() - PLAYING:物理更新、管道生成、碰撞检测、计分判定全部激活
- GAME_OVER:冻结所有动态逻辑,等待玩家点击重置
update(timestamp: number): void {
// ...deltaTime 计算...
if (this.status === GameStatus.READY) {
this.bird.y = this.screenHeight / 2 + Math.sin(timestamp / 300) * 10;
this.bird.updateWingAnimation(deltaTime);
return;
}
if (this.status === GameStatus.GAME_OVER) {
return;
}
// PLAYING 状态:物理、管道、碰撞、计分...
}状态切换的触发点被精心设计:
| 当前状态 | 点击行为 | 目标状态 |
|---|---|---|
| READY | startGame() | PLAYING |
| PLAYING | bird.jump() | 保持 PLAYING |
| GAME_OVER | reset() | READY |
jump(): void {
if (this.status === GameStatus.PLAYING) {
this.bird.jump(GameConfig.JUMP_FORCE);
} else if (this.status === GameStatus.READY) {
this.startGame();
} else if (this.status === GameStatus.GAME_OVER) {
this.reset();
}
}这种集中式的状态管理避免了分散在各处的布尔标志(如 isPlaying、isGameOver),使得状态流转清晰可追踪。
小鸟物理系统——重力、跳跃、速度更新的数学模型
小鸟的运动遵循经典的匀加速直线运动模型,由 BirdModel 封装。其核心物理量包括垂直位置 y、垂直速度 velocityY,以及由 GameConfig 统一配置的重力加速度与跳跃冲量:
export class GameConfig {
static readonly GRAVITY: number = 0.08;
static readonly JUMP_FORCE: number = -9;
}物理更新在 BirdModel.updatePhysics() 中完成,采用欧拉积分法:
updatePhysics(gravity: number, deltaTime: number): void {
this.velocityY = this.velocityY + gravity * deltaTime / 16;
this.y = this.y + this.velocityY * deltaTime / 16;
// 根据速度更新旋转角度
this.rotation = Math.min(45, Math.max(-25, this.velocityY * 3));
}这里的关键设计是 时间归一化系数 deltaTime / 16。以 16ms 作为基准帧时长(对应约 60FPS),将物理计算与真实时间解耦:无论设备实际帧率如何,小鸟在 1 秒内的运动轨迹保持一致。GRAVITY = 0.08 与 JUMP_FORCE = -9 的数值经过 vp 坐标系下的反复调优——屏幕高度约 804vp,过大的重力会让小鸟下坠过快失去操控感,过小的跳跃力度则无法应对密集的管道间隙。
旋转角度的计算进一步增强了视觉反馈:下落时鸟头朝下(最大 45°),上升时鸟头微抬(最大 -25°),速度越大角度越极端,让玩家仅凭视觉就能预判轨迹。
jump(jumpForce: number): void {
this.velocityY = jumpForce;
}跳跃的本质是瞬间赋予一个向上的初速度,随后重力持续将其向下拉,形成抛物线轨迹。

管道生成算法——随机间隙位置、间距控制、反应时间设计
管道是游戏的核心障碍物,其生成策略直接决定了难度曲线。GameEngine.spawnPipes() 实现了智能的生成控制:
spawnPipes(): void {
const rightmostX: number = this.getRightmostPipeX();
if (rightmostX < this.screenWidth - GameConfig.PIPE_SPACING) {
const minGapY: number = GameConfig.MIN_GAP_Y;
const maxGapY: number = this.screenHeight - GameConfig.GROUND_HEIGHT - GameConfig.MIN_GAP_Y;
const gapY: number = minGapY + Math.random() * (maxGapY - minGapY);
const spawnX: number = rightmostX === 0 ? this.screenWidth + 300 : this.screenWidth + 50;
const pipe: PipeModel = new PipeModel(spawnX, gapY, this.screenHeight, GameConfig.GROUND_HEIGHT);
this.pipes.push(pipe);
}
}算法要点解析:
间距控制:PIPE_SPACING = 220 规定了相邻两组管道的最小水平间隔。只有当最右侧管道离开屏幕右边缘超过 220vp 时,才生成新管道。这避免了管道过于密集导致无法通过,也防止了过于稀疏让游戏变得无聊。
随机间隙位置:间隙中心 gapY 在 [MIN_GAP_Y, screenHeight - GROUND_HEIGHT - MIN_GAP_Y] 范围内随机分布。MIN_GAP_Y = 130 确保间隙不会贴顶或贴地,始终给玩家留出可操作空间。
反应时间设计:第一个管道的生成位置特别设置为 screenWidth + 300,而非后续的 screenWidth + 50。这 300vp 的缓冲距离给玩家从 READY 切换到 PLAYING 后足够的反应时间——小鸟从屏幕 35% 高度开始,需要约 1.5 秒才会遇到第一组管道,让玩家适应节奏。
PipeModel 本身是一个轻量数据结构:
export class PipeModel {
x: number = 0;
gapY: number = 0;
gapHeight: number = 130;
width: number = 55;
passed: boolean = false;
}passed 字段用于计分去重,确保每组管道只被计算一次。

碰撞检测——AABB 矩形碰撞(BoundsModel.intersects)
碰撞检测是游戏公平性的基石。我们采用经典的 AABB(Axis-Aligned Bounding Box) 矩形碰撞检测,由 BoundsModel 实现:
export class BoundsModel {
left: number;
top: number;
width: number;
height: number;
intersects(other: BoundsModel): boolean {
return (
this.left < other.getRight() &&
this.getRight() > other.left &&
this.top < other.getBottom() &&
this.getBottom() > other.top
);
}
}AABB 碰撞的核心逻辑是判断两个矩形在 X 轴和 Y 轴上的投影是否同时重叠。四个条件缺一不可:
this.left < other.getRight():本矩形左边缘在对方右边缘左侧this.getRight() > other.left:本矩形右边缘在对方左边缘右侧this.top < other.getBottom():本矩形上边缘在对方下边缘上方this.getBottom() > other.top:本矩形下边缘在对方上边缘下方
在 GameEngine.checkCollisions() 中,碰撞检测分两层执行:
checkCollisions(): void {
const birdBounds: BoundsModel = this.bird.getBounds();
const groundY: number = this.screenHeight - GameConfig.GROUND_HEIGHT;
// 与地面碰撞
if (birdBounds.getBottom() > groundY) {
this.gameOver();
return;
}
// 与管道碰撞
for (let i: number = 0; i < this.pipes.length; i++) {
const topBounds: BoundsModel = this.pipes[i].getTopPipeBounds();
const bottomBounds: BoundsModel = this.pipes[i].getBottomPipeBounds(this.screenHeight, GameConfig.GROUND_HEIGHT);
if (birdBounds.intersects(topBounds) || birdBounds.intersects(bottomBounds)) {
this.gameOver();
return;
}
}
}小鸟的碰撞矩形以中心点计算:
getBounds(): BoundsModel {
return new BoundsModel(
this.x - this.width / 2,
this.y - this.height / 2,
this.width,
this.height
);
}管道的碰撞矩形则包含头部外扩:
getTopPipeBounds(): BoundsModel {
return new BoundsModel(
this.x - this.capExtraWidth,
0,
this.width + this.capExtraWidth * 2,
this.gapY - this.gapHeight / 2
);
}capExtraWidth = 7 让碰撞区域略宽于管道主体,确保视觉接触即判定碰撞,避免玩家感觉"明明没碰到却死了"的挫败感。
计分系统——通过管道间隙的判定逻辑
计分逻辑出奇地简洁,却暗藏设计巧思:
updateScore(): void {
for (let i: number = 0; i < this.pipes.length; i++) {
if (!this.pipes[i].passed && this.pipes[i].getCenterX() < this.bird.x) {
this.pipes[i].passed = true;
this.score = this.score + 1;
this.scoreScale = 1.5;
this.scoreAnimTimer = 0;
}
}
}判定条件 getCenterX() < this.bird.x 意味着:当管道的中心线从小鸟右侧移动到左侧时,即视为成功通过。这个设计有几点考量:
- 中心线判定:以管道中心而非边缘作为判定基准,避免小鸟在管道正上方/下方横向移动时误触发
- 单次计分:
passed标志位确保每组管道只计一次分,即使小鸟在管道附近来回移动 - 即时反馈:计分瞬间触发分数放大动画(
scoreScale = 1.5),给玩家正向激励
最高分在 gameOver() 中持久化更新:
gameOver(): void {
this.status = GameStatus.GAME_OVER;
if (this.score > this.bestScore) {
this.bestScore = this.score;
}
}动画系统——翅膀扇动、分数缩放动画
游戏引擎管理两类动画:小鸟翅膀扇动和分数弹出效果。
翅膀扇动 由 BirdModel 自主管理,采用帧动画机制:
updateWingAnimation(deltaTime: number): void {
this.wingTimer = this.wingTimer + deltaTime;
if (this.wingTimer > 100) {
this.wingTimer = 0;
this.wingFrame = this.wingFrame + 1;
if (this.wingFrame > 2) {
this.wingFrame = 0;
}
}
}每 100ms 切换一帧,wingFrame 在 0、1、2 之间循环。渲染层根据当前帧索引绘制不同的翅膀姿态,形成连续的扇动效果。这个动画在 READY 和 PLAYING 状态都会执行,让小鸟始终保持生命力。
分数缩放动画 由 GameEngine 集中控制:
updateScoreAnimation(deltaTime: number): void {
if (this.scoreScale > 1) {
this.scoreAnimTimer = this.scoreAnimTimer + deltaTime;
this.scoreScale = 1.5 - (this.scoreAnimTimer / 200) * 0.5;
if (this.scoreScale < 1) {
this.scoreScale = 1;
}
}
}计分时 scoreScale 被设为 1.5,随后在 200ms 内线性衰减回 1.0。渲染层将 scoreScale 应用到分数文本的绘制尺寸上,形成"分数弹出"的视觉效果。这种简单的插值动画无需借助复杂的动画库,纯代码即可实现流畅反馈。
时间步控制——deltaTime 与固定时间步
游戏循环的核心是时间步控制。GameEngine.update() 在每一帧被 Canvas 的 onFrame() 回调触发,接收浏览器提供的 timestamp(毫秒级高精度时间):
update(timestamp: number): void {
if (this.lastTime === 0) {
this.lastTime = timestamp;
}
const deltaTime: number = Math.min(timestamp - this.lastTime, 33);
this.lastTime = timestamp;
// ...
}deltaTime 计算:timestamp - this.lastTime 得到两帧之间的真实时间间隔。Math.min(..., 33) 是关键的安全阀——当设备卡顿或切回前台时,防止 deltaTime 暴增至数百毫秒导致物理爆炸(小鸟瞬间飞出屏幕)。33ms 对应约 30FPS 的最低帧率保障。
时间归一化:所有物理和移动计算都遵循统一的模式:
// 小鸟物理
this.velocityY = this.velocityY + (gravity * deltaTime) / 16;
this.y = this.y + (this.velocityY * deltaTime) / 16;
// 管道移动
this.x = this.x - (speed * deltaTime) / 16;以 16ms(60FPS)为基准,将位移和速度变化与帧率解耦。这意味着在 120Hz 高刷屏上游戏运行更流畅,但小鸟的飞行轨迹与 60Hz 设备完全一致——这是保证游戏公平性的关键。
坐标系统适配——vp 坐标与物理参数的调优
HarmonyOS ArkUI 使用 vp(virtual pixel,虚拟像素)作为长度单位,它会根据屏幕密度自动缩放。nova 12 的物理分辨率为 1084x2412,但在 vp 坐标系下,实际可用的 Canvas 高度约为 804vp——这是调优所有物理参数时必须牢记的基准。
export class GameConfig {
static readonly GRAVITY: number = 0.08;
static readonly JUMP_FORCE: number = -9;
static readonly GAME_SPEED: number = 1.5;
static readonly GROUND_HEIGHT: number = 80;
static readonly PIPE_SPACING: number = 220;
static readonly MIN_GAP_Y: number = 130;
static readonly BIRD_START_X: number = 80;
}参数调优的物理直觉:
- GRAVITY = 0.08:在 804vp 的高度下,自由落体从顶部到底部约需 2 秒,给予玩家充足的反应窗口
- JUMP_FORCE = -9:每次跳跃提供约 120vp 的上升高度,恰好能穿越标准 130vp 的管道间隙
- GAME_SPEED = 1.5:管道以每秒约 94vp 的速度左移(1.5 * 60),配合 220vp 的间距,玩家每 2.3 秒面对一组新管道
- GROUND_HEIGHT = 80:地面占据屏幕底部约 10%,留出足够的飞行空间
无敌时间机制进一步照顾了新手体验:
startGame(): void {
this.status = GameStatus.PLAYING;
this.bird.y = this.screenHeight * 0.35;
this.bird.velocityY = 0;
this.bird.jump(GameConfig.JUMP_FORCE);
this.invincibleTimer = 500; // 500ms 无敌时间
}游戏开始后的 500ms 内,checkCollisions() 直接返回,避免玩家因从 READY 切换到 PLAYING 的延迟点击而立即撞管。

总结
本文从状态机、物理系统、管道生成、碰撞检测、计分逻辑、动画反馈、时间步控制和坐标适配八个维度,完整拆解了像素小鸟游戏的核心引擎。整个引擎约 300 行代码,却涵盖了游戏开发中最经典的设计模式:
- 状态机 管理游戏生命周期,逻辑清晰无歧义
- 欧拉积分 驱动物理模拟,时间归一化保证跨设备一致性
- AABB 碰撞 简单高效,足以应对 2D 矩形场景
- 对象池思想 通过管道数组的增删复用,避免频繁创建销毁对象
- deltaTime 安全阀 防止卡顿导致的物理异常
在 HarmonyOS ArkTS 的 vp 坐标系下,所有物理参数都需要以屏幕相对比例而非绝对像素来思考。804vp 的可用高度是调优的锚点,重力、跳跃力度、管道间距都在这个尺度上找到了平衡点。
下一篇文章将聚焦性能优化与真机调试——如何让游戏在 nova 12 上以稳定 60FPS 运行,以及 ArkTS 游戏开发中常见的性能陷阱与规避策略。