游戏常见设计模式

游戏常见设计模式

时间:2026/04/09

关键词:单例、状态模式、命令模式、观察者、组件化、对象池
核心目标:从游戏开发常见场景出发,理解哪些模式真的有用,以及哪些模式容易被滥用。


1. 游戏代码为什么特别容易模式化

游戏逻辑常见特点:

  • 实体多
  • 状态多
  • 事件多
  • 生命周期复杂
  • 性能敏感

因此很多经典模式在游戏里非常常见,但也特别容易被滥用。


2. 单例模式:能用,但要克制

单例通常用于:

  • 配置中心
  • 日志系统
  • 全局资源管理器

最简单安全的写法通常是局部静态:

1
2
3
4
5
6
7
8
9
10
class GameConfig {
public:
static GameConfig& instance() {
static GameConfig cfg;
return cfg;
}

private:
GameConfig() = default;
};

优点:

  • 简单
  • 线程安全初始化

缺点:

  • 全局依赖隐蔽
  • 测试困难
  • 生命周期难拆

所以经验上:

  • 单例适合少量基础设施,不适合把一切都做成全局对象

3. 状态模式:替代大 switch

当一个角色会在多种状态之间切换,例如:

  • Idle
  • Chase
  • Attack
  • Dead

如果全写在一个大 switch 里,代码会越来越乱。
状态模式的思路是:

  • 每个状态自己负责更新逻辑和转移条件
1
2
3
4
5
6
struct Monster;

struct State {
virtual ~State() = default;
virtual void update(Monster& m) = 0;
};

这样“状态行为”会比“状态枚举 + 大分支”更容易扩展。


4. 命令模式:把输入和行为解耦

适用场景:

  • 输入映射
  • 回放系统
  • AI 行为排队
  • 网络同步操作记录

基本思路:

  • 把“做什么”封装成命令对象
1
2
3
4
struct Command {
virtual ~Command() = default;
virtual void execute() = 0;
};

这样可以做到:

  • 排队执行
  • 延迟执行
  • 撤销/重放

5. 观察者 / 事件模式

适合:

  • UI 更新
  • 成就系统
  • 音效触发
  • 状态广播

思路是:

  • 某个系统发事件
  • 多个订阅者响应

优点:

  • 降低模块直接耦合

风险:

  • 调用链变隐蔽
  • 调试困难

所以事件系统要控制好:

  • 事件粒度
  • 生命周期
  • 订阅关系

6. 组件化 / ECS 思路

传统继承层级:

  • Monster -> BossMonster -> FlyingBossMonster -> ...

很容易爆炸。
组件化更偏向:

  • Position
  • Render
  • Physics
  • Health

通过组合形成实体能力。

这样更灵活,也更适合:

  • 数据驱动
  • 批量更新
  • SoA / cache 友好设计

7. 对象池:减少频繁分配

游戏里这些对象往往高频创建销毁:

  • 子弹
  • 粒子
  • 临时特效

如果每次都 new/delete,可能带来:

  • 分配开销
  • 碎片
  • 抖动

对象池的思路是:

  • 提前分配一批对象
  • 使用时取出
  • 用完后归还

但要注意:

  • 池化会增加状态管理复杂度
  • 不是所有对象都值得池化

8. 模板方法模式

适用于:

  • 主流程固定
  • 个别步骤由派生类决定

例如角色更新:

1
2
3
4
5
6
7
8
9
10
11
12
struct Character {
virtual ~Character() = default;
virtual void think() = 0;
virtual void move() = 0;
virtual void draw() = 0;

void update() {
think();
move();
draw();
}
};

这种模式的优点是流程稳定,但要避免基类职责过重。


9. 游戏开发里最常见的误区

9.1 什么都做成单例

最后会形成巨型全局依赖网。

9.2 继承层级过深

很多时候组合比继承更稳。

9.3 事件系统滥用

会让控制流难以追踪。

9.4 为了模式而模式

很多小项目只需要清晰结构,不需要把所有经典模式全搬进来。


10. 参考实例:对象池管理子弹

游戏里子弹、粒子、临时特效经常大量创建和销毁。
对象池可以把频繁分配变成复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <optional>
#include <cstddef>
#include <vector>

struct Bullet {
bool active = false;
float x = 0.0f;
float y = 0.0f;
float vx = 0.0f;
float vy = 0.0f;
float life = 0.0f;
};

class BulletPool {
public:
explicit BulletPool(std::size_t capacity)
: bullets_(capacity) {}

Bullet* spawn(float x, float y, float vx, float vy) {
for (auto& b : bullets_) {
if (!b.active) {
b = Bullet{
.active = true,
.x = x,
.y = y,
.vx = vx,
.vy = vy,
.life = 2.0f,
};
return &b;
}
}

return nullptr; // 池满,可以选择丢弃或扩容
}

void update(float dt) {
for (auto& b : bullets_) {
if (!b.active) {
continue;
}

b.x += b.vx * dt;
b.y += b.vy * dt;
b.life -= dt;

if (b.life <= 0.0f) {
b.active = false;
}
}
}

const std::vector<Bullet>& bullets() const {
return bullets_;
}

private:
std::vector<Bullet> bullets_;
};

这个例子体现了对象池的几个取舍:

  • 分配次数少,运行时更稳定
  • 对象地址相对稳定
  • 需要显式维护 active 状态
  • 池满策略必须提前设计

11. 一页总结

游戏里真正常用、且值得优先掌握的几个模式是:

  1. 状态模式
  2. 命令模式
  3. 观察者 / 事件模式
  4. 组件化 / ECS
  5. 对象池

单例不是不能用,但一定要克制。

如果只记一句:

游戏模式的价值不在“名词多高级”,而在于它能不能降低复杂状态和高频对象管理的混乱度。