游戏设计模式
概述
设计模式在游戏开发中扮演着至关重要的角色。与通用软件开发不同,游戏开发面临着高性能要求、实时交互、复杂状态管理和大量对象管理等独特挑战,这使得设计模式在游戏领域的应用具有鲜明的行业特色。
《Game Programming Patterns》(游戏编程模式)是游戏开发者的必读书目,它系统性地阐述了适用于游戏场景的经典设计模式。本文选取了五个在游戏开发中最常使用的设计模式——单例模式、对象池模式、状态机模式、命令模式和观察者模式——结合实际游戏应用场景进行深入解析,并通过一个综合 Demo 展示它们的交互式运行方式。
单例模式(Singleton)
概念
单例模式确保一个类在整个生命周期中只有一个实例,并提供一个全局访问点。这是游戏开发中最常用也最有争议的模式。适度的使用可以简化架构,滥用则会导致代码耦合度上升和测试困难。
游戏中的应用场景
GameManager(游戏管理器) 是单例模式的典型例子——整个游戏中只有一个管理器负责游戏状态、关卡进度和分数管理:
class GameManager {
constructor() {
if (GameManager.instance) return GameManager.instance
this.score = 0
this.gameState = 'menu'
this.currentLevel = 1
GameManager.instance = this
}
addScore(points) {
this.score += points
}
getInstance() {
return GameManager.instance || new GameManager()
}
}AudioManager(音频管理器) 同样适用单例模式——同时播放多个音频通道需要统一的音量控制、音效挂起和资源管理。AudioContext 的创建也受到浏览器限制,全局只有一个实例是合理的。
InputManager(输入管理器) 负责统一处理键盘、鼠标、触摸和手柄输入,将物理设备输入抽象为游戏逻辑可消费的指令。全局统一的输入管理避免了多个模块各自监听事件带来的冲突。
注意事项
单例模式需要谨慎使用。过度依赖全局状态会导致:
- 隐式依赖:函数调用者的依赖关系不明确,降低代码可读性
- 测试困难:全局状态在测试之间不易重置,导致测试结果不可靠
- 并发问题:多线程环境下需要额外同步
业界倾向于通过依赖注入(Dependency Injection)来替代部分单例的使用,但在小型游戏项目中,适度的单例模式可以显著降低开发复杂度。
对象池模式(Object Pool)
概念
对象池模式通过复用已创建的对象来减少频繁的创建和销毁操作,从而降低内存分配和 GC(垃圾回收)的开销。这是游戏开发中最重要的性能优化模式之一。
游戏中的应用场景
子弹系统是对象池最典型的应用。在一款射击游戏中,子弹被发射后飞行一段时间即被销毁。如果每发子弹都通过 new Bullet() 创建、再通过标记删除等待 GC 回收,频繁的内存分配和垃圾回收会导致严重的帧率抖动(Jank)。对象池方案预先创建一批子弹对象,发射时从池中获取,回收时归还池中:
class BulletPool {
constructor(size) {
this.pool = []
this.active = []
for (let i = 0; i < size; i++) {
this.pool.push(this.createBullet())
}
}
acquire() {
if (this.pool.length === 0) return null
const bullet = this.pool.pop()
bullet.active = true
this.active.push(bullet)
return bullet
}
release(bullet) {
bullet.active = false
bullet.reset()
const idx = this.active.indexOf(bullet)
if (idx !== -1) this.active.splice(idx, 1)
this.pool.push(bullet)
}
}粒子系统同样受益于对象池。一场爆炸特效可能涉及数百个粒子,每个粒子只有几秒生命周期。对象池确保粒子的创建和回收不会引起 GC 抖动。此外,对象池还可用于:
- 音效播放器实例:避免频繁创建和销毁音频节点
- 网络消息对象:减少通信过程中的对象创建
- AI 寻路节点:复用大量临时的寻路计算中间对象
池大小管理
对象池的核心挑战在于确定合适的池大小。池太小会导致 acquire() 返回 null,影响功能;池太大会浪费内存。实践中通常采用"按需增长 + 水位线监控"策略,在运行时动态调整池容量。
状态机模式(FSM)
概念
有限状态机(Finite State Machine, FSM)是游戏角色行为控制的基础模式。它将角色的行为建模为一组有限的状态以及状态之间的转移规则,使角色行为逻辑清晰可维护。
游戏中的应用场景
角色动画控制是状态机最经典的应用。一个平台跳跃游戏的主角通常包含以下状态:Idle(待机)、Run(奔跑)、Jump(跳跃)、Fall(下落)、Attack(攻击)和 Hurt(受伤)。状态转移规则定义了哪些转换是合法的:
- Idle → Run(按下方向键)
- Run → Idle(松开方向键)
- Idle/Run → Jump(按下跳跃键)
- Jump → Fall(垂直速度由正转负)
- Fall → Idle/Run(落地)
- Idle/Run/ Jump → Attack(按下攻击键,但 Jump 状态下不可攻击)
class FSM {
constructor() {
this.states = {}
this.currentState = null
this.transitions = {}
}
addState(name, state) {
this.states[name] = state
}
addTransition(from, to, condition) {
if (!this.transitions[from]) this.transitions[from] = []
this.transitions[from].push({ to, condition })
}
changeState(newState) {
if (this.currentState) this.states[this.currentState].onExit()
this.currentState = newState
this.states[this.currentState].onEnter()
}
update(input) {
const currentTransitions = this.transitions[this.currentState] || []
for (const t of currentTransitions) {
if (t.condition(input)) {
this.changeState(t.to)
return
}
}
this.states[this.currentState].onUpdate(input)
}
}状态机的高级变体
分层状态机(Hierarchical FSM) 允许状态嵌套,子状态继承父状态的转移规则。例如"战斗状态"下包含"近战攻击"、"远程射击"和"防御"三个子状态。
并行状态机 允许角色同时处于多个状态,例如跑步的同时可以射击(上身射击状态 + 下身跑步状态)。这在 3D 动作游戏中非常常见。
行为树(Behavior Tree) 比 FSM 更适合复杂的 AI 行为编排,通过组合节点(序列、选择器、并行)来构建行为逻辑,可扩展性和可维护性都优于 FSM。
命令模式(Command)
概念
命令模式将操作请求封装为独立的对象,使请求的发起者与执行者解耦。在游戏开发中,命令模式的核心价值在于支持操作的历史记录、回放和撤销。
游戏中的应用场景
输入系统是命令模式最直接的应用。玩家的每个按键输入都被封装为 Command 对象,存储在输入缓冲区中:
class Command {
constructor(action, timestamp) {
this.action = action
this.timestamp = timestamp
}
execute(player) {
// 由子类实现具体的操作
}
}
class MoveCommand extends Command {
constructor(direction, timestamp) {
super('move', timestamp)
this.direction = direction
}
execute(player) {
player.move(this.direction)
}
}
class InputBuffer {
constructor(size = 20) {
this.buffer = []
this.maxSize = size
}
addCommand(command) {
if (this.buffer.length >= this.maxSize) this.buffer.shift()
this.buffer.push(command)
}
replay(player) {
for (const cmd of this.buffer) {
cmd.execute(player)
}
}
undoLast(player) {
const cmd = this.buffer.pop()
if (cmd) cmd.undo(player)
}
}回放系统利用命令模式录制玩家操作并重新执行,是实现"死亡回放"、"竞速回放"等功能的基石。每个命令记录操作类型和时间戳,回放时按时间顺序重新执行。
可撤销操作在策略类游戏中尤为重要。玩家误操作时需要能够撤销——命令模式通过存储每个操作的逆操作来实现。回合制游戏中常见的"悔棋"功能就是命令模式撤销能力的典型应用。
网络同步同样可以借助命令模式。将玩家输入封装为命令,通过网络发送到服务端执行,确保所有客户端的状态一致性。
观察者模式(Observer)
概念
观察者模式定义了一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会收到通知。它实现了事件源与事件监听者的解耦。
游戏中的应用场景
成就系统是观察者模式的教科书例子。当玩家达成某个条件时(如击杀 100 个敌人),游戏中的各种系统需要做出响应——成就系统弹出解锁通知、音效系统播放成就音效、UI 系统更新成就界面。通过事件总线(EventBus),这些系统无需直接依赖彼此:
class EventBus {
constructor() {
this.listeners = {}
}
on(event, callback) {
if (!this.listeners[event]) this.listeners[event] = []
this.listeners[event].push(callback)
}
off(event, callback) {
if (!this.listeners[event]) return
this.listeners[event] = this.listeners[event]
.filter(cb => cb !== callback)
}
emit(event, data) {
if (!this.listeners[event]) return
this.listeners[event].forEach(cb => cb(data))
}
}
// 使用
const eventBus = new EventBus()
// 成就系统订阅
eventBus.on('enemy_killed', data => {
if (data.totalKills === 100) unlockAchievement('百人斩')
})
// 音效系统订阅
eventBus.on('enemy_killed', () => playSound('kill_confirm'))
// 触发事件
eventBus.emit('enemy_killed', { enemyId: 42, totalKills: 100 })解耦游戏逻辑与表现层是观察者模式的另一个重要应用。当玩家获得经验值时,经验值的计算由游戏逻辑层完成,而经验条的动画、升级特效和音效则由表现层通过事件驱动。这种分离使得游戏逻辑易于测试和复用。
UI 数据绑定在现代游戏引擎中广泛使用观察者模式。当血条、金币数等数据变化时,所有关联的 UI 元素自动更新。这也是 MVVM 架构的核心思想。
注意事项
观察者模式需要注意事件泄漏问题——订阅了事件但没有取消订阅会导致内存泄漏和逻辑错误。在 Unity 中需要格外注意,C# 中 += 注册的事件必须对应 -= 取消。另外,大量观察者链式触发可能导致性能问题和调试困难,大型系统中推荐使用有命名空间的事件系统。
模式组合实践
在实际游戏项目中,这些设计模式通常是组合使用的:单例模式管理全局 GameManager,观察者模式解耦成就/音效系统,对象池模式优化子弹和粒子性能,状态机模式控制角色行为,命令模式处理输入缓冲和回放。理解每种模式的适用场景和局限性,才能在实际开发中做出合理的技术选型。
下方的 Demo 通过一个综合页面展示了五种设计模式的运行机制和交互方式,帮助你直观理解它们各自的行为特征。