Skip to content

游戏设计模式

预计阅读 25 分钟
版本信息
最后更新:2026-07-09

设计模式是解决特定问题的可复用方案。游戏开发中的设计模式与 Web 开发有相似之处,但也有独特的需求——每帧更新、实时交互、内存敏感。本章系统讲解游戏中最重要的 7 种设计模式,每种都附带前端视角对照和实战代码。


为什么前端需要学游戏设计模式?

前端对比:Web 模式 vs 游戏模式
原则/模式Web 前端中的体现游戏中的体现
单一职责组件只做一件事Entity 只负责一种行为
开闭原则HOC / Composition组件组合优于继承
观察者EventEmitter / Vue watch成就系统、事件总线
状态机XState / reducer游戏流程控制(菜单/游戏中/暂停/结束)
对象池连接池(DB)、线程池子弹、粒子、敌人回收复用
享元CSS 雪碧图、字体子集化大量相同外观对象的共享渲染数据
命令Redux actions、Undo/Redo输入映射、动作录制回放

核心差异: Web 模式侧重于"数据 → 视图"的声明式映射;游戏模式侧重于"输入 → 模拟 → 渲染"的逐帧命令式更新。理解这种思维转变是学习游戏设计模式的关键。


1. 命令模式 (Command Pattern)

适用场景

  • 输入按键的可配置映射(玩家可自定义按键)
  • 撤销/重做系统(棋类、解谜游戏)
  • 动作录制与回放(竞速游戏 Ghost、回放系统)

前端类比

Redux actions、UndoManager、Macro recording

实现

typescript
// 命令接口
interface ICommand {
  execute(): void
  undo(): void
}

// 具体命令:移动
class MoveCommand implements ICommand {
  private previousPosition: { x: number; y: number }

  constructor(
    private entity: { x: number; y: number },
    private dx: number,
    private dy: number
  ) {
    this.previousPosition = { x: entity.x, y: entity.y }
  }

  execute(): void {
    this.entity.x += this.dx
    this.entity.y += this.dy
  }

  undo(): void {
    this.entity.x = this.previousPosition.x
    this.entity.y = this.previousPosition.y
  }
}

// 输入处理器:将按键映射到命令
class InputHandler {
  private keyBindings = new Map<string, () => ICommand>()

  bindKey(key: string, commandFactory: () => ICommand): void {
    this.keyBindings.set(key, commandFactory)
  }

  handleInput(key: string): ICommand | null {
    const factory = this.keyBindings.get(key)
    return factory ? factory() : null
  }
}

// 使用示例
const player = { x: 0, y: 0 }
const input = new InputHandler()

// 配置按键映射(玩家可自定义)
input.bindKey('w', () => new MoveCommand(player, 0, -10))
input.bindKey('s', () => new MoveCommand(player, 0, 10))
input.bindKey('a', () => new MoveCommand(player, -10, 0))
input.bindKey('d', () => new MoveCommand(player, 10, 0))

// 动作历史(用于回放系统)
class ActionRecorder {
  private history: ICommand[] = []

  record(command: ICommand): void {
    command.execute()
    this.history.push(command)
  }

  replay(): void {
    for (const cmd of this.history) {
      cmd.execute()
    }
  }
}

2. 对象池模式 (Object Pool Pattern)

适用场景

  • 频繁创建/销毁的对象(子弹、粒子、敌人、道具)
  • 减少 GC 压力,维持稳定帧率

前端类比

数据库连接池、线程池

实现

typescript
// 通用对象池
class ObjectPool<T> {
  private available: T[] = []
  private active: T[] = []

  constructor(
    private factory: () => T,
    private reset: (obj: T) => void,
    private initialSize: number = 10
  ) {
    // 预热:提前创建一批对象
    for (let i = 0; i < this.initialSize; i++) {
      this.available.push(this.factory())
    }
  }

  /** 从池中获取一个对象(池空时创建新的)*/
  acquire(): T {
    const obj = this.available.pop() ?? this.factory()
    this.active.push(obj)
    return obj
  }

  /** 将对象归还池中 */
  release(obj: T): void {
    const index = this.active.indexOf(obj)
    if (index !== -1) {
      this.active.splice(index, 1)
      this.reset(obj)
      this.available.push(obj)
    }
  }

  /** 批量归还 */
  releaseAll(): void {
    for (const obj of this.active) {
      this.reset(obj)
      this.available.push(obj)
    }
    this.active = []
  }

  /** 遍历所有活跃对象 */
  forEachActive(callback: (obj: T) => void): void {
    for (const obj of this.active) {
      callback(obj)
    }
  }

