☰
游戏设计模式实战指南:GameDevMind 五模式配套代码精讲
2026/10/9 1:32:36 网站建设 项目流程
  • 文档
  • 知识库
  • 教程
  • 游戏开发

【免费下载链接】GameDevMind

最全面的游戏开发技术图谱(Game Development Map)。帮助游戏开发者们在已知问题上节省时间,省出更多的精力投入到更有创造性的工作中去。

项目地址:https://gitcode.com/gonglei007/GameDevMind
点击查看免费下载

导读:本文以 GameDevMind 仓库中「设计模式实战」配套代码(code/artile-sample-code/01-foundation/02-design-patterns/)为骨架,逐一剖析单例、观察者、状态、对象池、命令五种在游戏开发中最常用的设计模式:先给出可直接编译运行的代码,再结合仓库源码逐层讲解实现原理、适用场景与常见坑点,并延伸至仓库中更完整的工程级实现(输入录制/撤销/重做、对象池性能基准测试)。读完本文,你将掌握五种模式的"游戏化"写法,能够直接复制运行,并懂得如何用它们解决内存抖动、输入缓冲、状态爆炸、系统解耦等真实问题。

设计模式是程序开发的一项内功,合理选用(而非滥用)能让系统更健壮、更易读、更易维护。在游戏开发中,模式的应用场景尤为具体:单例管理全局系统、观察者驱动事件系统、状态机控制角色行为、对象池优化高频对象、命令模式实现输入缓冲与回放。GameDevMind 仓库的 设计模式知识图谱 系统整理了这些模式的维度、应用场景与常见问题,而配套示例代码则给出了可运行的最小实现,两者互为印证。

示例全景与运行方式

仓库中 设计模式实战 — 配套代码 共提供 5 个示例,覆盖 5 种核心模式,C++ 与 Python 两种语言:

模式文件语言游戏场景
单例模式(MonoSingleton)singleton.cppC++全局唯一管理器(音频、配置)
观察者模式(事件系统)observer.pyPython事件总线 / 信号槽
状态模式(角色状态机)state_machine.cppC++角色 Idle / Run / Attack / Dead
对象池模式object_pool.cppC++子弹 / 粒子高频生成回收
命令模式(输入缓冲)command.cppC++格斗输入缓冲 / 录像回放

运行方式(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 演示六步流程:

  1. 创建玩家「勇者」;
  2. 录制输入序列:向右移动 → 向下移动 → 攻击 → 火球术(SkillCommand支持执行 lambda 与撤销 lambda 双回调)→ 向前跑;
  3. 批量执行全部命令;
  4. 撤销最近 2 步(UndoLast);
  5. 重做 1 步(RedoLast,重做火球术冲刺);
  6. 完整回放(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)。帮助游戏开发者们在已知问题上节省时间,省出更多的精力投入到更有创造性的工作中去。

项目地址:https://gitcode.com/gonglei007/GameDevMind
点击查看免费下载

相关推荐

上一篇:alive-progress主题系统完全解析:内置样式与自定义方案终极指南
下一篇:新手 10 分钟跑通 d2s-editor:免费暗黑2存档编辑器使用指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询