一、观察者模式 + 核心定义
关键字:观察者、被观察者、订阅 - 通知、解耦、实时通信
初学者必懂:观察者模式的核心是“订阅 - 通知”机制——一个对象(被观察者)发生变化时,会自动通知所有关注它的对象(观察者),而且两者之间不需要直接关联,实现代码解耦!
先看一个游戏开发中的经典痛点:你要给游戏加一个 成就系统,比如“从桥上掉下去”“杀死 1000 只怪物”。如果直接写硬编码,会发生什么?
// 糟糕的硬编码:物理引擎中直接调用成就系统
void Physics::updateEntity(Entity& entity) {
bool wasOnSurface = entity.isOnSurface();
entity.accelerate(GRAVITY);
entity.update();
// 直接在物理代码中调用成就解锁——严重耦合!
if (wasOnSurface && !entity.isOnSurface() && entity.isHero()) {
Achievements::getInstance()->unlockFallOffBridge();
}
}
问题分析:① 耦合严重:物理引擎和成就系统绑死,修改成就逻辑要改物理代码;② 扩展性差:新增“播放坠落音效”“显示提示文字”,还要在物理代码中加新调用;③ 维护困难:代码分散在各个模块,排查问题时要全局找调用。
你(观察者)订阅了《游戏开发周报》(被观察者):① 你不需要知道报社(被观察者)的印刷、发行流程,只需留下联系方式;② 报社出新报纸时,会主动给所有订阅者(观察者)发报纸,不用逐个通知;③ 你不想订了可以取消订阅,报社不用管你是谁,只需从列表中移除。这就是观察者模式的核心逻辑!
观察者模式的核心公式
观察者模式 = 被观察者(主题)+ 观察者(订阅者)+ 订阅 / 取消订阅 + 通知
- 被观察者(Subject/ 主题):发生变化的对象,维护一个观察者列表,提供“订阅”“取消订阅”和“通知”的接口(比如物理引擎、玩家角色)
- 观察者(Observer/ 订阅者):关注被观察者变化的对象,提供“接收通知”的接口(比如成就系统、音效系统、UI 系统)
- 订阅 / 取消订阅:观察者注册到被观察者的列表,或从列表中移除
- 通知:被观察者状态变化时,遍历观察者列表,调用每个观察者的“接收通知”方法
核心价值:解耦发送者和接收者——被观察者不知道观察者的具体实现,只知道它们实现了统一的接口;观察者也不用主动查询被观察者的状态,只需等待通知。两者可以独立修改、扩展,互不影响。
二、成就系统案例:从零实现观察者模式
关键字:接口定义、被观察者实现、观察者实现、订阅通知流程
我们以“从桥上掉下去解锁成就”为例,一步步实现观察者模式,解决硬编码的耦合问题!
1. 步骤 1:定义核心接口(面向接口编程)
首先定义两个接口:Observer(观察者接口)和 Subject(被观察者接口),这是解耦的关键——所有观察者和被观察者都遵循统一规则。
观察者接口(Observer)
观察者需要一个“接收通知”的方法,参数包含“发生变化的对象”和“事件类型”(让观察者知道发生了什么):
// 事件类型枚举:定义游戏中可能发生的事件
enum Event {
EVENT_ENTITY_FELL, // 实体坠落事件
EVENT_ENTITY_KILLED, // 实体被杀死事件
EVENT_LEVEL_UP // 等级提升事件
// 可以扩展更多事件
};
// 观察者接口:所有观察者都要实现这个接口
class Observer {
public:
virtual ~Observer() {} // 虚析构函数,确保子类正确销毁
// 接收通知的方法:entity= 发生事件的实体,event= 事件类型
virtual void onNotify(const Entity& entity, Event event) = 0;
};
初学者须知:纯虚函数(=0)意味着这个接口只定义“规则”,不提供具体实现,子类必须重写 onNotify 方法才能实例化。这样保证了所有观察者都有统一的“接收通知”入口。
被观察者接口(Subject)
被观察者需要维护观察者列表,并提供“添加观察者”“移除观察者”“通知所有观察者”的方法:
// 被观察者接口:所有被观察者都要实现这个接口
class Subject {
public:
virtual ~Subject() {}
// 订阅:添加观察者到列表
virtual void addObserver(Observer* observer) = 0;
// 取消订阅:从列表中移除观察者
virtual void removeObserver(Observer* observer) = 0;
protected:
// 通知所有观察者:被观察者内部调用,对外隐藏实现
virtual void notify(const Entity& entity, Event event) = 0;
};
设计技巧:notify 方法设为 protected——只有被观察者自己能触发通知,外部不能随意调用,保证了通知的安全性(比如不能手动调用 notify 解锁成就)。
2. 步骤 2:实现被观察者(物理引擎)
物理引擎是“被观察者”——它能检测到实体坠落事件,需要实现 Subject 接口,维护观察者列表,并在事件发生时发送通知。
// 物理引擎类:被观察者,检测实体状态变化
class Physics : public Subject {
public:
Physics() : numObservers_(0) {} // 初始化观察者数量为 0
// 实现 addObserver:添加观察者到列表
void addObserver(Observer* observer) override {
if (numObservers_ < MAX_OBSERVERS) {// 防止数组越界 observers_[numObservers_++] = observer; } } // 实现 removeObserver:从列表中移除观察者 void removeObserver(Observer* observer) override {int index = -1; // 找到观察者在数组中的位置 for (int i = 0; i < numObservers_; i++) {if (observers_[i] == observer) {index = i; break;} } if (index != -1) {// 移除:将后面的观察者向前移动一位,覆盖要移除的元素 for (int i = index; i < numObservers_ – 1; i++) {observers_[i] = observers_[i + 1]; } numObservers_–; // 观察者数量减 1 } } // 物理引擎的核心方法:更新实体状态 void updateEntity(Entity& entity) {bool wasOnSurface = entity.isOnSurface(); // 之前是否在地面上 entity.accelerate(GRAVITY); // 应用重力 entity.update(); // 更新实体位置 // 检测是否发生“坠落事件”(之前在地面,现在不在)if (wasOnSurface && !entity.isOnSurface()) {// 触发通知:告诉所有观察者“这个实体坠落了”notify(entity, EVENT_ENTITY_FELL); } } private: static const int MAX_OBSERVERS = 10; // 最大观察者数量(可根据需求调整)Observer* observers_[MAX_OBSERVERS]; // 观察者列表(数组存储,简单易懂)int numObservers_; // 当前观察者数量 // 实现 notify:遍历观察者列表,发送通知 void notify(const Entity& entity, Event event) override {for (int i = 0; i < numObservers_; i++) {// 调用每个观察者的 onNotify 方法,传递实体和事件 observers_[i]->onNotify(entity, event);
}
}
};
初学者解读:① 用数组存储观察者列表(简单直观,适合初学者;实际项目中可改用 vector 动态数组);② addObserver 和 removeObserver 负责维护列表;③ updateEntity 检测到坠落事件后,调用 notify 方法,自动通知所有观察者;④ 物理引擎完全不知道观察者是成就系统还是音效系统,只知道它们实现了 Observer 接口——解耦成功!
3. 步骤 3:实现观察者(成就系统)
成就系统是“观察者”——它关注物理引擎的坠落事件,需要实现 Observer 接口,在 onNotify 方法中处理通知,判断是否解锁成就。
// 成就类型枚举
enum Achievement {
ACHIEVEMENT_FELL_OFF_BRIDGE, // 从桥上掉下去
ACHIEVEMENT_KILL_1000_MONSTERS, // 杀死 1000 只怪物
ACHIEVEMENT_LEVEL_50 // 等级达到 50 级
};
// 成就系统类:观察者,接收物理引擎的通知
class Achievements : public Observer {
public:
// 实现 onNotify:接收通知并处理
void onNotify(const Entity& entity, Event event) override {
switch (event) {
case EVENT_ENTITY_FELL:
// 判断:是否是主角,且主角在桥上
if (entity.isHero() && isHeroOnBridge(entity)) {
unlockAchievement(ACHIEVEMENT_FELL_OFF_BRIDGE);
}
break;
case EVENT_ENTITY_KILLED:
// 处理“杀死怪物”成就(简化逻辑)
if (!entity.isHero()) {// 被杀死的是怪物
monsterKillCount_++;
if (monsterKillCount_ >= 1000) {
unlockAchievement(ACHIEVEMENT_KILL_1000_MONSTERS);
}
}
break;
// 可以扩展其他事件的处理
default:
break;
}
}
private:
int monsterKillCount_ = 0; // 怪物击杀计数
// 判断主角是否在桥上(实际项目中需要根据位置检测)
bool isHeroOnBridge(const Entity& hero) {
// 简化逻辑:假设坐标 x 在 100-200 之间是桥的位置
return hero.getPosition().x >= 100 && hero.getPosition().x <= 200; } // 解锁成就(显示动画、播放音效等)void unlockAchievement(Achievement achievement) {switch (achievement) {case ACHIEVEMENT_FELL_OFF_BRIDGE: std::cout << “🎉 成就解锁:失足英雄!从桥上掉下去的感觉如何?” << std::endl; // 实际项目中:播放成就动画、添加成就图标到 UI、播放解锁音效 break; case ACHIEVEMENT_KILL_1000_MONSTERS: std::cout << “🎉 成就解锁:怪物终结者!已杀死 1000 只怪物!” << std::endl; break; default: break;} } };
设计亮点:① 成就系统只关注自己关心的事件(EVENT_ENTITY_FELL、EVENT_ENTITY_KILLED),其他事件忽略;② 成就系统的逻辑完全独立,修改成就条件、解锁效果,不需要改动物理引擎代码;③ 可以轻松扩展更多成就,只需在 onNotify 中添加 case 分支。
4. 步骤 4:使用观察者模式(订阅 - 通知流程)
最后,我们将物理引擎(被观察者)和成就系统(观察者)关联起来,完成“订阅 - 通知”的完整流程:
int main() {
// 1. 创建被观察者:物理引擎
Physics physics;
// 2. 创建观察者:成就系统
Achievements achievements;
// 3. 订阅:将成就系统注册到物理引擎的观察者列表
physics.addObserver(&achievements);
// 4. 模拟游戏运行:主角实体
Entity hero(true); // true 表示是主角
hero.setPosition(Vector(150, 300)); // 主角初始位置在桥上(x=150)
// 5. 物理引擎更新主角状态:检测到主角坠落
physics.updateEntity(hero); // 主角从桥上掉下去,触发 EVENT_ENTITY_FELL 事件
// 6. 输出结果:成就系统收到通知,解锁“失足英雄”成就
// 控制台会打印:🎉 成就解锁:失足英雄!从桥上掉下去的感觉如何?
// 7.(可选)取消订阅:如果不需要成就系统了
// physics.removeObserver(&achievements);
return 0;
}
5. 扩展:添加多个观察者(音效系统、UI 系统)
观察者模式支持多个观察者同时订阅一个被观察者——比如主角坠落时,除了解锁成就,还要播放坠落音效、更新 UI 提示。我们只需新增观察者,无需修改物理引擎代码!
// 音效系统:观察者,关注坠落事件
class AudioSystem : public Observer {
public:
void onNotify(const Entity& entity, Event event) override {
if (event == EVENT_ENTITY_FELL && entity.isHero()) {
std::cout << "🔊 播放坠落音效:啊——!" << std::endl;
}
}
};
// UI 系统:观察者,关注坠落事件
class UISystem : public Observer {
public:
void onNotify(const Entity& entity, Event event) override {
if (event == EVENT_ENTITY_FELL && entity.isHero()) {
std::cout << "📱 UI 提示:主角已坠落!生命值 -10" << std::endl;
}
}
};
// 扩展后的 main 函数
int main() {
Physics physics;
Achievements achievements;
AudioSystem audioSystem; // 新增音效系统观察者
UISystem uiSystem; // 新增 UI 系统观察者
// 多个观察者订阅物理引擎
physics.addObserver(&achievements);
physics.addObserver(&audioSystem);
physics.addObserver(&uiSystem);
Entity hero(true);
hero.setPosition(Vector(150, 300));
physics.updateEntity(hero); // 触发坠落事件
// 输出结果:
// 🎉 成就解锁:失足英雄!从桥上掉下去的感觉如何?
// 🔊 播放坠落音效:啊——!
// 📱 UI 提示:主角已坠落!生命值 -10
return 0;
}
神奇之处:物理引擎只调用了一次 notify 方法,就自动通知了成就系统、音效系统、UI 系统三个观察者!而且新增观察者时,物理引擎的代码一行都不用改——这就是“开闭原则”(对扩展开放,对修改关闭)的完美体现!
三、深度优化:解决观察者模式的常见问题
关键字:链式观察者、内存管理、线程安全、顺序无关性
基础实现解决了耦合问题,但在实际项目中还有一些坑需要规避——比如动态内存分配、观察者销毁时的悬空指针、多线程环境下的安全问题。我们来一一优化!
1. 优化 1:链式观察者(避免动态数组的局限)
基础实现用固定大小的数组存储观察者,有两个缺点:① 数组大小固定,观察者数量不能超过 MAX_OBSERVERS;② 增删观察者时可能需要移动数组元素,效率低。
优化方案:用 链表 存储观察者——观察者对象本身包含“下一个观察者”的指针,形成链式结构,无需动态分配数组,支持无限个观察者(理论上)。
// 优化后的观察者接口:添加 next 指针,作为链表节点
class Observer {
friend class Subject; // 让 Subject 可以访问 next_指针
public:
virtual ~Observer() {}
virtual void onNotify(const Entity& entity, Event event) = 0;
private:
Observer* next_ = nullptr; // 指向链表中的下一个观察者
};
// 优化后的被观察者(Subject 接口实现)
class Subject {
public:
Subject() : head_(nullptr) {}
// 添加观察者:插入到链表头部(O(1)时间复杂度)
void addObserver(Observer* observer) override {
if (!observer) return;
observer->next_ = head_; // 新观察者的 next 指向当前头节点
head_ = observer; // 头节点更新为新观察者
}
// 移除观察者:遍历链表找到并删除(O(n)时间复杂度)
void removeObserver(Observer* observer) override {
if (!observer || !head_) return;
// 特殊情况:要删除的是头节点
if (head_ == observer) {
head_ = observer->next_;
observer->next_ = nullptr;
return;
}
// 遍历链表,找到要删除观察者的前一个节点
Observer* current = head_;
while (current->next_ != nullptr) {
if (current->next_ == observer) {
current->next_ = observer->next_; // 跳过要删除的节点
observer->next_ = nullptr;
return;
}
current = current->next_;
}
}
protected:
// 通知所有观察者:遍历链表
void notify(const Entity& entity, Event event) override {
Observer* current = head_;
while (current != nullptr) {
current->onNotify(entity, event);
current = current->next_; // 移动到下一个观察者
}
}
private:
Observer* head_; // 链表头节点
};
优化亮点:① 无需固定数组大小,支持任意多个观察者;② 添加观察者是 O(1)操作,效率极高;③ 无需动态分配数组,避免内存碎片;④ 缺点是移除观察者需要遍历链表(O(n)),但实际项目中“移除观察者”的场景远少于“添加”和“通知”,影响不大。
2. 优化 2:内存管理(避免悬空指针)
基础实现有个致命问题:如果观察者被销毁了,但没有从被观察者的列表中移除,被观察者通知时会调用销毁对象的 onNotify 方法——导致悬空指针错误(程序崩溃)!
错误场景:① 创建 Achievements 对象并订阅 Physics;② 销毁 Achievements 对象(比如切换场景时);③ Physics 触发事件,调用已销毁对象的 onNotify——程序崩溃!
解决方案:观察者销毁时自动取消订阅——在观察者的析构函数中调用被观察者的 removeObserver 方法:
// 改进的成就系统:析构时自动取消订阅
class Achievements : public Observer {
public:
// 构造函数:传入要订阅的被观察者
Achievements(Subject* subject) : subject_(subject) {
if (subject_) {
subject_->addObserver(this); // 构造时自动订阅
}
}
// 析构函数:自动取消订阅
~Achievements() override {
if (subject_) {
subject_->removeObserver(this); // 销毁时移除自己,避免悬空指针
}
}
// 其他方法不变 …
private:
Subject* subject_; // 保存订阅的被观察者指针
};
// 使用示例
int main() {
Physics physics;
{
// 局部作用域:Achievements 对象在作用域结束后销毁
Achievements achievements(&physics); // 构造时自动订阅
Entity hero(true);
physics.updateEntity(hero); // 正常触发成就
}
// 此时 achievements 已销毁,且自动从 physics 的观察者列表中移除
Entity hero(true);
physics.updateEntity(hero); // 不会调用已销毁的观察者,安全!
return 0;
}
初学者注意:如果一个观察者订阅了多个被观察者,需要在析构函数中逐一取消订阅。另外,被观察者销毁时,也可以发送一个“销毁通知”,让所有观察者主动取消订阅,避免观察者持有被观察者的悬空指针。
3. 优化 3:线程安全(多线程环境下的问题)
游戏通常是多线程的(比如主线程处理逻辑、渲染线程处理画面、音频线程处理音效)。如果被观察者在主线程触发通知,而观察者的 onNotify 方法中包含线程不安全的操作(比如修改共享数据),会导致数据竞争、程序崩溃!
解决方案:
- ① 通知时加锁:被观察者的 notify 方法中使用互斥锁(mutex),确保同一时间只有一个线程调用观察者的 onNotify;
- ② 异步通知:被观察者不直接调用观察者的 onNotify,而是将通知加入一个线程安全的队列,观察者在自己的线程中处理队列中的通知(避免阻塞被观察者);
- ③ 避免在 onNotify 中做耗时操作:onNotify 方法应尽量简洁,只做“接收事件、触发回调”的逻辑,耗时操作(比如加载资源、网络请求)应放到其他线程。
// 线程安全的被观察者:添加互斥锁
#include
class ThreadSafeSubject : public Subject {
protected:
void notify(const Entity& entity, Event event) override {
std::lock_guard lock(mutex_); // 加锁,自动释放
Observer* current = head_;
while (current != nullptr) {
current->onNotify(entity, event);
current = current->next_;
}
}
private:
std::mutex mutex_; // 互斥锁,保护观察者列表的访问
};
// 物理引擎继承线程安全的被观察者
class Physics : public ThreadSafeSubject {
// 其他实现不变 …
};
4. 优化 4:顺序无关性(观察者之间互不依赖)
链式观察者的通知顺序是“后订阅的先收到通知”(因为插入到链表头部)。如果观察者之间有依赖(比如 A 必须在 B 之前处理通知),会导致逻辑错误!
错误场景:① 先订阅 UI 系统,后订阅成就系统;② 通知顺序是“成就系统 → UI 系统”;③ 如果 UI 系统需要根据成就解锁状态更新显示,会出现“UI 更新在前,成就解锁在后”的错误。
解决方案:
- ① 设计时保证观察者顺序无关:观察者之间不应有依赖,每个观察者都应独立处理通知,不依赖其他观察者的处理结果;
- ② 指定通知优先级:给观察者添加优先级属性,被观察者按优先级排序后通知(比如优先级高的 UI 系统先收到通知);
- ③ 插入到链表尾部:修改 addObserver 方法,将新观察者插入到链表尾部,保证“先订阅的先收到通知”。
四、观察者模式的深度分析:适用场景与常见误区
关键字:适用场景、常见误区、与其他模式的区别、现代语言优化
1. 适用场景(新手必记)
- ✅ 一个对象的变化需要通知多个其他对象,且不知道具体有多少个对象需要通知(比如主角状态变化通知 UI、音效、成就、任务系统);
- ✅ 需要解耦发送者和接收者,让它们独立扩展(比如物理引擎和游戏玩法系统分离);
- ✅ 观察者需要根据被观察者的状态变化做出响应,而不需要主动查询(比如 UI 自动更新,不需要轮询数据);
- ✅ 多个对象需要监听同一个事件(比如多个系统监听“玩家死亡”事件)。
2. 常见误区(新手避坑)
- ❌ 过度使用观察者模式:如果只有一个观察者,或观察者和被观察者耦合度本来就很高(比如一个类的内部辅助类),没必要用观察者模式,反而增加复杂度;
- ❌ 通知传递过多数据:onNotify 的参数应尽量简洁(只传必要的信息),避免传递大对象(比如整个实体的所有数据),影响性能;
- ❌ 观察者之间有依赖:设计时应保证观察者独立,不依赖其他观察者的处理顺序或结果,否则会导致逻辑混乱、难以调试;
- ❌ 忽略内存管理:忘记在观察者销毁时取消订阅,导致悬空指针错误;
- ❌ 在 onNotify 中做耗时操作:同步通知会阻塞被观察者,导致游戏卡顿(比如在 onNotify 中加载大资源)。
3. 与其他模式的区别(避免混淆)
很多人会混淆这两个模式,核心区别在于“是否有中间件”:
① 观察者模式:被观察者直接通知观察者,是“一对一”或“一对多”的直接通信,没有中间层;
② 发布 - 订阅模式:有一个中间层(比如消息队列),发布者发布消息到队列,订阅者从队列接收消息,发布者和订阅者完全解耦,不知道对方的存在。
游戏中,观察者模式更适合“同一模块内的通信”(比如物理引擎和成就系统),发布 - 订阅模式更适合“跨模块、跨线程的通信”(比如服务器和客户端的消息同步)。
两者都用于解耦,但方向不同:
① 观察者模式:解耦“一对多”的单向通信(被观察者→观察者),观察者之间互不干扰;
② 中介者模式:解耦“多对多”的双向通信(多个对象之间互相交互),通过中介者统一协调,避免对象之间直接关联。
比如:游戏中的玩家、怪物、道具之间的交互,适合用中介者模式;而玩家状态变化通知多个系统,适合用观察者模式。
4. 现代语言的优化实现(摆脱类继承)
基础实现用“类继承”的方式,在现代语言(如 C ++11+、Java8+、Python、JavaScript)中,可以用“函数指针”“闭包”“Lambda 表达式”简化实现,摆脱繁琐的类继承!
C++ Lambda 表达式实现(更简洁)
不需要定义 Observer 接口,直接用 std::function 存储观察者的回调函数:
#include
#include
// 被观察者:用 std::function 存储回调函数(观察者)
class Subject {
public:
// 订阅:添加回调函数(Lambda 表达式或函数指针)
void addObserver(std::function<void(const Entity&, Event)> callback) {
callbacks_.push_back(callback);
}
// 通知:调用所有回调函数
void notify(const Entity& entity, Event event) {
for (auto& callback : callbacks_) {
callback(entity, event); // 直接调用回调,无需 onNotify 方法
}
}
private:
std::vector<std::function<void(const Entity&, Event)>> callbacks_;
};
// 使用示例:无需定义 Observer 子类,直接用 Lambda 订阅
int main() {
Subject physics;
int monsterKillCount = 0;
// 订阅成就系统的回调(Lambda 表达式)
physics.addObserver([&](const Entity& entity, Event event) {
if (event == EVENT_ENTITY_FELL && entity.isHero()) {
std::cout << “🎉 成就解锁:失足英雄!” << std::endl; } else if (event == EVENT_ENTITY_KILLED && !entity.isHero()) {monsterKillCount++; if (monsterKillCount >= 1000) {
std::cout << “🎉 成就解锁:怪物终结者!” << std::endl; } } }); // 订阅音效系统的回调 physics.addObserver([](const Entity& entity, Event event) {if (event == EVENT_ENTITY_FELL && entity.isHero()) {std::cout << “🔊 播放坠落音效:啊——!” << std::endl;} }); // 触发事件 Entity hero(true); physics.notify(hero, EVENT_ENTITY_FELL); // 通知所有回调 return 0; }
现代实现优势:① 代码更简洁,无需定义 Observer 接口和子类;② 灵活度更高,可以直接用 Lambda 表达式、函数指针、成员函数作为观察者;③ 减少继承层次,降低代码复杂度;④ 适合快速开发和小型模块。
五、现实复杂场景:观察者模式的高级应用
关键字:UI 状态同步、事件总线、网络同步、日志系统
1. UI 状态同步(实时更新玩家信息)
游戏 UI 需要实时显示玩家的生命值、金币、等级等状态——如果 UI 系统主动轮询这些状态,会浪费 CPU 资源;用观察者模式,玩家角色(被观察者)状态变化时主动通知 UI(观察者),高效又解耦。
// 玩家角色(被观察者)
class Player : public Subject {
public:
void setHealth(int health) {
if (health != health_) {
health_ = health;
notify(*this, EVENT_HEALTH_CHANGED); // 生命值变化,发送通知
}
}
void addGold(int gold) {
gold_ += gold;
notify(*this, EVENT_GOLD_CHANGED); // 金币变化,发送通知
}
private:
int health_ = 100;
int gold_ = 0;
};
// UI 系统(观察者)
class PlayerUI : public Observer {
public:
void onNotify(const Entity& entity, Event event) override {
const Player& player = dynamic_cast(entity);
switch (event) {
case EVENT_HEALTH_CHANGED:
std::cout << “❤️ 生命值更新:” << player.getHealth() << std::endl; // 更新 UI 血条 break; case EVENT_GOLD_CHANGED: std::cout << “💰 金币更新:” << player.getGold() << std::endl; // 更新 UI 金币显示 break; } } };
2. 事件总线(全局事件系统)
大型游戏中,多个模块之间需要跨模块通信(比如战斗模块通知任务模块“杀死 BOSS”,任务模块通知成就模块“完成主线任务”)。可以用“事件总线”(Event Bus)——一个全局的被观察者,所有模块都通过它订阅和发布事件,实现全局解耦。
// 全局事件总线(单例模式)
class EventBus : public Subject {
public:
static EventBus& getInstance() {
static EventBus instance;
return instance;
}
// 禁止拷贝和赋值
EventBus(const EventBus&) = delete;
EventBus& operator=(const EventBus&) = delete;
private:
EventBus() {}
};
// 战斗模块:发布“杀死 BOSS”事件
void BattleSystem::killBoss() {
// 发布事件到全局事件总线
EventBus::getInstance().notify(bossEntity, EVENT_BOSS_KILLED);
}
// 任务模块:订阅“杀死 BOSS”事件
class TaskSystem : public Observer {
public:
TaskSystem() {
// 订阅全局事件总线
EventBus::getInstance().addObserver(this);
}
void onNotify(const Entity& entity, Event event) override {
if (event == EVENT_BOSS_KILLED) {
std::cout << “📋 任务更新:主线任务“杀死 BOSS”完成!” << std::endl; // 通知成就系统(通过事件总线再次发布事件)EventBus::getInstance().notify(playerEntity, EVENT_MAIN_TASK_COMPLETED); } } };
优势:① 所有模块通过事件总线通信,无需互相引用;② 新增模块时,只需订阅相关事件,无需修改现有模块;③ 便于调试和维护,所有事件都通过统一的总线流转。
3. 网络同步(多人游戏的状态同步)
多人游戏中,服务器需要将玩家的操作(比如移动、攻击)同步给其他客户端。可以用观察者模式:服务器的玩家对象(被观察者)状态变化时,通知所有订阅的客户端(观察者),客户端收到通知后更新本地画面。
实现要点:① 事件需要序列化(比如转为 JSON/Protobuf),通过网络传输;② 客户端收到事件后,在主线程中处理(避免多线程问题);③ 处理网络延迟,通过插值、预测等算法优化同步效果。
4. 日志系统(记录关键事件)
游戏需要记录关键事件(比如玩家登录、充值、解锁成就),用于数据分析和问题排查。可以用观察者模式:所有模块在发生关键事件时,发布事件到日志系统(观察者),日志系统统一记录到文件或数据库。
class LogSystem : public Observer {
public:
void onNotify(const Entity& entity, Event event) override {
// 记录日志:时间 + 事件类型 + 实体信息
time_t now = time(nullptr);
char timeStr[64];
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", localtime(&now));
std::cout << "[" << timeStr << "] 事件:" << event << ",实体:" << entity.getId() << std::endl;
// 写入日志文件
logFile << "[" << timeStr << "] 事件:" << event << ",实体:" << entity.getId() << std::endl;
}
};
// 其他模块发布事件
void Player::login() {
EventBus::getInstance().notify(*this, EVENT_PLAYER_LOGIN); // 登录事件
}
void PaymentSystem::recharge(int amount) {
EventBus::getInstance().notify(playerEntity, EVENT_PLAYER_RECHARGE); // 充值事件
}
终极总结:观察者模式是游戏开发中“解耦通信”的核心模式,从简单的成就系统、UI 同步,到复杂的事件总线、网络同步,都能发挥巨大作用。它的核心思想是“订阅 - 通知”,关键是“面向接口编程”和“内存安全”。初学者要记住:不要为了用模式而用模式,只有当需要解耦“一对多”的通信时,才是观察者模式的用武之地!
“`