  get activeCount(): number {
    return this.active.length
  }
  get availableCount(): number {
    return this.available.length
  }
}

// 实战:子弹池
interface Bullet {
  x: number
  y: number
  vx: number
  vy: number
  active: boolean
  sprite: any // 精灵引用
}

const bulletPool = new ObjectPool<Bullet>(
  // 工厂:创建一个哑子弹对象
  () => ({ x: 0, y: 0, vx: 0, vy: 0, active: false, sprite: null }),
  // 重置:归还前清理状态
  (b) => {
    b.x = 0
    b.y = 0
    b.vx = 0
    b.vy = 0
    b.active = false
  },
  50 // 预创建 50 个
)

// 发射子弹
function fireBullet(fromX: number, fromY: number, angle: number): void {
  const bullet = bulletPool.acquire()
  bullet.x = fromX
  bullet.y = fromY
  bullet.vx = Math.cos(angle) * 500
  bullet.vy = Math.sin(angle) * 500
  bullet.active = true
}

// 每帧回收飞出屏幕的子弹
function updateBullets(dt: number, screenWidth: number): void {
  const toRelease: Bullet[] = []

  bulletPool.forEachActive((bullet) => {
    bullet.x += bullet.vx * dt
    bullet.y += bullet.vy * dt

    if (bullet.x < 0 || bullet.x > screenWidth) {
      toRelease.push(bullet)
    }
  })

  for (const bullet of toRelease) {
    bulletPool.release(bullet)
  }
}
对象池使用建议
  1. 预热池大小 = 最大同时出现数 × 1.2 — 预创建足够对象,避免运行时创建的开销
  2. 周期性收缩 — 长时间无大量对象时释放多余池容量,回收内存
  3. 不要池化引擎内置对象 — Cocos 的 Node、Unity 的 GameObject 由引擎自身管理生命周期
  4. 仅池化数据对象 — 子弹位置/速度等数据结构适合池化,UI 元素不适合

3. 观察者模式 (Observer Pattern)

适用场景

  • 成就系统("击杀 100 个敌人" → 解锁成就)
  • UI 跨模块更新(分数变化 → 更新 HUD)
  • 游戏事件总线("玩家死亡" → 触发多个响应)

前端类比

EventEmitter、Vue watch、RxJS Subject

实现

typescript
// 类型安全的事件总线
type EventMap = {
  'player:score': { score: number; combo: number }
  'player:death': { position: { x: number; y: number }; killer: string }
  'enemy:killed': { type: string; position: { x: number; y: number } }
  'achievement:unlocked': { id: string; name: string }
  'game:pause': void
  'game:resume': void
}

class GameEventBus {
  private listeners = new Map<string, Set<Function>>()

  on<K extends keyof EventMap>(event: K, callback: (data: EventMap[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set())
    }
    this.listeners.get(event)!.add(callback)
  }

  off<K extends keyof EventMap>(event: K, callback: (data: EventMap[K]) => void): void {
    this.listeners.get(event)?.delete(callback)
  }

  emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
    this.listeners.get(event)?.forEach((cb) => cb(data))
  }
}

// 全局事件总线单例
const Events = new GameEventBus()

// --- 使用示例 ---

// 成就系统订阅击杀事件
let killCount = 0
Events.on('enemy:killed', (data) => {
  killCount++
  if (killCount >= 100) {
    Events.emit('achievement:unlocked', {
      id: 'kill_100',
      name: '百人斩',
    })
  }
})

// UI 层订阅分数变化
Events.on('player:score', ({ score, combo }) => {
  updateScoreDisplay(score)
  if (combo > 5) {
    showComboEffect(combo)
  }
})

// 游戏统计订阅多个事件
class GameAnalytics {
  init(): void {
    Events.on('player:death', (data) => {
      this.track('player_death', {
        position: data.position,
        timestamp: Date.now(),
      })
    })
    Events.on('achievement:unlocked', (data) => {
      this.track('achievement', { id: data.id, timestamp: Date.now() })
    })
  }
}

4. 状态模式 (State Pattern)

适用场景

  • 游戏流程管理(菜单 → 游戏中 → 暂停 → 结算)
  • 角色行为 AI(巡逻 → 追击 → 攻击 → 逃跑)
  • UI 界面切换

前端类比

XState、useReducer

实现

typescript
// 状态接口
interface IGameState {
  enter(): void
  exit(): void
  update(dt: number): void
  handleInput(action: string): void
}

// 具体状态:游戏中
class PlayingState implements IGameState {
  constructor(private context: GameContext) {}

  enter(): void {
    console.log('游戏开始')
    this.context.startGameLoop()
  }

  exit(): void {
    console.log('退出游戏状态')
  }

