如果两块代码耦合,意味着你必须同时了解这两块代码。如果你让他们解耦,那么你只需要了解其一。观察者模式便是专为实现它而诞生的:“在对象间定义一种一对多的依赖关系,以便当某对象状态改变时,与它存在依赖关系的所有对象都能收到通知并自动进行更新”。大家一定都听说过一直很流行的MVC框架,其底层就是观察者模式。观察者模式应用十分广泛,在游戏中善用观察者模式,可以让你的代码更加“纯净”。
假设你现在在维护一个横版动作游戏,主角的任务就是一路砍杀,最终虐杀BOSS。杀敌的路上充满了坎坷,为了对玩家更加友好,策划要求主角在打破一个木桶时,在UI界面上给出一句提示“打破所有木桶即可开启密道”。类似的需求可能有很多,战斗系统的各种特殊状况都可能需要UI界面的配合,但是我们真的要直接把UI显示的代码加入战斗系统吗?答案是否定的,如果我们这样做显然会增加UI系统和战斗系统的耦合,下次再去修改一个UI的时候,你很可能就要去先去看看为什么战场里的怪物会追着你不放了。这时候观察者模式就派上用场了,它使得战场能够发出一个消息,并通知对消息感兴趣的对象,而不用关心是谁收到了通知。
大致是下面这个样子:
void MoveableObject::OnAttack(AttackParameters& stAttackParameters)
{
CalAttack(stAttackParameters);
if (m_iMyHp <= 0 && m_iType == OBJECT_TYPE::WOOD)
{
notify(stAttackParameters.m_pFighter, EVENT_WOOD_DEAD);
}
}
当木桶被打破的时候就发出一个消息,m_pFighter打破了一只木桶。UI系统只需要注册为战斗系统的观察者,它便可以收到这条消息。
观察者模式的通常实现:
1.观察者接口
class Observer
{
public:
virtual ~Observer() {}
virtual void onNotify(const Entity& entity, Event event) = 0;
};
那么UI系统可以这么实现
class UIManager : public Observer
{
public:
virtual void onNotify(const Entity& entity, Event event)
{
switch (event)
{
case EVENT_WOOD_DEAD:
if (entity.isMyPlayer())
{
ShowTipsUI("WOOD_DEAD");
}
break;
// Handle other events
}
}
private:
void ShowTipsUI(std::string tipsContent)
{
//TODO
}
};
2.被观察者接口
class Subject
{
private:
Observer* observers_[MAX_OBSERVERS];
int numObservers_;
public:
//修改观察者列表的公有函数
void addObserver(Observer* observer)
{
// Add to array...
}
void removeObserver(Observer* observer)
{
// Remove from array...
}
protected:
//发送通知
void notify(const Entity& entity, Event event)
{
for (int i = 0; i < numObservers_; i++)
{
observers_[i]->onNotify(entity, event);
}
}
};
那么战斗系统对应的就是调用上面提到的函数,并且发送notify给UI系统了。当然,在这之前,UI系统首先要使用addObserver注册为战斗系统的观察者
class MoveableObject : public Subject
{
public:
void OnAttack(AttackParameters& stAttackParameters);
};
完美,现在我们可以欢乐地写代码了,想要分工合作很方便,我可以直接告诉旁边的同事,只要监听EVENT_WOOD_DEAD事件,并且显示一个提示的UI。但是还是要说说它的缺点,那就是需要注意对象生命周期的管理!!假如在某种情况下,比如重启游戏。我们先关闭了UI系统,但是却没有告诉战斗系统,那么战斗系统中就出现了野指针,对C++来说,崩溃只是一瞬间的事。所以我们一定要事先明确观察者和被观察者的生命周期,如果不能保证观察者在被观察者之后释放,就一定要在释放观察者的时候调removeObserver先删除被观察者中对观察者的引用。实际上我们的游戏已经因为类似的野指针崩溃过好多次了!
说到这里,有些敏感的同学应该会发现,这和事件系统很像。《游戏编程模式》这本书中也提到了观察者系统和事件系统的区别,关于那句话我还带有一些疑惑。在我的理解中,事件系统其实就是观察者模式的一种升级版的应用,它不再由被观察者维护观察者的列表,转而使用一个公共的类来做这些。当我需要抛出一个事件的时候,我只需调用EventManager::dispatchEvent(Event* pstEvent)即可。关心某个事件的系统,也只需要调用EventManager::addEventListener(const std::string& szEventType, ...)即可监听对应的事件,比起需要实现各种接口来说,不管是简洁性还是灵活性都有很大的提升。
基于观察者模式,以及观察者模式衍生出来的各种系统,我们可以把程序的不同模块分离。还记得我第一次理解我们项目结构时的那种兴奋,大概的形式如下:
可以看到,核心战斗逻辑既不依赖引擎,也不会和业务逻辑代码纠缠不清,他们之间通过事件交互。这也就意味着,即使更换引擎、使用新的业务逻辑和UI,我们的代码一样可以被复用,这听起来真的令人振奋!!这是一种技术的积累,你会看到一块代码越变越好,越来越优雅完善,这块代码在将来的某一天可能会成为一个人、或一个公司的核心竞争力。
参考资料:
[1] 游戏编程模式
[2] 大话设计模式