- 文档
- 知识库
- 教程
- 游戏开发
【免费下载链接】GameDevMind
最全面的游戏开发技术图谱(Game Development Map)。帮助游戏开发者们在已知问题上节省时间,省出更多的精力投入到更有创造性的工作中去。
导读:本文以 GameDevMind 仓库中「设计模式实战」配套代码(code/artile-sample-code/01-foundation/02-design-patterns/)为骨架,逐一剖析单例、观察者、状态、对象池、命令五种在游戏开发中最常用的设计模式:先给出可直接编译运行的代码,再结合仓库源码逐层讲解实现原理、适用场景与常见坑点,并延伸至仓库中更完整的工程级实现(输入录制/撤销/重做、对象池性能基准测试)。读完本文,你将掌握五种模式的"游戏化"写法,能够直接复制运行,并懂得如何用它们解决内存抖动、输入缓冲、状态爆炸、系统解耦等真实问题。
设计模式是程序开发的一项内功,合理选用(而非滥用)能让系统更健壮、更易读、更易维护。在游戏开发中,模式的应用场景尤为具体:单例管理全局系统、观察者驱动事件系统、状态机控制角色行为、对象池优化高频对象、命令模式实现输入缓冲与回放。GameDevMind 仓库的 设计模式知识图谱 系统整理了这些模式的维度、应用场景与常见问题,而配套示例代码则给出了可运行的最小实现,两者互为印证。
示例全景与运行方式
仓库中 设计模式实战 — 配套代码 共提供 5 个示例,覆盖 5 种核心模式,C++ 与 Python 两种语言:
| 模式 | 文件 | 语言 | 游戏场景 |
|---|---|---|---|
| 单例模式(MonoSingleton) | singleton.cpp | C++ | 全局唯一管理器(音频、配置) |
| 观察者模式(事件系统) | observer.py | Python | 事件总线 / 信号槽 |
| 状态模式(角色状态机) | state_machine.cpp | C++ | 角色 Idle / Run / Attack / Dead |
| 对象池模式 | object_pool.cpp | C++ | 子弹 / 粒子高频生成回收 |
| 命令模式(输入缓冲) | command.cpp | C++ | 格斗输入缓冲 / 录像回放 |
运行方式(README 原文,均可在仓库目录下直接执行):
# C++ g++ -std=c++17 singleton.cpp -o singleton && ./singleton # Python python3 observer.py其余 C++ 示例同理编译:g++ -std=c++17 state_machine.cpp -o state_machine && ./state_machine、g++ -std=c++17 object_pool.cpp -o object_pool && ./object_pool、g++ -std=c++17 command.cpp -o command && ./command。下面按模式逐一展开。
单例模式:全局唯一的管理器
知识图谱对单例的定义是:确保类只有一个实例,提供全局访问点,典型应用是各基础系统——GameSystem、SoundSystem、ResourceManager、NetworkManager 等(见 1.2.1.设计模式.md)。singleton.cpp 给出了三个递进版本。
版本一:C++11 线程安全单例
GameConfig用「函数局部静态变量」实现懒加载单例——static局部变量在 C++11 及之后由标准保证初始化线程安全:
class GameConfig { public: static GameConfig& getInstance() { static GameConfig instance; // C++11 保证线程安全 return instance; } void setVolume(int v) { volume_ = v; } int getVolume() const { return volume_; } private: GameConfig() : volume_(80) {} // 构造函数私有 ~GameConfig() = default; GameConfig(const GameConfig&) = delete; // 禁止拷贝 GameConfig& operator=(const GameConfig&) = delete; // 禁止赋值 int volume_; };三个关键设计:构造私有(外部无法 new)、拷贝/赋值 delete(防止复制破坏唯一性)、通过getInstance()静态访问。这就是知识图谱中「线程安全 → 静态初始化」的代码级答案。主函数验证了全局唯一性:setVolume(50)后再次获取仍读回 50。
版本二:模板化 MonoSingleton(模拟 Unity 风格)
Unity/Cocos 中最常见的模式是让每个管理器继承一个MonoSingleton<T>基类:
template<typename T> class MonoSingleton { public: static T& getInstance() { static T instance; return instance; } protected: MonoSingleton() = default; virtual ~MonoSingleton() = default; private: MonoSingleton(const MonoSingleton&) = delete; MonoSingleton& operator=(const MonoSingleton&) = delete; }; class AudioManager : public MonoSingleton<AudioManager> { friend class MonoSingleton<AudioManager>; public: void playBGM(const std::string& name) { /* ... */ } private: AudioManager() = default; // 私有构造,仅模板基类可实例化 };这是 CRTP(奇异递归模板模式):每个管理器只需继承MonoSingleton<自己>,即可免费获得全局唯一实例。AudioManager的构造函数同样私有,并借助friend允许模板基类构造它。assert(&am == &AudioManager::getInstance())验证了两次获取同一实例。
版本三:Service Locator —— 单例的进阶替代
知识图谱提醒单例的典型问题是「测试困难」与「全局状态污染」,解决方向是「依赖注入替代、提供设置实例方法、接口抽象」。代码给出了一个务实答案——服务定位器 + 接口 + Null Object:
class IAudioService { // 抽象接口 public: virtual void playSound(const std::string&) = 0; }; class RealAudioService : public IAudioService { /* 真实播放 */ }; class NullAudioService : public IAudioService { /* 静默 — Null Object */ }; class ServiceLocator { public: static IAudioService& getAudio() { return *audioService_; } static void provide(IAudioService* service) { audioService_ = service; } private: static inline IAudioService* audioService_ = nullptr; static inline NullAudioService nullService_; // 未注入时返回空对象 };好处:调用方依赖接口而非具体类;provide()可在测试时注入 Mock;未注入时默认返回空实现,避免空指针崩溃。这是从「全局唯一」走向「可替换」的重要演进。
观察者模式:轻量事件总线
知识图谱中观察者/发布订阅的定位是:两系统无耦合交互,一系统变化让另一系统知道并响应,好处是系统更独立、性能可控,游戏中的典型实现就是事件系统。observer.py 实现了一个极简 EventBus。
EventBus 三件套:on / off / emit
class EventBus: def __init__(self): self._handlers: Dict[str, List[Callable]] = {} def on(self, event: str, callback: Callable): # 订阅 self._handlers.setdefault(event, []).append(callback) def off(self, event: str, callback: Callable): # 取消订阅 if event in self._handlers: self._handlers[event] = [h for h in self._handlers[event] if h != callback] def emit(self, event: str, **kwargs): # 触发 for handler in self._handlers.get(event, []): handler(kwargs)核心设计:以字符串事件名为键、回调函数列表为值的字典。emit时通过关键字参数(**kwargs)把数据打包成字典传给所有订阅者,订阅者解包使用,互不依赖。
游戏事件实战
代码用Player+ 四个响应函数模拟完整游戏流程:命中反馈、死亡掉落、分数变化、成就解锁:
bus.on("player:hit", on_player_hit) bus.on("player:death", on_player_death) bus.on("score:changed", on_score_changed) bus.on("achievement:unlocked", on_achievement) bus.emit("player:hit", attacker="哥布林", target="英雄", damage=15) bus.emit("score:changed", delta=100, total=150) bus.emit("achievement:unlocked", name="初出茅庐", desc="首次击败 Boss") bus.emit("player:death", name="英雄", drop_count=3)演示末尾还验证了取消订阅:bus.off("player:hit", on_player_hit)后监听者列表清空。事件名的命名空间:动作风格(如player:hit、score:changed)在大型项目里便于检索与归类。
实战注意点(来自知识图谱)
- 内存泄漏:对象销毁时必须
off取消订阅,或用弱引用——否则被销毁的对象仍留在回调列表里; - 通知顺序:多个订阅者默认按注册顺序执行,若需优先级需额外机制;
- 循环通知:避免在回调里再次修改被观察者导致无限触发;
- 性能:高频事件要考虑批量通知或限制订阅者数量。
状态模式:角色状态机
知识图谱给出角色状态机的经典迁移图:Idle ↔ Walk(移动输入/停止)、Idle/Walk → Attack(攻击输入)、Attack → Idle(动作完成)。state_machine.cpp 用「状态基类 + 具体状态类 + Character 持有当前状态」实现,替代层层 if-else。
状态基类与迁移接口
class CharacterState { public: virtual void enter(Character&) {} // OnEnter:进入状态 virtual void update(Character&) {} // OnUpdate:每帧更新 virtual void exit(Character&) {} // OnExit:离开状态 virtual std::string name() const = 0; };Character::changeState()是状态迁移的统一入口,遵循「先 exit 旧状态、再 enter 新状态」的 FSM 生命周期约定:
void changeState(std::unique_ptr<CharacterState> newState) { if (state_) state_->exit(*this); state_ = std::move(newState); if (state_) state_->enter(*this); }四个具体状态
| 状态类 | enter 行为 | update 行为 | 对应知识图谱场景 |
|---|---|---|---|
IdleState | 进入待机 | 播放待机动画、随机小动作 | 待机 |
RunState | 开始奔跑 | — | 移动 |
AttackState | 开始攻击并清零连击数 | 累计连击,第 3 次触发"三连击" | 攻击 |
DeadState | 宣告阵亡 | — | 死亡 |
主函数完整演示了状态流转:Idle →(移动)→ Run →(攻击)→ Attack(连击 4 帧触发三连击)→(HP ≤ 0)→ Dead,全程用stateName()打印当前状态。
状态爆炸与优化方向
知识图谱指出状态机的典型问题是状态数量爆炸与转换逻辑复杂,解决方向包括:分层状态机、行为树、状态转换表、状态池。当角色动作超过 10 个、转换规则彼此耦合时,就应考虑这些升级方案,而不是无限堆叠状态类。
对象池模式:子弹与粒子的内存救星
对象池的动机非常明确(见 object_pool.cpp 注释):弹幕游戏每秒产生数百颗子弹,频繁new/delete会导致内存碎片和 GC 抖动,而对象池预先分配并循环复用。知识图谱同时把它列为工厂模式的典型应用场景之一——「对象池:工厂创建和管理池中对象实例」。
池的核心实现
template<typename T> class ObjectPool { public: explicit ObjectPool(size_t initialSize = 32) { for (size_t i = 0; i < initialSize; i++) { // 预分配 auto obj = std::make_unique<T>(); obj->reset(); available_.push(obj.get()); allObjects_.push_back(std::move(obj)); } } T* acquire() { // 获取 if (available_.empty()) { // 池耗尽:动态扩展 for (int i = 0; i < 16; i++) { /* 新增 16 个 */ } } T* obj = available_.front(); available_.pop(); return obj; } void release(T* obj) { // 归还 obj->reset(); available_.push(obj); } private: std::queue<T*> available_; // 空闲队列 std::vector<std::unique_ptr<T>> allObjects_; // 所有权容器 };数据结构上采用「双容器」:allObjects_(unique_ptr持有所有权、永不释放)+available_(空闲指针队列)。这样既保证内存只分配一次,又能 O(1) 获取/归还。Bullet的reset()负责把对象恢复为"新子弹"状态(坐标清零、生命周期复位为 5.0f、active=false)。
演示流程:初始 4 颗 → 发射 6 颗(池自动扩展 +16,并打印警告)→ 回收 4 颗 → 再次获取 2 颗时复用回收的子弹(输出复用 Bullet #x),直观展示循环复用。
进阶:仓库中的性能基准测试
仓库在工程目录下提供了更完整的版本 object_pool/main.cpp:模拟300 帧、每帧 200 颗子弹(总计 60000 颗),用std::chrono::high_resolution_clock对比两种策略:
- 策略 A:每帧
new Bullet(),过期delete; - 策略 B:预分配
ObjectPool<Bullet> pool(bullets_per_frame * 2),Acquire()/Release()循环复用。
基准使用固定随机种子(std::mt19937 rng(42))保证两种策略完全同场景可复现,输出new/delete 耗时、对象池耗时、加速比与节省时间,并给出结论:对象池避免频繁 malloc/free,减少内存碎片,帧率更稳定。该目录还带 CMakeLists.txt 可直接构建,README.md 说明运行方式——建议自行运行对比,验证对象池在真实负载下的收益。
注意:加速比是运行环境相关的实测数据,不同编译器、平台结果不同,本文不预设具体数字。
命令模式:输入缓冲与回放系统
命令模式把「请求」封装为对象,从而支持参数化、队列化、日志化与撤销/重做(知识图谱定义)。command.cpp 以格斗游戏输入缓冲为场景,演示「录制 → 分帧执行 → 历史回放」全流程。
命令抽象与接收者
class Command { public: virtual void execute() = 0; virtual void undo() {} // 预留撤销能力 virtual std::string describe() const = 0; // 描述,用于回放记录 };接收者Fighter提供 jump / punch / kick / special / block 五个动作,每个具体命令(JumpCommand、PunchCommand…)持有接收者引用,在execute()中调用对应动作。命令与接收者分离,是命令模式的精髓——发出命令的代码不需要知道接收者是谁。
InputHandler:队列缓冲 + 历史回放
class InputHandler { public: void enqueue(std::unique_ptr<Command> cmd) { // 入队(缓冲) buffer_.push(std::move(cmd)); } void processFrame() { // 每帧执行最早的命令 if (buffer_.empty()) return; auto cmd = std::move(buffer_.front()); buffer_.pop(); history_.push_back(cmd->describe()); // 写入历史 cmd->execute(); } private: std::queue<std::unique_ptr<Command>> buffer_; std::vector<std::string> history_; };演示模拟格斗游戏的典型输入序列↓↘→P, K, P:玩家可能在同一帧内快速按键,系统将三条命令入队后分三帧(Frame 1/2/3)依次执行,最终showHistory()打印Frame 1: Special、Frame 2: Kick、Frame 3: Punch的完整回放记录。这正是知识图谱中「游戏 → 撤销/重做功能、操作历史、宏命令」的场景实现。
进阶:仓库中的录制/撤销/重做/回放实现
工程目录 command/ 给出了更完整的版本,main.cpp 演示六步流程:
- 创建玩家「勇者」;
- 录制输入序列:向右移动 → 向下移动 → 攻击 → 火球术(
SkillCommand支持执行 lambda 与撤销 lambda 双回调)→ 向前跑; - 批量执行全部命令;
- 撤销最近 2 步(
UndoLast); - 重做 1 步(
RedoLast,重做火球术冲刺); - 完整回放(
Replay):重置玩家后从录制序列重新执行。
其中SkillCommand的构造展示了命令模式的灵活之处——执行与撤销逻辑可以直接用 lambda 注入,无需为每个技能单独建类;CommandQueue(command_queue.hpp)则管理录制序列、撤销栈与重做栈。目录内同样提供 CMakeLists.txt 便于构建验证。
实战注意点
- 命令对象过多:高频率命令(如移动指令)可用对象池复用,知识图谱明确建议「命令对象池、轻量级命令、合并相似命令」;
- 撤销深度限制:撤销栈要限制深度(如编辑器常限 50 步),避免内存无限增长;
- 序列化:若命令需跨端传输(网络同步)或持久化(录像存档),
describe()这类序列化接口就是基础。
模式之外:知识图谱中的架构补充
知识图谱在五种模式之外,还整理了与游戏架构强相关的三个模式,可作为延伸阅读(1.2.1.设计模式.md):
- 工厂模式(Factory):封装对象创建过程,适合「按条件创建不同类型」「创建过程复杂」的场景;与对象池天然搭配(池内实例的创建统一由工厂管理);
- MVC:Model(数据/业务)、View(纯表现)、Controller(业务逻辑)分离,适用于好友、排行榜、投票等有表现与交互的功能;Controller 膨胀时可抽取 Service 层,View-Model 耦合用观察者解耦;
- ECS:数据(Component)与逻辑(System)分离、Entity 仅作 ID 组合,适合大量对象的高性能处理(物理、渲染),但不适合偏业务流程的功能。
知识图谱还为每个模式附带了AI Coding 提示词范例(如「为这个 C# SoundSystem 实现线程安全的懒加载单例,并提供一个 SetInstance 方法便于测试时注入 Mock」),可作为用 AI 辅助编写模式化代码时的交互模板,详见原文。
总结:五模式的选型速查
| 模式 | 一句话定位 | 典型场景 | 核心坑点 |
|---|---|---|---|
| 单例 | 全局唯一、全局访问 | 配置、音频、资源管理 | 线程安全、测试困难、全局状态 |
| 观察者 | 一对多解耦通知 | 事件系统、UI 更新、网络状态通知 | 内存泄漏(未取消订阅)、循环通知 |
| 状态机 | 状态行为封装进独立类 | 角色行为、登录/网络/房间状态 | 状态爆炸、转换逻辑复杂 |
| 对象池 | 预分配 + 循环复用 | 子弹、粒子、高频临时对象 | 复用对象必须 reset 干净 |
| 命令 | 请求封装为对象 | 输入缓冲、撤销/重做、回放、宏 | 命令对象过多、撤销深度 |
建议的动手路径:先按 README 的命令编译运行全部 5 个示例,观察输出;再打开工程版 command/ 与 object_pool/ 的基准/撤销重做实现,对比示例版与工程版的差异;最后回到 知识图谱,用「会遇到哪些问题」清单逐条对照自己项目中的真实代码。设计模式的价值不在背定义,而在把「已知问题的成熟解法」内化为肌肉记忆——这正是 GameDevMind 仓库「在已知问题上节省时间」的初衷。
- 文档
- 知识库
- 教程
- 游戏开发
【免费下载链接】GameDevMind
最全面的游戏开发技术图谱(Game Development Map)。帮助游戏开发者们在已知问题上节省时间,省出更多的精力投入到更有创造性的工作中去。
相关推荐
GameDevMind 游戏开发设计模式实战指南:从单例到 ECS 的九大模式与 AI Coding 协同
GameDevMind 游戏开发设计模式实战指南:从单例到 ECS 的九大模式与 AI Coding 协同 设计模式是程序开发的一项内功,合理选用(而非滥用)设
文档教程知识库游戏开发GameDevMind 程序设计篇:设计模式、数据结构、算法与代码重构实战指南
GameDevMind 程序设计篇:设计模式、数据结构、算法与代码重构实战指南 程序设计是游戏开发团队最重要的基本功,它决定了整个团队的开发效率与软件维护成本。
文档教程知识库游戏开发Swift 创建型设计模式(Creational Design Patterns)实战指南:基于 Design-Patterns-In-Swift 源码逐模式精讲
Swift 创建型设计模式(Creational Design Patterns)实战指南:基于 Design Patterns In Swift 源码逐模式精
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考