  update(dt: number): void {
    this.context.updateEntities(dt)
    this.context.checkCollisions()
    this.context.updateScore()
  }

  handleInput(action: string): void {
    switch (action) {
      case 'pause':
        this.context.setState(new PausedState(this.context))
        break
      case 'flap':
        this.context.player.flap()
        break
    }
  }
}

// 具体状态:暂停
class PausedState implements IGameState {
  enter(): void {
    this.context.pauseGameLoop()
    this.context.showPauseMenu()
  }

  exit(): void {
    this.context.hidePauseMenu()
  }

  update(): void {
    /* 暂停时不做更新 */
  }

  handleInput(action: string): void {
    switch (action) {
      case 'resume':
        this.context.setState(new PlayingState(this.context))
        break
      case 'quit':
        this.context.setState(new MenuState(this.context))
        break
    }
  }
}

// 具体状态:菜单
class MenuState implements IGameState {
  enter(): void {
    this.context.showMainMenu()
  }

  exit(): void {
    this.context.hideMainMenu()
  }

  update(): void {
    /* 菜单不更新游戏逻辑 */
  }

  handleInput(action: string): void {
    if (action === 'start') {
      this.context.setState(new PlayingState(this.context))
    }
  }
}

// 状态上下文
class GameContext {
  private currentState: IGameState

  constructor() {
    this.currentState = new MenuState(this)
    this.currentState.enter()
  }

  setState(newState: IGameState): void {
    this.currentState.exit()
    this.currentState = newState
    this.currentState.enter()
  }

  update(dt: number) {
    this.currentState.update(dt)
  }
  handleInput(action: string) {
    this.currentState.handleInput(action)
  }

  // 以下方法由具体状态调用
  player: any // 玩家引用
  startGameLoop() {
    /* ... */
  }
  pauseGameLoop() {
    /* ... */
  }
  updateEntities(dt: number) {
    /* ... */
  }
  checkCollisions() {
    /* ... */
  }
  updateScore() {
    /* ... */
  }
  showPauseMenu() {
    /* ... */
  }
  hidePauseMenu() {
    /* ... */
  }
  showMainMenu() {
    /* ... */
  }
  hideMainMenu() {
    /* ... */
  }
}

// 并行状态机(同时管理多个独立状态维度)
class ParallelStateMachine {
  private states = new Map<string, IGameState>()

  // 例如:游戏流程状态 + 网络状态 + 音频状态同时独立运行
  addLayer(name: string, state: IGameState): void {
    this.states.set(name, state)
    state.enter()
  }

  updateAll(dt: number): void {
    this.states.forEach((state) => state.update(dt))
  }

  setLayerState(name: string, newState: IGameState): void {
    const old = this.states.get(name)
    old?.exit()
    this.states.set(name, newState)
    newState.enter()
  }
}

5. 组件模式 (Component Pattern / ECS)

适用场景

  • 避免深层继承("Entity → MovableEntity → FlyingEntity → Bird → AngryBird")
  • 灵活组合行为("可移动 + 可碰撞 + 可渲染 + 有血量" 自由组合)
  • 大型项目中的实体管理

前端类比

React 的 Composition over Inheritance、Vue 3 Composables

实现

typescript
// 轻量级 ECS(Entity-Component-System)实现

// 组件:纯数据容器
interface PositionComponent {
  x: number
  y: number
}
interface VelocityComponent {
  vx: number
  vy: number
}
interface HealthComponent {
  current: number
  max: number
}
interface RenderComponent {
  sprite: string
  visible: boolean
}
interface PlayerInputComponent {
  enabled: boolean
}

// 实体:组件集合
class Entity {
  id: number
  private components = new Map<string, any>()

  constructor(id: number) {
    this.id = id
  }

  add<T>(name: string, component: T): this {
    this.components.set(name, component)
    return this
  }

  get<T>(name: string): T | undefined {
    return this.components.get(name)
  }

  has(name: string): boolean {
    return this.components.has(name)
  }

  remove(name: string): void {
    this.components.delete(name)
  }
}

// 系统:处理拥有特定组件组合的实体
class MovementSystem {
  update(entities: Entity[], dt: number): void {
    for (const entity of entities) {
      const pos = entity.get<PositionComponent>('position')
      const vel = entity.get<VelocityComponent>('velocity')
      if (!pos || !vel) continue // 实体没有移动相关组件,跳过

      pos.x += vel.vx * dt
      pos.y += vel.vy * dt
    }
  }
}

