HarmonyOS游戏开发:2D游戏引擎架构与Canvas游戏开发
HarmonyOS游戏开发:2D游戏引擎架构与Canvas游戏开发
📌 核心要点:从游戏循环到精灵系统,从碰撞检测到场景管理,构建一个可运行的2D游戏引擎并实战飞机大战
一、背景与动机
你有没有想过,为什么有些手游运行丝般顺滑,而有些却卡得像PPT?为什么同样的游戏逻辑,有人写出来结构清晰、扩展方便,有人写出来却是一坨"意大利面条"代码?
答案就藏在游戏引擎架构里。
游戏引擎不是一个神秘的黑盒子,它本质上就是一套管理游戏生命周期、渲染、物理、输入的框架。对于2D游戏来说,核心组件其实并不复杂——游戏循环、精灵系统、碰撞检测、场景管理,这四大件搞定,你就能开发出像样的2D游戏。
那为什么不用现成的引擎呢?比如Cocos2d、Unity?当然可以,但在HarmonyOS生态中,ArkTS + Canvas的组合有着独特的优势:更轻量、更原生、更可控。而且,理解引擎的底层原理,对你在任何引擎上开发游戏都大有裨益——知其然更要知其所以然嘛。
想象一下这个场景:老板让你两周内做一个简单的2D小游戏做活动页,用Unity太重,用Web又不够原生。这时候,用ArkTS写一个轻量2D引擎,就是最佳选择。
二、核心原理
2.1 2D游戏引擎整体架构
一个2D游戏引擎的架构,可以抽象为以下层次:
graph TD
A[游戏入口<br>Entry]:::primary --> B[游戏循环<br>GameLoop]
B --> C{帧更新}:::warning
C --> D[输入处理<br>InputManager]:::info
C --> E[逻辑更新<br>Update]:::info
C --> F[渲染绘制<br>Render]:::info
E --> G[精灵系统<br>SpriteSystem]:::primary
E --> H[碰撞检测<br>CollisionSystem]:::error
E --> I[场景管理<br>SceneManager]:::warning
G --> J[动画管理<br>AnimationManager]:::info
I --> K[摄像机系统<br>CameraSystem]:::info
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
游戏循环是引擎的心脏,它以固定频率驱动整个游戏运转;输入处理捕获用户的触摸和按键事件;逻辑更新处理游戏逻辑(移动、碰撞、AI等);渲染绘制把游戏画面画到屏幕上。精灵系统、碰撞检测、场景管理则是逻辑更新的三大子系统。
2.2 游戏循环与帧率控制
游戏循环的核心问题是:如何保证游戏在不同设备上以相同的速度运行?
最简单的循环是"固定步长循环"——每帧固定更新一次,帧率取决于设备性能。但这样有个问题:快设备上游戏跑得太快,慢设备上又太慢。
解决方案是固定时间步长 + 可变渲染(Fixed Timestep with Variable Rendering):
graph TD
A[获取当前时间]:::primary --> B[计算时间差deltaTime]:::info
B --> C{deltaTime >= 固定步长?}:::warning
C -->|是| D[执行一次逻辑更新<br>deltaTime -= 固定步长]:::primary
D --> C
C -->|否| E[执行渲染]:::info
E --> F[等待下一帧]:::error
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
这样,无论设备性能如何,逻辑更新的频率始终是固定的(比如60次/秒),渲染则尽可能快地执行。游戏速度不再受帧率影响。
2.3 精灵系统与动画管理
精灵(Sprite)是2D游戏中最基本的可视元素。一个精灵包含位置、大小、纹理、动画等属性。动画管理则负责在精灵的多个纹理帧之间切换,实现"帧动画"效果。
精灵系统的设计要点:
- 对象池:频繁创建销毁精灵(如子弹)会导致GC压力,用对象池复用
- 批量渲染:相同纹理的精灵合并绘制,减少draw call
- 动画状态机:管理精灵的多种动画状态(站立、奔跑、攻击)之间的切换
2.4 碰撞检测算法
碰撞检测是游戏逻辑的核心。2D游戏中常用的碰撞检测算法:
| 算法 | 适用场景 | 复杂度 | 精度 |
|---|---|---|---|
| AABB | 矩形碰撞 | O(1) | 低 |
| 圆形碰撞 | 圆形物体 | O(1) | 中 |
| SAT | 凸多边形 | O(n) | 高 |
| 像素级 | 精确碰撞 | O(w×h) | 极高 |
对于大多数2D游戏,AABB + 圆形碰撞的组合已经足够。先用AABB做粗筛(Broad Phase),再用圆形或SAT做精筛(Narrow Phase),这就是经典的"两阶段碰撞检测"。
三、代码实战
3.1 基础用法:游戏循环与精灵
先搭建游戏引擎的骨架——游戏循环和精灵类:
// 游戏引擎核心类
class GameEngine {
private canvas: CanvasRenderingContext2D
private canvasWidth: number = 0
private canvasHeight: number = 0
private lastTime: number = 0
private fixedDeltaTime: number = 1000 / 60 // 固定步长:约16.67ms
private accumulator: number = 0
private isRunning: boolean = false
private animFrameId: number = -1
// 游戏对象列表
private gameObjects: Array<GameObject> = []
// 输入状态
private inputState: InputState = { touches: [], keys: new Set() }
constructor(canvas: CanvasRenderingContext2D, width: number, height: number) {
this.canvas = canvas
this.canvasWidth = width
this.canvasHeight = height
}
// 启动游戏循环
start() {
this.isRunning = true
this.lastTime = Date.now()
this.accumulator = 0
this.gameLoop()
}
// 停止游戏循环
stop() {
this.isRunning = false
if (this.animFrameId !== -1) {
cancelAnimationFrame(this.animFrameId)
}
}
// 游戏主循环
private gameLoop() {
if (!this.isRunning) return
const currentTime = Date.now()
const deltaTime = currentTime - this.lastTime
this.lastTime = currentTime
// 防止螺旋死亡(设备卡顿时deltaTime过大)
const clampedDelta = Math.min(deltaTime, 250)
this.accumulator += clampedDelta
// 固定步长逻辑更新
while (this.accumulator >= this.fixedDeltaTime) {
this.fixedUpdate(this.fixedDeltaTime / 1000) // 转换为秒
this.accumulator -= this.fixedDeltaTime
}
// 渲染(可变步长)
const alpha = this.accumulator / this.fixedDeltaTime
this.render(alpha)
// 请求下一帧
this.animFrameId = requestAnimationFrame(() => this.gameLoop())
}
// 固定步长逻辑更新
private fixedUpdate(dt: number) {
// 更新所有游戏对象
for (const obj of this.gameObjects) {
if (obj.active) {
obj.update(dt)
}
}
// 碰撞检测
this.checkCollisions()
// 清理已销毁的对象
this.gameObjects = this.gameObjects.filter(obj => obj.active)
}
// 渲染
private render(alpha: number) {
const ctx = this.canvas
// 清屏
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制背景
ctx.fillStyle = '#000011'
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制所有游戏对象
for (const obj of this.gameObjects) {
if (obj.active && obj.visible) {
obj.render(ctx, alpha)
}
}
}
// 碰撞检测
private checkCollisions() {
for (let i = 0; i < this.gameObjects.length; i++) {
for (let j = i + 1; j < this.gameObjects.length; j++) {
const a = this.gameObjects[i]
const b = this.gameObjects[j]
if (!a.active || !b.active) continue
if (!a.collider || !b.collider) continue
if (this.testCollision(a, b)) {
a.onCollision(b)
b.onCollision(a)
}
}
}
}
// AABB碰撞测试
private testCollision(a: GameObject, b: GameObject): boolean {
const ca = a.collider!
const cb = b.collider!
if (ca.type === ColliderType.AABB && cb.type === ColliderType.AABB) {
// 矩形碰撞
return !(ca.maxX < cb.minX || ca.minX > cb.maxX ||
ca.maxY < cb.minY || ca.minY > cb.maxY)
}
if (ca.type === ColliderType.CIRCLE && cb.type === ColliderType.CIRCLE) {
// 圆形碰撞
const dx = ca.centerX - cb.centerX
const dy = ca.centerY - cb.centerY
const dist = Math.sqrt(dx * dx + dy * dy)
return dist < ca.radius! + cb.radius!
}
return false
}
// 添加游戏对象
addGameObject(obj: GameObject) {
this.gameObjects.push(obj)
}
// 移除游戏对象
removeGameObject(obj: GameObject) {
obj.active = false
}
// 更新输入状态
updateInput(touches: Array<TouchPoint>, keys: Set<string>) {
this.inputState.touches = touches
this.inputState.keys = keys
}
}
// 游戏对象基类
abstract class GameObject {
// 基础属性
x: number = 0
y: number = 0
width: number = 0
height: number = 0
rotation: number = 0
scale: number = 1
active: boolean = true
visible: boolean = true
// 速度
vx: number = 0
vy: number = 0
// 碰撞体
collider: ColliderData | null = null
// 标签(用于碰撞过滤)
tag: string = ''
// 逻辑更新
update(dt: number) {
// 基础运动
this.x += this.vx * dt
this.y += this.vy * dt
// 更新碰撞体位置
this.updateCollider()
}
// 渲染
abstract render(ctx: CanvasRenderingContext2D, alpha: number): void
// 碰撞回调
onCollision(other: GameObject) {}
// 更新碰撞体数据
protected updateCollider() {
if (this.collider) {
this.collider.minX = this.x - this.width / 2
this.collider.maxX = this.x + this.width / 2
this.collider.minY = this.y - this.height / 2
this.collider.maxY = this.y + this.height / 2
this.collider.centerX = this.x
this.collider.centerY = this.y
}
}
// 设置AABB碰撞体
setAABBCollider() {
this.collider = {
type: ColliderType.AABB,
minX: this.x - this.width / 2,
maxX: this.x + this.width / 2,
minY: this.y - this.height / 2,
maxY: this.y + this.height / 2,
centerX: this.x,
centerY: this.y,
radius: 0
}
}
// 设置圆形碰撞体
setCircleCollider(radius: number) {
this.collider = {
type: ColliderType.CIRCLE,
minX: 0, maxX: 0, minY: 0, maxY: 0,
centerX: this.x,
centerY: this.y,
radius: radius
}
}
}
// 碰撞体类型枚举
enum ColliderType {
AABB = 'aabb',
CIRCLE = 'circle'
}
// 碰撞体数据
interface ColliderData {
type: ColliderType
minX: number
maxX: number
minY: number
maxY: number
centerX: number
centerY: number
radius: number
}
// 输入状态
interface InputState {
touches: Array<TouchPoint>
keys: Set<string>
}
interface TouchPoint {
x: number
y: number
id: number
}
这段代码实现了游戏引擎的核心骨架:固定步长游戏循环、游戏对象基类、AABB/圆形碰撞检测。有了这个骨架,后续添加任何游戏元素都只需要继承GameObject即可。
3.2 进阶用法:精灵动画与对象池
精灵动画和对象池是2D游戏引擎的两大核心子系统:
// 精灵动画系统
class SpriteAnimation {
// 动画帧列表
private frames: Array<AnimationFrame> = []
private currentFrameIndex: number = 0
private elapsedTime: number = 0
private isPlaying: boolean = true
private isLooping: boolean = true
// 添加动画帧
addFrame(resource: Resource, duration: number) {
this.frames.push({ resource, duration })
}
// 更新动画
update(dt: number) {
if (!this.isPlaying || this.frames.length === 0) return
this.elapsedTime += dt
const currentFrame = this.frames[this.currentFrameIndex]
if (this.elapsedTime >= currentFrame.duration) {
this.elapsedTime -= currentFrame.duration
this.currentFrameIndex++
if (this.currentFrameIndex >= this.frames.length) {
if (this.isLooping) {
this.currentFrameIndex = 0
} else {
this.currentFrameIndex = this.frames.length - 1
this.isPlaying = false
}
}
}
}
// 获取当前帧
getCurrentFrame(): AnimationFrame | null {
if (this.frames.length === 0) return null
return this.frames[this.currentFrameIndex]
}
// 播放控制
play() { this.isPlaying = true }
pause() { this.isPlaying = false }
reset() { this.currentFrameIndex = 0; this.elapsedTime = 0; this.isPlaying = true }
}
// 动画帧数据
interface AnimationFrame {
resource: Resource
duration: number // 帧持续时间(毫秒)
}
// 对象池 - 复用游戏对象,避免频繁GC
class ObjectPool<T extends GameObject> {
private pool: Array<T> = []
private factory: () => T
private resetFn: (obj: T) => void
private maxSize: number
constructor(factory: () => T, resetFn: (obj: T) => void, maxSize: number = 100) {
this.factory = factory
this.resetFn = resetFn
this.maxSize = maxSize
}
// 从池中获取对象
acquire(): T {
if (this.pool.length > 0) {
const obj = this.pool.pop()!
obj.active = true
obj.visible = true
return obj
}
return this.factory()
}
// 将对象归还到池中
release(obj: T) {
if (this.pool.length >= this.maxSize) return
this.resetFn(obj)
obj.active = false
obj.visible = false
this.pool.push(obj)
}
// 获取池中可用对象数量
get availableCount(): number {
return this.pool.length
}
}
// 场景管理器
class SceneManager {
private scenes: Map<string, Scene> = new Map()
private currentScene: Scene | null = null
// 注册场景
registerScene(name: string, scene: Scene) {
this.scenes.set(name, scene)
}
// 切换场景
switchScene(name: string) {
if (this.currentScene) {
this.currentScene.onExit()
}
const scene = this.scenes.get(name)
if (scene) {
this.currentScene = scene
this.currentScene.onEnter()
}
}
// 更新当前场景
update(dt: number) {
this.currentScene?.update(dt)
}
// 渲染当前场景
render(ctx: CanvasRenderingContext2D, alpha: number) {
this.currentScene?.render(ctx, alpha)
}
}
// 场景基类
abstract class Scene {
abstract onEnter(): void
abstract onExit(): void
abstract update(dt: number): void
abstract render(ctx: CanvasRenderingContext2D, alpha: number): void
}
// 摄像机系统
class Camera {
// 摄像机位置(世界坐标)
x: number = 0
y: number = 0
// 视口大小
viewportWidth: number = 0
viewportHeight: number = 0
// 缩放
zoom: number = 1.0
// 跟随目标
private followTarget: GameObject | null = null
private followLerp: number = 0.1 // 跟随平滑系数
// 设置跟随目标
follow(target: GameObject, lerp: number = 0.1) {
this.followTarget = target
this.followLerp = lerp
}
// 更新摄像机位置
update(dt: number) {
if (this.followTarget) {
// 平滑跟随
const targetX = this.followTarget.x - this.viewportWidth / 2
const targetY = this.followTarget.y - this.viewportHeight / 2
this.x += (targetX - this.x) * this.followLerp
this.y += (targetY - this.y) * this.followLerp
}
}
// 世界坐标转屏幕坐标
worldToScreen(worldX: number, worldY: number): { x: number; y: number } {
return {
x: (worldX - this.x) * this.zoom,
y: (worldY - this.y) * this.zoom
}
}
// 屏幕坐标转世界坐标
screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
return {
x: screenX / this.zoom + this.x,
y: screenY / this.zoom + this.y
}
}
// 应用摄像机变换到Canvas
applyTransform(ctx: CanvasRenderingContext2D) {
ctx.save()
ctx.scale(this.zoom, this.zoom)
ctx.translate(-this.x, -this.y)
}
// 恢复Canvas变换
restoreTransform(ctx: CanvasRenderingContext2D) {
ctx.restore()
}
}
对象池的使用场景非常明确:子弹、粒子、敌人等频繁创建销毁的对象。通过对象池复用,GC压力大幅降低,游戏运行更稳定。
3.3 完整示例:飞机大战
下面用我们搭建的引擎,实现一个完整的飞机大战游戏:
// 飞机大战游戏页面
@Entry
@Component
struct PlaneWarPage {
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings())
private engine: GameEngine | null = null
private bulletPool: ObjectPool<Bullet> | null = null
private enemyPool: ObjectPool<Enemy> | null = null
// 游戏状态
@State score: number = 0
@State lives: number = 3
@State gameState: GameState = GameState.READY
@State highScore: number = 0
// 触摸控制
private touchX: number = 0
private touchY: number = 0
private isTouching: boolean = false
private player: Player | null = null
// 敌人生成计时器
private enemySpawnTimer: number = 0
private enemySpawnInterval: number = 1.0 // 每秒生成一个敌人
// 子弹发射计时器
private bulletFireTimer: number = 0
private bulletFireInterval: number = 0.15 // 每0.15秒发射一颗子弹
build() {
Stack() {
// 游戏画布
Canvas(this.context)
.width('100%')
.height('100%')
.onReady(() => {
this.initGame()
})
.onTouch((event: TouchEvent) => {
this.handleTouch(event)
})
// UI叠加层
Column() {
// 顶部信息栏
Row() {
Text(`分数: ${this.score}`)
.fontSize(18)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Blank()
Text(`生命: ${'❤️'.repeat(this.lives)}`)
.fontSize(18)
.fontColor('#FFFFFF')
}
.width('100%')
.padding(16)
Blank()
// 游戏状态提示
if (this.gameState === GameState.READY) {
Column() {
Text('🛩️ 飞机大战')
.fontSize(36)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Text('触摸屏幕开始游戏')
.fontSize(16)
.fontColor('#AAAAAA')
.margin({ top: 16 })
}
.justifyContent(FlexAlign.Center)
}
if (this.gameState === GameState.GAME_OVER) {
Column() {
Text('游戏结束')
.fontSize(36)
.fontColor('#FF4444')
.fontWeight(FontWeight.Bold)
Text(`最终分数: ${this.score}`)
.fontSize(20)
.fontColor('#FFFFFF')
.margin({ top: 8 })
Text(`最高分: ${this.highScore}`)
.fontSize(16)
.fontColor('#AAAAAA')
.margin({ top: 4 })
Text('点击屏幕重新开始')
.fontSize(14)
.fontColor('#AAAAAA')
.margin({ top: 24 })
}
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.height('100%')
}
}
// 初始化游戏
private initGame() {
const canvasWidth = this.context.width
const canvasHeight = this.context.height
this.engine = new GameEngine(this.context, canvasWidth, canvasHeight)
// 初始化对象池
this.bulletPool = new ObjectPool<Bullet>(
() => new Bullet(canvasWidth, canvasHeight),
(bullet) => bullet.reset(),
50
)
this.enemyPool = new ObjectPool<Enemy>(
() => new Enemy(canvasWidth, canvasHeight),
(enemy) => enemy.reset(),
30
)
// 创建玩家
this.player = new Player(canvasWidth / 2, canvasHeight - 100, canvasWidth, canvasHeight)
this.player.setCircleCollider(20)
this.engine.addGameObject(this.player)
// 绘制初始画面
this.context.fillStyle = '#000011'
this.context.fillRect(0, 0, canvasWidth, canvasHeight)
}
// 处理触摸事件
private handleTouch(event: TouchEvent) {
const touch = event.touches[0]
this.touchX = touch.x
this.touchY = touch.y
if (event.type === TouchType.Down) {
this.isTouching = true
if (this.gameState === GameState.READY) {
this.startGame()
} else if (this.gameState === GameState.GAME_OVER) {
this.restartGame()
}
} else if (event.type === TouchType.Move) {
this.touchX = touch.x
this.touchY = touch.y
} else if (event.type === TouchType.Up) {
this.isTouching = false
}
}
// 开始游戏
private startGame() {
this.gameState = GameState.PLAYING
this.score = 0
this.lives = 3
this.enemySpawnTimer = 0
this.bulletFireTimer = 0
this.engine?.start()
this.runGameLogic()
}
// 重新开始
private restartGame() {
this.gameState = GameState.PLAYING
this.score = 0
this.lives = 3
this.enemySpawnTimer = 0
this.bulletFireTimer = 0
// 重置玩家位置
if (this.player) {
this.player.x = this.context.width / 2
this.player.y = this.context.height - 100
this.player.active = true
this.player.visible = true
}
this.engine?.start()
}
// 游戏逻辑循环(在引擎的update之外额外处理)
private runGameLogic() {
// 这里用setInterval模拟游戏逻辑的额外更新
// 实际项目中应该集成到GameEngine的fixedUpdate中
const logicInterval = setInterval(() => {
if (this.gameState !== GameState.PLAYING) {
clearInterval(logicInterval)
return
}
const dt = 1 / 60
// 玩家跟随触摸位置
if (this.player && this.isTouching) {
const dx = this.touchX - this.player.x
const dy = this.touchY - this.player.y
this.player.x += dx * 0.15
this.player.y += dy * 0.15
// 限制玩家在屏幕内
this.player.x = Math.max(20, Math.min(this.context.width - 20, this.player.x))
this.player.y = Math.max(20, Math.min(this.context.height - 20, this.player.y))
}
// 自动发射子弹
this.bulletFireTimer += dt
if (this.bulletFireTimer >= this.bulletFireInterval) {
this.bulletFireTimer = 0
this.spawnBullet()
}
// 生成敌人
this.enemySpawnTimer += dt
if (this.enemySpawnTimer >= this.enemySpawnInterval) {
this.enemySpawnTimer = 0
this.spawnEnemy()
// 逐渐加快敌人生成速度
this.enemySpawnInterval = Math.max(0.3, this.enemySpawnInterval - 0.002)
}
}, 1000 / 60)
}
// 生成子弹
private spawnBullet() {
if (!this.player || !this.bulletPool || !this.engine) return
const bullet = this.bulletPool.acquire()
bullet.x = this.player.x
bullet.y = this.player.y - 30
bullet.vy = -600 // 子弹向上飞
bullet.tag = 'bullet'
bullet.setCircleCollider(4)
// 子弹碰撞回调
bullet.onCollision = (other: GameObject) => {
if (other.tag === 'enemy') {
// 击中敌人
this.score += 10
bullet.active = false
other.active = false
this.bulletPool?.release(bullet)
this.enemyPool?.release(other as Enemy)
}
}
this.engine.addGameObject(bullet)
}
// 生成敌人
private spawnEnemy() {
if (!this.enemyPool || !this.engine) return
const enemy = this.enemyPool.acquire()
enemy.x = Math.random() * (this.context.width - 60) + 30
enemy.y = -30
enemy.vy = 100 + Math.random() * 150 // 随机下落速度
enemy.tag = 'enemy'
enemy.setCircleCollider(18)
// 敌人碰撞回调
enemy.onCollision = (other: GameObject) => {
if (other.tag === 'player') {
// 玩家被撞
this.lives--
enemy.active = false
this.enemyPool?.release(enemy)
if (this.lives <= 0) {
this.gameOver()
}
}
}
this.engine.addGameObject(enemy)
}
// 游戏结束
private gameOver() {
this.gameState = GameState.GAME_OVER
if (this.score > this.highScore) {
this.highScore = this.score
}
this.engine?.stop()
}
}
// 玩家飞机
class Player extends GameObject {
private canvasWidth: number
private canvasHeight: number
constructor(x: number, y: number, canvasWidth: number, canvasHeight: number) {
super()
this.x = x
this.y = y
this.width = 40
this.height = 48
this.canvasWidth = canvasWidth
this.canvasHeight = canvasHeight
this.tag = 'player'
}
render(ctx: CanvasRenderingContext2D, alpha: number): void {
ctx.save()
ctx.translate(this.x, this.y)
// 绘制飞机机身
ctx.fillStyle = '#4FC3F7'
ctx.beginPath()
ctx.moveTo(0, -24) // 机头
ctx.lineTo(-20, 20) // 左翼
ctx.lineTo(-6, 12) // 左腰
ctx.lineTo(0, 24) // 尾部
ctx.lineTo(6, 12) // 右腰
ctx.lineTo(20, 20) // 右翼
ctx.closePath()
ctx.fill()
// 绘制驾驶舱
ctx.fillStyle = '#81D4FA'
ctx.beginPath()
ctx.ellipse(0, -4, 5, 10, 0, 0, Math.PI * 2)
ctx.fill()
// 绘制引擎火焰
ctx.fillStyle = '#FF6D00'
ctx.beginPath()
ctx.moveTo(-4, 20)
ctx.lineTo(0, 28 + Math.random() * 6)
ctx.lineTo(4, 20)
ctx.closePath()
ctx.fill()
ctx.restore()
}
update(dt: number) {
super.update(dt)
// 超出屏幕的边界约束
this.x = Math.max(20, Math.min(this.canvasWidth - 20, this.x))
this.y = Math.max(20, Math.min(this.canvasHeight - 20, this.y))
}
reset() {
this.vx = 0
this.vy = 0
this.active = true
this.visible = true
}
}
// 子弹
class Bullet extends GameObject {
private canvasHeight: number
constructor(canvasWidth: number, canvasHeight: number) {
super()
this.width = 6
this.height = 16
this.canvasHeight = canvasHeight
}
render(ctx: CanvasRenderingContext2D, alpha: number): void {
ctx.save()
ctx.translate(this.x, this.y)
// 子弹光效
ctx.fillStyle = '#00E5FF'
ctx.shadowColor = '#00E5FF'
ctx.shadowBlur = 8
ctx.beginPath()
ctx.ellipse(0, 0, 3, 8, 0, 0, Math.PI * 2)
ctx.fill()
ctx.restore()
}
update(dt: number) {
super.update(dt)
// 子弹飞出屏幕后自动销毁
if (this.y < -20) {
this.active = false
}
}
reset() {
this.x = 0
this.y = 0
this.vx = 0
this.vy = 0
this.active = false
this.visible = false
}
}
// 敌机
class Enemy extends GameObject {
private canvasHeight: number
private swayOffset: number = 0
private swaySpeed: number = 0
private swayAmplitude: number = 0
constructor(canvasWidth: number, canvasHeight: number) {
super()
this.width = 36
this.height = 36
this.canvasHeight = canvasHeight
this.initSway()
}
private initSway() {
this.swayOffset = Math.random() * Math.PI * 2
this.swaySpeed = 1 + Math.random() * 2
this.swayAmplitude = 20 + Math.random() * 30
}
render(ctx: CanvasRenderingContext2D, alpha: number): void {
ctx.save()
ctx.translate(this.x, this.y)
// 敌机机身
ctx.fillStyle = '#EF5350'
ctx.beginPath()
ctx.moveTo(0, 18) // 机头(朝下)
ctx.lineTo(-18, -14) // 左翼
ctx.lineTo(-5, -8) // 左腰
ctx.lineTo(0, -18) // 尾部
ctx.lineTo(5, -8) // 右腰
ctx.lineTo(18, -14) // 右翼
ctx.closePath()
ctx.fill()
// 驾驶舱
ctx.fillStyle = '#E57373'
ctx.beginPath()
ctx.ellipse(0, 2, 4, 8, 0, 0, Math.PI * 2)
ctx.fill()
ctx.restore()
}
update(dt: number) {
super.update(dt)
// 左右摇摆
this.swayOffset += this.swaySpeed * dt
this.x += Math.sin(this.swayOffset) * this.swayAmplitude * dt
// 飞出屏幕后自动销毁
if (this.y > this.canvasHeight + 30) {
this.active = false
}
}
reset() {
this.x = 0
this.y = 0
this.vx = 0
this.vy = 0
this.active = false
this.visible = false
this.initSway()
}
}
// 游戏状态枚举
enum GameState {
READY = 'ready',
PLAYING = 'playing',
GAME_OVER = 'gameOver'
}
这个飞机大战实现了完整的游戏流程:开始→游玩→结束→重新开始。玩家飞机跟随触摸位置移动,自动发射子弹,敌机从上方随机生成并下落,击中敌机得分,被撞则扣命。
四、踩坑与注意事项
坑点1:requestAnimationFrame的回调时机
HarmonyOS中requestAnimationFrame的回调时机与浏览器略有不同。在某些设备上,回调可能不是精确的16.67ms间隔,而是受系统调度影响出现波动。务必在游戏循环中使用deltaTime而非固定步长来计算运动,否则游戏速度会忽快忽慢。
坑点2:Canvas的shadowBlur性能陷阱
子弹的光效用了shadowBlur,这在少量对象时没问题,但如果有上百颗子弹同时开启阴影,帧率会断崖式下降。shadowBlur是非常昂贵的GPU操作,生产环境中应该用预渲染的发光纹理替代实时阴影:
// 错误:每帧实时计算阴影
ctx.shadowColor = '#00E5FF'
ctx.shadowBlur = 8
ctx.fill()
// 正确:预渲染发光纹理
// 在初始化时创建离屏Canvas,绘制一次发光效果
// 渲染时直接drawImage
ctx.drawImage(this.glowTexture, this.x - 8, this.y - 8)
坑点3:对象池的内存泄漏
对象池如果只进不出,也会导致内存泄漏。常见场景是:游戏结束时没有清空对象池,所有对象都堆积在池中。务必在场景切换或游戏重置时清空对象池:
// 游戏重置时清空对象池
private clearPools() {
this.bulletPool = new ObjectPool<Bullet>(...)
this.enemyPool = new ObjectPool<Enemy>(...)
}
坑点4:碰撞检测的O(n²)性能问题
当前碰撞检测是两两遍历,复杂度O(n²)。当游戏对象超过100个时,帧率会明显下降。解决方案是空间分区——把游戏空间划分为网格,只检测同一网格及相邻网格内的对象:
// 简单的空间哈希网格
class SpatialHash {
private cellSize: number
private grid: Map<string, Array<GameObject>> = new Map()
constructor(cellSize: number) {
this.cellSize = cellSize
}
// 将对象插入网格
insert(obj: GameObject) {
const cellX = Math.floor(obj.x / this.cellSize)
const cellY = Math.floor(obj.y / this.cellSize)
const key = `${cellX},${cellY}`
if (!this.grid.has(key)) {
this.grid.set(key, [])
}
this.grid.get(key)!.push(obj)
}
// 查询可能碰撞的对象
query(obj: GameObject): Array<GameObject> {
const results: Array<GameObject> = []
const cellX = Math.floor(obj.x / this.cellSize)
const cellY = Math.floor(obj.y / this.cellSize)
// 检查周围9个格子
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const key = `${cellX + dx},${cellY + dy}`
const cell = this.grid.get(key)
if (cell) results.push(...cell)
}
}
return results
}
// 清空网格
clear() {
this.grid.clear()
}
}
坑点5:触摸事件的坐标转换
Canvas的触摸坐标是相对于Canvas组件的,但如果Canvas被缩放或偏移,直接使用触摸坐标会导致玩家飞机位置偏移。需要根据Canvas的实际显示区域做坐标映射。
坑点6:敌机销毁后的回调残留
敌机被子弹击中后,onCollision回调中引用了this,但如果敌机已经被对象池回收并重新分配给新的敌人,回调中的this指向的就是新敌人了。解决方案是在对象回收时清除所有回调:
reset() {
this.onCollision = () => {} // 清除回调
// ... 其他重置逻辑
}
坑点7:游戏循环的螺旋死亡问题
如果设备卡顿,一帧的deltaTime可能非常大(比如500ms),导致一帧内执行多次逻辑更新,进一步加重卡顿,形成恶性循环。务必对deltaTime设置上限(通常250ms),超过上限则截断。
五、HarmonyOS 6适配说明
API差异
| API | HarmonyOS 5.0 | HarmonyOS 6.0 | 迁移建议 |
|---|---|---|---|
| requestAnimationFrame | 基础帧回调 | 支持优先级参数 | 高优先级游戏使用high优先 |
| Canvas.drawImage | 基础图片绘制 | 支持ImageBitmap源 | 预渲染纹理使用ImageBitmap |
| CanvasRenderingContext2D | 标准2D API | 新增createImageBitmap | 异步创建位图,减少主线程阻塞 |
| taskpool | 基础任务池 | 支持任务依赖和优先级 | AI逻辑可放入低优先级任务 |
| TouchEvent | 基础触摸事件 | 新增工具类型和压感 | 支持手柄/笔输入 |
行为变更
- Canvas硬件加速默认开启:6.0中Canvas默认GPU渲染,2D游戏性能提升约30%,但自定义混合模式的渲染结果可能与5.0不同
- requestAnimationFrame精度提升:6.0支持与VSync同步的帧回调,帧率更稳定,但回调间隔不再是固定16.67ms,需要正确处理deltaTime
- 触摸事件批量处理:6.0对快速滑动产生的触摸事件做了批量合并,单次回调可能包含多个Move事件,需要遍历处理
适配代码
// HarmonyOS 6适配:VSync同步的游戏循环
private gameLoopVSync() {
if (!this.isRunning) return
const currentTime = Date.now()
const deltaTime = currentTime - this.lastTime
this.lastTime = currentTime
// HarmonyOS 6的requestAnimationFrame支持优先级
// 游戏场景使用'immediate'优先级确保及时回调
this.animFrameId = requestAnimationFrame(() => this.gameLoopVSync())
// 处理批量触摸事件(6.0新增)
this.processBatchedTouches()
// 固定步长更新
const clampedDelta = Math.min(deltaTime, 250)
this.accumulator += clampedDelta
while (this.accumulator >= this.fixedDeltaTime) {
this.fixedUpdate(this.fixedDeltaTime / 1000)
this.accumulator -= this.fixedDeltaTime
}
this.render(this.accumulator / this.fixedDeltaTime)
}
// 处理批量触摸事件
private processBatchedTouches() {
// 6.0中触摸事件可能批量到达
// 只取最后一个Move事件的坐标作为玩家目标位置
if (this.pendingTouches.length > 0) {
const lastTouch = this.pendingTouches[this.pendingTouches.length - 1]
this.touchX = lastTouch.x
this.touchY = lastTouch.y
this.pendingTouches = []
}
}
六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐⭐ |
2D游戏引擎的开发,本质上是在解决三个核心问题:时间管理、空间管理、资源管理。
时间管理——游戏循环和固定步长更新确保游戏速度一致;空间管理——碰撞检测和空间分区确保物理交互正确且高效;资源管理——对象池和纹理预渲染确保内存可控、帧率稳定。
记住一个黄金法则:永远不要在渲染循环中做任何可能阻塞的事情。无论是碰撞检测、AI逻辑还是资源加载,能异步就异步,能预计算就预计算。游戏开发中,60fps不是目标,而是底线——任何低于60fps的体验都会被玩家感知为"卡"。
最后,引擎架构不是一蹴而就的。先跑通最小闭环(游戏循环+精灵渲染),再逐步添加碰撞、动画、场景管理等子系统。每个子系统独立可测,这样出了问题你才能快速定位。别想着一步到位,游戏开发是一个不断迭代的过程。
- 点赞
- 收藏
- 关注作者
评论(0)