HarmonyOS游戏开发:2D游戏引擎架构与Canvas游戏开发

举报
Jack20 发表于 2026/06/23 19:55:22 2026/06/23
【摘要】 HarmonyOS游戏开发:2D游戏引擎架构与Canvas游戏开发📌 核心要点:从游戏循环到精灵系统,从碰撞检测到场景管理,构建一个可运行的2D游戏引擎并实战飞机大战 一、背景与动机你有没有想过,为什么有些手游运行丝般顺滑,而有些却卡得像PPT?为什么同样的游戏逻辑,有人写出来结构清晰、扩展方便,有人写出来却是一坨"意大利面条"代码?答案就藏在游戏引擎架构里。游戏引擎不是一个神秘的黑盒子...

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的体验都会被玩家感知为"卡"。

最后,引擎架构不是一蹴而就的。先跑通最小闭环(游戏循环+精灵渲染),再逐步添加碰撞、动画、场景管理等子系统。每个子系统独立可测,这样出了问题你才能快速定位。别想着一步到位,游戏开发是一个不断迭代的过程。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。