class RenderSystem {
  update(entities: Entity[], ctx: CanvasRenderingContext2D): void {
    for (const entity of entities) {
      const pos = entity.get<PositionComponent>('position')
      const render = entity.get<RenderComponent>('render')
      if (!pos || !render || !render.visible) continue

      // 使用精灵渲染实体
      ctx.fillStyle = 'red'
      ctx.fillRect(pos.x, pos.y, 32, 32)
    }
  }
}

class HealthSystem {
  update(entities: Entity[]): void {
    for (const entity of entities) {
      const health = entity.get<HealthComponent>('health')
      if (!health) continue

      if (health.current <= 0) {
        // 触发死亡逻辑
        entity.remove('health')
      }
    }
  }
}

// --- 使用示例:组装不同实体 ---
const world = new EntityManager()

// 玩家:可移动 + 有血量 + 可渲染 + 可输入
const player = world
  .createEntity()
  .add('position', { x: 100, y: 200 })
  .add('velocity', { vx: 0, vy: 0 })
  .add('health', { current: 100, max: 100 })
  .add('render', { sprite: 'player.png', visible: true })
  .add('playerInput', { enabled: true })

// 子弹:可移动 + 可渲染,没有血量
const bullet = world
  .createEntity()
  .add('position', { x: 100, y: 200 })
  .add('velocity', { vx: 500, vy: 0 })
  .add('render', { sprite: 'bullet.png', visible: true })
// 子弹没有 HealthComponent,碰撞系统不会检查它的血量

// 静态障碍物:只有位置和渲染,没有速度
const obstacle = world
  .createEntity()
  .add('position', { x: 300, y: 200 })
  .add('render', { sprite: 'rock.png', visible: true })
// 障碍物没有 VelocityComponent,移动系统会自动跳过它

// 每帧更新所有系统
function gameLoop(dt: number): void {
  world.systems.movement.update(world.entities, dt)
  world.systems.collision.update(world.entities)
  world.systems.health.update(world.entities)
  world.systems.render.update(world.entities, ctx)
}
ECS 的代价

ECS 提供无与伦比的灵活性,但有以下代价:

  • 缓存不友好 — 组件散落在内存各处,CPU 缓存命中率低(真正的 ECS 通过 Archetype 解决)
  • 调试复杂 — 错误追踪比继承体系困难("这个实体为什么在动?"需要检查多个系统)
  • 学习成本高 — 团队需要理解 ECS 的思维模式

建议: 小游戏(< 1000 个实体)不需要完整 ECS。组件模式的核心思想("组合优于继承")用简单的 Entity.components Map 就可以体现。


6. 享元模式 (Flyweight Pattern)

适用场景

  • 大量相同外观的对象(森林中的树木、弹幕中的子弹)
  • 共享不变的属性(纹理、网格数据、字体)

前端类比

CSS Sprite、字体子集化、React.memo

实现

typescript
// 享元:共享的固有状态
class TreeType {
  constructor(
    public name: string,
    public texture: string, // 所有同类型树共享的纹理
    public color: string,
    public height: number
  ) {}
}

// 享元工厂
class TreeTypeFactory {
  private types = new Map<string, TreeType>()

  getTreeType(name: string, texture: string, color: string, height: number): TreeType {
    const key = `${name}_${texture}_${color}_${height}`
    if (!this.types.has(key)) {
      this.types.set(key, new TreeType(name, texture, color, height))
    }
    return this.types.get(key)!
  }
}

// 具体实例:仅保存变化的状态
class Tree {
  constructor(
    public type: TreeType, // 共享的享元
    public x: number,
    public y: number,
    public scale: number // 每棵树独立的变化
  ) {}

  draw(ctx: CanvasRenderingContext2D): void {
    // 使用共享的 TreeType 纹理渲染
    ctx.fillStyle = this.type.color
    ctx.fillRect(this.x, this.y, 20 * this.scale, this.type.height * this.scale)
  }
}

// 使用:创建一片森林
const factory = new TreeTypeFactory()
const forest: Tree[] = []

// 只有 3 种树类型,但可以生成 10000 棵树
const pine = factory.getTreeType('pine', 'pine.png', '#2d5a27', 100)
const oak = factory.getTreeType('oak', 'oak.png', '#3d7a37', 120)
const cherry = factory.getTreeType('cherry', 'cherry.png', '#ff9999', 80)

for (let i = 0; i < 10000; i++) {
  const type = [pine, oak, cherry][Math.floor(Math.random() * 3)]
  forest.push(new Tree(type, Math.random() * 2000, Math.random() * 1000, 0.5 + Math.random() * 1.5))
}

// 内存对比:
// 没有享元:10000 棵树 × (名字 + 纹理引用 + 颜色 + 高度) ≈ 大量重复
// 使用享元:3 个 TreeType + 10000 个 (引用 + x + y + scale) ≈ 大幅减少

