游戏设计模式与架构
概述
游戏项目从小变大时,代码组织方式会成为成败关键。本文包含两大部分:设计模式解决"如何写得可维护",架构范式解决"如何组织才可扩展"。我们会先快速回顾游戏开发五大核心设计模式(详细代码见 游戏设计模式),然后重点深入 ECS 与 OOP 的架构对比,并通过一个可运行示例演示两者差异。
游戏开发五大设计模式
单例模式(Singleton)
确保一个类全局只有一个实例,提供全局访问点。适用于管理器类:GameManager、AudioManager、InputManager。
js
class GameManager {
static instance = null
constructor() {
if (GameManager.instance) return GameManager.instance
this.score = 0
this.state = 'menu'
GameManager.instance = this
}
}
const gm = new GameManager() // 无论 new 几次,始终同一个实例注意:单例本质是全局状态,滥用会导致隐性依赖、难以测试。小项目适用,大项目优先依赖注入。
对象池模式(Object Pool)
复用创建成本高的对象,避免频繁 new/GC。适用于子弹、粒子、敌人等高频创建销毁的场景:
js
class ObjectPool {
constructor(factory, initialSize) {
this.factory = factory
this.pool = []
this.active = []
for (let i = 0; i < initialSize; i++) this.pool.push(factory())
}
acquire() {
const obj = this.pool.pop() || this.factory()
this.active.push(obj)
return obj
}
release(obj) {
this.active.splice(this.active.indexOf(obj), 1)
this.pool.push(obj)
}
}状态机模式(State / FSM)
把对象的复杂行为拆解为有限状态 + 转移规则。适用于角色 AI、动画切换、UI 流程:
js
class FSM {
constructor() {
this.states = {}
this.current = null
}
addState(name, { enter, update, exit }) {
this.states[name] = { enter, update, exit }
}
change(name) {
if (this.current) this.states[this.current].exit?.()
this.current = name
this.states[name].enter?.()
}
update(dt) {
this.states[this.current]?.update?.(dt)
}
}
// 用法:角色的 Idle/Run/Jump 三个状态
const fsm = new FSM()
fsm.addState('Idle', { update: () => { if (keys.right) fsm.change('Run') } })
fsm.addState('Run', { update: () => { if (keys.jump) fsm.change('Jump') } })
fsm.addState('Jump', { update: () => { /* 跳跃物理 */ } })观察者模式(Observer)
定义一对多依赖:事件发生时自动通知所有订阅者,实现解耦。适用于成就系统、事件驱动、UI 更新:
js
class EventBus {
constructor() {
this.listeners = {}
}
on(event, fn) {
(this.listeners[event] ||= []).push(fn)
}
off(event, fn) {
this.listeners[event] = (this.listeners[event] || []).filter(f => f !== fn)
}
emit(event, data) {
;(this.listeners[event] || []).forEach(fn => fn(data))
}
}
// 用法:战斗系统发事件,成就/UI/音效各干各的
const bus = new EventBus()
bus.on('enemy-killed', () => { score += 10; checkAchievement() })
bus.emit('enemy-killed', enemy)命令模式(Command)
把"操作"封装成对象,支持撤销、重放、输入映射。适用于可撤销操作、连招系统、玩家输入:
js
// 命令对象:封装"执行"与"撤销"
class MoveCommand {
constructor(entity, dx, dy) {
this.entity = entity
this.dx = dx
this.dy = dy
}
execute() {
this.entity.x += this.dx
this.entity.y += this.dy
}
undo() {
this.entity.x -= this.dx
this.entity.y -= this.dy
}
}
// 命令历史:支持撤销
const history = []
const cmd = new MoveCommand(player, 10, 0)
cmd.execute()
history.push(cmd)
// 撤销上一步
history.pop().undo()模式选型速查表
| 模式 | 解决的核心问题 | 典型应用 | 何时避免 |
|---|---|---|---|
| 单例 | 全局唯一访问 | 管理器、资源服务 | 大项目、需要测试时 |
| 对象池 | 高频对象创建开销 | 子弹、粒子、敌人 | 低频对象、池管理复杂 |
| 状态机 | 复杂行为切换 | 角色 AI、动画、流程 | 状态爆炸(改用行为树) |
| 观察者 | 模块解耦通知 | 事件、成就、UI 联动 | 滥用导致调用链失控 |
| 命令 | 操作封装与撤销 | 输入、连招、编辑器 | 简单直接调用即可 |
ECS 与 OOP 架构对比
OOP 架构:以类为中心
传统面向对象架构把"一个游戏实体"建模为一个对象,数据与行为封装在类里:
js
// OOP:一个 Player 类,数据 + 行为全在里面
class Player {
constructor(x, y) {
this.x = x
this.y = y
this.hp = 100
this.speed = 200
}
update(dt) {
this.x += this.speed * dt
if (this.hp <= 0) this.onDie()
}
render(ctx) {
ctx.fillRect(this.x, this.y, 20, 20)
}
onDie() {
// 处理死亡逻辑
}
}优点:直觉、封装好、面向业务建模自然。 缺点:
- 继承深井:
Player extends Character extends Entity,层级深后行为复用困难 - 功能正交问题:飞行怪、地行怪、会飞的玩家……多维度变化靠继承组合爆炸
- 性能:数据分散在对象中,缓存不友好,大量实体遍历时 CPU 缓存命中差
ECS 架构:数据驱动
ECS(Entity Component System)由三部分组成:
- Entity(实体):只是一个 ID,不含任何数据
- Component(组件):纯数据块,如 Position、Velocity、Health
- System(系统):纯逻辑函数,批量处理拥有特定组件的实体
js
// ---- 组件:纯数据 ----
const components = {
position: new Map(), // entityId -> { x, y }
velocity: new Map(), // entityId -> { vx, vy }
health: new Map(), // entityId -> { hp }
renderable: new Map() // entityId -> { color }
}
// 实体:就是一个数字 ID
let nextId = 0
function createEntity() { return nextId++ }
// 创建"一个移动的红色方块"
const e = createEntity()
components.position.set(e, { x: 100, y: 100 })
components.velocity.set(e, { vx: 50, vy: 0 })
components.renderable.set(e, { color: 'red' })
// ---- 系统:纯逻辑,处理所有拥有对应组件的实体 ----
const MovementSystem = {
update(dt) {
// 遍历 velocity 表,连续内存访问,缓存友好
for (const [id, vel] of components.velocity) {
const pos = components.position.get(id)
if (!pos) continue
pos.x += vel.vx * dt
pos.y += vel.vy * dt
}
}
}
const RenderSystem = {
render(ctx) {
for (const [id, r] of components.renderable) {
const pos = components.position.get(id)
ctx.fillStyle = r.color
ctx.fillRect(pos.x, pos.y, 20, 20)
}
}
}优点:
- 组合优于继承:一个实体 = 组件的任意组合,新类型无需新建类
- 性能:组件存连续数组,系统批量遍历,CPU 缓存命中率高
- 易扩展:加新能力 = 加新组件 + 新系统,不改动现有代码
- 数据与逻辑分离:便于序列化、热更新、网络同步
缺点:概念门槛高、代码不直观、小项目过度设计反而繁琐。
完整 ECS 迷你示例
一个"红块追击蓝块"的 ECS 示例,展示系统如何协同(方向键控制蓝块躲避,红块由追击系统自动追踪):
ECS vs OOP 选型指南
| 维度 | OOP | ECS |
|---|---|---|
| 上手难度 | 低,符合直觉 | 高,需转变思维 |
| 代码可读性 | 高,业务语义清晰 | 低,逻辑分散在系统 |
| 扩展能力 | 继承爆炸后变差 | 组合天然易扩展 |
| 性能 | 一般(缓存不友好) | 优(连续内存批量处理) |
| 序列化/网络同步 | 需逐个处理 | 天然支持(组件即数据) |
| 适用规模 | 中小型项目 | 中大型、实体数量多、需要热更 |
决策建议:
- 小游戏 / 原型:OOP 快速简单
- 实体多、类型多变的游戏(如弹幕、生存类):ECS 优势明显
- 需要热更新 / 存档 / 网络同步:ECS 数据驱动天然适合
- 混合方案:多数引擎内部是 ECS 内核(如 Unity、Godot),但暴露 OOP 风格 API 供开发者使用——这是成熟的选择
现实中如何选择
实际项目中常见做法是混合架构:
- 用 ECS 管理"世界中的大量实体"(敌人、子弹、粒子)
- 用 OOP 管理"少数复杂实体"(玩家、Boss、交互系统)
- 单例/管理器负责全局服务(音频、输入、资源)
这样既能享受 ECS 的性能与扩展性,又保留 OOP 的直观性。
本章小结
- 五大模式:单例(全局唯一)、对象池(对象复用)、状态机(行为切换)、观察者(事件解耦)、命令(操作封装)——各有明确的适用场景与陷阱
- OOP:以类为中心,直观易用,但继承深井与缓存不友好是硬伤
- ECS:以数据为中心,组合优于继承,性能与扩展性俱佳,门槛较高
- 选型:小项目用 OOP,大规模/高性能/需热更用 ECS,生产实践通常是混合架构
- 模式与架构不是教条,适合当前项目的规模与团队,才是正确的选择