主题切换
游戏设计模式
预计阅读 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.2 — 预创建足够对象,避免运行时创建的开销
- 周期性收缩 — 长时间无大量对象时释放多余池容量,回收内存
- 不要池化引擎内置对象 — Cocos 的 Node、Unity 的 GameObject 由引擎自身管理生命周期
- 仅池化数据对象 — 子弹位置/速度等数据结构适合池化,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) // 渲染插值
)
}
}📚 相关阅读
- 03 游戏开发核心概念 — 游戏循环和状态机的基础理论
- 08 性能优化与包体控制 — 对象池在性能优化中的实战应用
- 06 引擎深度实战 — 引擎中设计模式的体现
- 游戏测试专题 — 如何测试这些设计模式实现
📝 课后练习
练习 1:使用状态模式实现完整的游戏流程管理器
题目: 基于状态模式实现一个包含 4 个状态的游戏流程管理器,要求:
- 四种状态:菜单(Menu)、游戏中(Playing)、暂停(Paused)、结算(GameOver)
- 菜单状态:显示开始按钮,点击后过渡到"游戏中"
- 游戏中状态:运行游戏循环,支持暂停按钮和死亡事件
- 暂停状态:停止游戏循环,显示继续/返回菜单按钮
- 结算状态:显示分数和重新开始按钮,3 秒后才能点击
- 每个状态有独立的进场/退场动画逻辑(至少模拟 console.log 标识)
- 非法状态转换应被静默忽略并记录警告
参考答案:
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 的影响:
- 实现一个通用对象池(支持预创建、获取、回收、收缩)
- 分别用「直接 new」和「对象池」方式创建/销毁 10000 个子弹
- 测量两种方式下的:
- 总耗时(创建 + 销毁)
- 内存分配量(可通过
performance.memory估算) - 创建过程中是否触发了 GC 暂停
- 分析结果并说明什么场景下对象池收益最大
参考答案:
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)分析总结:
| 维度 | 无对象池 | 对象池 | 差距 |
|---|---|---|---|
| 对象创建次数 | 10000 | 120(预热) | 83x |
| GC 压力 | 高(10000 个临时对象触发多次 GC) | 极低(池对象常驻) | — |
| 帧率稳定性 | 射击时可能掉帧(GC 暂停) | 稳定 | — |
| 适用场景 | 低频操作(菜单、关卡切换) | 高频操作(子弹、粒子、敌人) | — |
对象池收益最大场景:
- 射击/弹幕游戏 — 子弹创建频率可达每秒数百个
- 粒子特效 — 单个爆炸可能涉及数百粒子
- 塔防/割草 — 敌人/子弹频繁生成和销毁
对象池不适用场景:
- 低频操作(开局加载、UI 弹窗)
- 受引擎管理的对象(Cocos Node、Unity GameObject — 引擎已有自己的池机制)
- 需要初始化逻辑复杂的对象(状态差异大,重置成本高)