7. 游戏循环模式 (Game Loop Pattern)

适用场景

  • 所有需要帧更新的游戏(几乎全部)
  • 不同的时间策略选择

前端类比

requestAnimationFrame 动画循环

实现:三种循环策略对比

typescript
// 策略 A:可变时间步长(最简单,适合休闲游戏)
class VariableTimestepLoop {
  private lastTime = 0
  private running = false

  start(update: (dt: number) => void): void {
    this.running = true
    this.lastTime = performance.now()

    const loop = (currentTime: number) => {
      if (!this.running) return
      const dt = (currentTime - this.lastTime) / 1000 // 转换为秒
      this.lastTime = currentTime

      update(dt) // dt 可能波动,物理模拟不稳定

      requestAnimationFrame(loop)
    }
    requestAnimationFrame(loop)
  }
}

// 策略 B:固定时间步长(适合物理模拟、确定性重放)
class FixedTimestepLoop {
  private readonly FIXED_DT = 1 / 60 // 固定 16.67ms
  private accumulator = 0
  private lastTime = 0
  private running = false

  start(update: (dt: number) => void, render: (alpha: number) => void): void {
    this.running = true
    this.lastTime = performance.now()

    const loop = (currentTime: number) => {
      if (!this.running) return
      const frameTime = (currentTime - this.lastTime) / 1000
      this.lastTime = currentTime

      // 防止螺旋式死亡(长时间暂停后时间累积过多)
      // 超过 250ms 的帧按 250ms 处理,避免一次性追赶过多物理步
      const clampedFrameTime = Math.min(frameTime, 0.25)
      this.accumulator += clampedFrameTime

      // 消耗累积时间,以固定步长更新
      while (this.accumulator >= this.FIXED_DT) {
        update(this.FIXED_DT) // 物理/逻辑始终按固定步长
        this.accumulator -= this.FIXED_DT
      }

      // 用剩余比例做插值渲染(避免视觉抖动)
      const alpha = this.accumulator / this.FIXED_DT
      render(alpha)

      requestAnimationFrame(loop)
    }
    requestAnimationFrame(loop)
  }
}

// 策略 C:解耦逻辑帧率与渲染帧率
class DecoupledLoop {
  private readonly LOGIC_FPS = 30 // 逻辑更新 30fps
  private readonly RENDER_FPS = 60 // 渲染 60fps
  private logicTimer = 0
  private renderTimer = 0
  private lastTime = 0
  private running = false

  start(update: (dt: number) => void, render: (interpolation: number) => void): void {
    const LOGIC_INTERVAL = 1000 / this.LOGIC_FPS // 33.3ms
    const RENDER_INTERVAL = 1000 / this.RENDER_FPS // 16.7ms

    this.running = true
    this.lastTime = performance.now()

    const loop = (currentTime: number) => {
      if (!this.running) return
      const elapsed = currentTime - this.lastTime
      this.lastTime = currentTime

      this.logicTimer += elapsed
      this.renderTimer += elapsed

      // 逻辑更新 30fps
      while (this.logicTimer >= LOGIC_INTERVAL) {
        update(LOGIC_INTERVAL / 1000)
        this.logicTimer -= LOGIC_INTERVAL
      }

      // 渲染 60fps,用剩余时间做插值
      if (this.renderTimer >= RENDER_INTERVAL) {
        const interpolation = this.logicTimer / LOGIC_INTERVAL
        render(interpolation)
        this.renderTimer -= RENDER_INTERVAL
      }

      // 如果两者都不需要更新,用 setTimeout 降低 CPU 占用
      const nextUpdate = Math.min(
        LOGIC_INTERVAL - this.logicTimer,
        RENDER_INTERVAL - this.renderTimer
      )
      setTimeout(() => requestAnimationFrame(loop), Math.max(0, nextUpdate))
    }
    requestAnimationFrame(loop)
  }
}
游戏循环模式选择指南
策略适用场景优点缺点
可变步长休闲游戏、UI 动画实现简单物理不稳定、帧率波动大
固定步长物理游戏、确定性回放物理稳定、可复现可能出现"追赶螺旋"
解耦循环性能要求高的游戏逻辑稳定 + 渲染流畅实现复杂、插值引入延迟
推荐微信小游戏固定步长(帧率不高时最稳定)

模式组合实战

真实游戏项目中很少单独使用一种模式。以下是一个典型组合示例:

游戏循环(Game Loop)
├── 状态机(State Pattern)— 管理游戏流程
│   ├── MenuState
│   ├── PlayingState
│   │   ├── 对象池(Object Pool)— 管理子弹/粒子
│   │   ├── 组件系统(Component)— 管理实体行为
│   │   └── 事件总线(Observer)— 连接各模块
│   └── GameOverState
└── 命令模式(Command)— 处理输入和回放
typescript
// 组合示例:使用固定游戏循环 + 状态机 + 事件总线 + 对象池
class Game {
  private loop: FixedTimestepLoop
  private stateMachine: GameContext
  private events = new GameEventBus()
  private bulletPool = new ObjectPool<Bullet>(/* ... */)

  constructor() {
    // 事件订阅
    this.events.on('player:death', () => {
      this.stateMachine.setState(new GameOverState(this.stateMachine))
    })

    this.events.on('bullet:fired', () => {
      // ...
    })

    // 启动固定步长循环
    this.loop = new FixedTimestepLoop()
    this.loop.start(
      (dt) => this.stateMachine.update(dt), // 逻辑更新
      (alpha) => this.render(alpha) // 渲染插值
    )
  }
}

📚 相关阅读


📝 课后练习

练习 1:使用状态模式实现完整的游戏流程管理器

题目: 基于状态模式实现一个包含 4 个状态的游戏流程管理器,要求:

  1. 四种状态:菜单(Menu)、游戏中(Playing)、暂停(Paused)、结算(GameOver)
  2. 菜单状态:显示开始按钮,点击后过渡到"游戏中"
  3. 游戏中状态:运行游戏循环,支持暂停按钮和死亡事件
  4. 暂停状态:停止游戏循环,显示继续/返回菜单按钮
  5. 结算状态:显示分数和重新开始按钮,3 秒后才能点击
  6. 每个状态有独立的进场/退场动画逻辑(至少模拟 console.log 标识)
  7. 非法状态转换应被静默忽略并记录警告

参考答案:

typescript
// 状态接口
interface IGameState {
  name: string
  onEnter(prevState: string): void
  onExit(nextState: string): void
  onUpdate(dt: number): void
  onButtonClick(button: string): void
}

// 状态管理器
class GameStateManager {
  private states = new Map<string, IGameState>()
  private currentState: IGameState
  private transitionLocked = false // 防止动画期间状态切换

  constructor() {
    this.currentState = this.createNullState()
  }

  register(state: IGameState): void {
    this.states.set(state.name, state)
  }

  transition(stateName: string): boolean {
    if (this.transitionLocked) {
      console.warn(`[State] 状态转换被锁定,忽略转到 "${stateName}" 的请求`)
      return false
    }

    const nextState = this.states.get(stateName)
    if (!nextState) {
      console.warn(`[State] 未知状态: "${stateName}"`)
      return false
    }

    const prevName = this.currentState.name
    this.currentState.onExit(stateName)
    this.currentState = nextState
    this.currentState.onEnter(prevName)

    console.log(`[State] ${prevName} → ${stateName}`)
    return true
  }

  update(dt: number): void {
    this.currentState.onUpdate(dt)
  }

  handleButton(button: string): void {
    this.currentState.onButtonClick(button)
  }

  lockTransition(): void {
    this.transitionLocked = true
  }
  unlockTransition(): void {
    this.transitionLocked = false
  }

  private createNullState(): IGameState {
    return {
      name: 'null',
      onEnter: () => {},
      onExit: () => {},
      onUpdate: () => {},
      onButtonClick: () => {},
    }
  }
}

// ===== 具体状态实现 =====

class MenuState implements IGameState {
  name = 'menu'

  constructor(private sm: GameStateManager) {}

  onEnter(prev: string): void {
    console.log('🎬 进入菜单 — 播放标题入场动画')
    // titleNode.runAction(fadeIn(0.5))
    // startButton.runAction(scaleTo(0.3, 1.0))
  }

  onExit(next: string): void {
    console.log('🎬 退出菜单 — 播放淡出动画')
    // 锁定 0.5 秒,动画结束后再解锁
    this.sm.lockTransition()
    setTimeout(() => this.sm.unlockTransition(), 500)
  }

  onUpdate(_dt: number): void {
    // 菜单背景动画(如星空移动)
  }

  onButtonClick(button: string): void {
    if (button === 'start') {
      this.sm.transition('playing')
    }
  }
}

class PlayingState implements IGameState {
  name = 'playing'
  private score = 0
  private elapsed = 0

  constructor(private sm: GameStateManager) {}

  onEnter(_prev: string): void {
    console.log('🎮 进入游戏 — 开始游戏循环')
    this.score = 0
    this.elapsed = 0
    // 显示 HUD
  }

  onExit(_next: string): void {
    console.log('🎮 退出游戏')
  }

  onUpdate(dt: number): void {
    this.elapsed += dt
    // 运行游戏逻辑
    // updateEntities(dt)
    // checkCollisions()
  }

  onButtonClick(button: string): void {
    switch (button) {
      case 'pause':
        this.sm.transition('paused')
        break
      case 'die':
        // 将分数传递给结算状态
        this.sm.transition('gameover')
        break
    }
  }
}

class PausedState implements IGameState {
  name = 'paused'

  constructor(private sm: GameStateManager) {}

  onEnter(_prev: string): void {
    console.log('⏸️ 暂停 — 停止游戏循环,显示暂停菜单')
    // 显示半透明遮罩 + 暂停菜单
  }

  onExit(_next: string): void {
    console.log('⏸️ 取消暂停')
    // 隐藏暂停菜单
  }

  onUpdate(_dt: number): void {
    // 暂停时不更新游戏逻辑
    // 但可以更新 UI 动画
  }

  onButtonClick(button: string): void {
    switch (button) {
      case 'resume':
        this.sm.transition('playing')
        break
      case 'quit':
        this.sm.transition('menu')
        break
    }
  }
}

class GameOverState implements IGameState {
  name = 'gameover'
  private canRestart = false
  private coolDownTimer: number | null = null

  constructor(private sm: GameStateManager) {}

  onEnter(_prev: string): void {
    console.log('💀 游戏结束 — 显示结算动画')
    this.canRestart = false

    // 3 秒冷却期
    setTimeout(() => {
      this.canRestart = true
      console.log('✅ 重新开始按钮已激活')
    }, 3000)
  }

  onExit(_next: string): void {
    if (this.coolDownTimer) clearTimeout(this.coolDownTimer)
  }

  onUpdate(_dt: number): void {
    // 结算动画
  }

  onButtonClick(button: string): void {
    if (button === 'restart' && this.canRestart) {
      this.sm.transition('playing')
    } else if (button === 'menu' && this.canRestart) {
      this.sm.transition('menu')
    } else if (!this.canRestart) {
      console.log('⏳ 请等待冷却时间结束')
    }
  }
}

// ===== 使用示例 =====
const sm = new GameStateManager()
sm.register(new MenuState(sm))
sm.register(new PlayingState(sm))
sm.register(new PausedState(sm))
sm.register(new GameOverState(sm))

sm.transition('menu') // ✅ null → menu

// 模拟游戏流程
sm.handleButton('start') // ✅ menu → playing
sm.handleButton('pause') // ✅ playing → paused
sm.handleButton('resume') // ✅ paused → playing
sm.handleButton('die') // ✅ playing → gameover

// 非法操作测试
sm.handleButton('resume') // ❌ 结算状态不接受 resume
练习 2:使用对象池管理子弹,测量 GC 差异

题目: 实现一个子弹对象池,并编写对比测试量化对象池对 GC 的影响:

  1. 实现一个通用对象池(支持预创建、获取、回收、收缩)
  2. 分别用「直接 new」和「对象池」方式创建/销毁 10000 个子弹
  3. 测量两种方式下的:
    • 总耗时(创建 + 销毁)
    • 内存分配量(可通过 performance.memory 估算)
    • 创建过程中是否触发了 GC 暂停
  4. 分析结果并说明什么场景下对象池收益最大

参考答案:

typescript
// BulletPool.ts
interface Bullet {
  x: number
  y: number
  vx: number
  vy: number
  damage: number
  active: boolean
  lifeTime: number
}

class BulletPool {
  private pool: Bullet[] = []
  private activeBullets: Bullet[] = []

  constructor(private initialSize: number = 100) {
    this.warmUp(initialSize)
  }

  /** 预创建子弹 */
  private warmUp(count: number): void {
    for (let i = 0; i < count; i++) {
      this.pool.push(this.createBullet())
    }
  }

  private createBullet(): Bullet {
    return { x: 0, y: 0, vx: 0, vy: 0, damage: 10, active: false, lifeTime: 0 }
  }

  /** 获取子弹 */
  acquire(x: number, y: number, vx: number, vy: number): Bullet {
    const bullet = this.pool.pop() ?? this.createBullet()
    bullet.x = x
    bullet.y = y
    bullet.vx = vx
    bullet.vy = vy
    bullet.active = true
    bullet.lifeTime = 0
    this.activeBullets.push(bullet)
    return bullet
  }

  /** 回收子弹 */
  release(bullet: Bullet): void {
    const index = this.activeBullets.indexOf(bullet)
    if (index !== -1) {
      this.activeBullets.splice(index, 1)
      bullet.active = false
      bullet.x = 0
      bullet.y = 0
      bullet.vx = 0
      bullet.vy = 0
      bullet.lifeTime = 0
      this.pool.push(bullet)
    }
  }

  /** 回收所有超出最大活跃数的子弹(收缩) */
  shrink(maxActive: number): void {
    if (this.pool.length <= maxActive) return
    const toRemove = this.pool.length - maxActive
    this.pool.splice(0, toRemove) // 释放多余的池对象
  }

  get activeCount(): number {
    return this.activeBullets.length
  }
  get pooledCount(): number {
    return this.pool.length
  }
}

// ===== 性能对比测试 =====

function measurePerformance(
  label: string,
  iterations: number,
  fn: () => void
): { time: number; memoryDelta: number } {
  const memBefore = (performance as any).memory?.usedJSHeapSize ?? 0
  const start = performance.now()

  for (let i = 0; i < iterations; i++) {
    fn()
  }

  const time = performance.now() - start
  const memAfter = (performance as any).memory?.usedJSHeapSize ?? 0

  console.log(
    `[${label}] ${iterations} 次 — ` +
      `耗时: ${time.toFixed(1)}ms | ` +
      `内存变化: ${((memAfter - memBefore) / 1024 / 1024).toFixed(2)}MB`
  )

  return { time, memoryDelta: memAfter - memBefore }
}

// 测试 1:直接 new(无对象池)
function testWithoutPool(): void {
  const bullets: Bullet[] = []

  // 模拟一波射击:100 个子弹
  for (let i = 0; i < 100; i++) {
    bullets.push({
      x: Math.random() * 500,
      y: Math.random() * 800,
      vx: Math.random() * 10,
      vy: Math.random() * 10,
      damage: 10,
      active: true,
      lifeTime: 0,
    })
  }

  // 模拟生命周期结束,释放引用
  bullets.length = 0 // 这些对象成为 GC 垃圾
}

// 测试 2:使用对象池
const pool = new BulletPool(120)

function testWithPool(): void {
  const active: Bullet[] = []

  // 模拟一波射击
  for (let i = 0; i < 100; i++) {
    const bullet = pool.acquire(
      Math.random() * 500,
      Math.random() * 800,
      Math.random() * 10,
      Math.random() * 10
    )
    active.push(bullet)
  }

  // 模拟生命周期结束,回收到池
  for (const bullet of active) {
    pool.release(bullet)
  }
  active.length = 0
}

// ===== 运行对比测试 =====
console.log('========== 对象池 vs 直接创建 GC 影响对比 ==========')

const ITERATIONS = 100
console.log(`\n每组执行 ${ITERATIONS} 波(每波 100 个子弹)`)
console.log(`总计创建/回收: ${ITERATIONS * 100} 次\n`)

// 预热(让 JIT 优化)
for (let i = 0; i < 10; i++) {
  testWithoutPool()
  testWithPool()
}

// 正式测试
const withoutResult = measurePerformance('❌ 无对象池', ITERATIONS, testWithoutPool)
const withResult = measurePerformance('✅ 对象池', ITERATIONS, testWithPool)

// 对比分析
const timeSaving = (((withoutResult.time - withResult.time) / withoutResult.time) * 100).toFixed(1)
console.log(`\n📊 对比结果:`)
console.log(`   对象池速度提升: ${timeSaving}%`)
console.log(
  `   对象池内存节省: ${((withoutResult.memoryDelta - withResult.memoryDelta) / 1024 / 1024).toFixed(2)}MB`
)

// 输出:
// ❌ 无对象池: 100 波 — 耗时: ~45ms | 内存变化: ~2.5MB(创建 + GC 回收)
// ✅ 对象池:   100 波 — 耗时: ~8ms  | 内存变化: ~0.1MB(无新对象,无 GC)

分析总结:

维度无对象池对象池差距
对象创建次数10000120(预热)83x
GC 压力高(10000 个临时对象触发多次 GC)极低(池对象常驻)
帧率稳定性射击时可能掉帧(GC 暂停)稳定
适用场景低频操作(菜单、关卡切换)高频操作(子弹、粒子、敌人)

对象池收益最大场景:

  1. 射击/弹幕游戏 — 子弹创建频率可达每秒数百个
  2. 粒子特效 — 单个爆炸可能涉及数百粒子
  3. 塔防/割草 — 敌人/子弹频繁生成和销毁

对象池不适用场景:

  1. 低频操作(开局加载、UI 弹窗)
  2. 受引擎管理的对象(Cocos Node、Unity GameObject — 引擎已有自己的池机制)
  3. 需要初始化逻辑复杂的对象(状态差异大,重置成本高)

用心学习,持续实践