游戏编程模式--观察者模式

观察者模式

  定义:在对象间定义一种一对多的关系,以便在某对象发生改变时,与它存在依赖关系的所有对象都能收到通知并自动进行更新。

  观察者模式的使用非常的广泛,我们熟知的MVC框架的底层就是观察者模式,java甚至直接把它集成到系统库中(java.util.Observer),c#更是直接将它集成在了语言层面(event关键字)。

解锁成就系统

  在现代的游戏中通常都会有一个成就系统,当你完成某个任务的时候,会解锁相应的成就。例如:“杀死100个恶魔”,“从桥上掉下”等。但要实现这么一个优雅的成就系统是比较棘手的,设想”从桥上掉下“这个任务,如果我们把它写在物理系统的碰撞检测中是可以工作的,但同时我们也引入了一个很强的耦合,这个耦合会让我们的代码显得丑陋而且不利于后期的更新和维护。这个时候我们就需要观察者模式了。

  那观察者模式是如何工作的了?简单的来说,它就是发生变化的对象发送一个消息通知所有对这个消息感兴趣的对象,不用关心具体是谁,而接收这个消息的对象根据消息的内容自动更新自己。一段简陋但有效的代码可以说明这个情况:

void  Physics::updateEntity(Entity& entity)
{
    bool wasOnSurface = entity.isOnSurface();
    entity.accelerate(GRAVITY);
    entity.update();
    if(wasOnSurface && !entity.isOnSurface())
    {
        notify(entity,EVENT_START_FALL);
    }
}

  在这里,当对象开始下落时会发送一个“EVENT_START_FALL"的消息,但系统并不关心谁会接到这个消息以及这个消息的处理细节。成就系统需要注册为”EVENT_START_FALL"消息的接收者,当物体掉落时,成就系统就会接收到消息,然后播放解锁成就的烟花动画。而这一切与物理系统完全解耦。

  那观察者模式具体时如何实现的?让我们直接从代码开始:

观察者

class Observer
{
public:
    virtual ~Observer() {}
    virtual void onNotify(const Entity& entity, Event event)=0;
};

  Observer是观察者类,用于接收消息。onNotify由具体的观察者实现,比如在成就系统中,我们可以定义:

class Achievements:public Observer
{
public:
    virtual void onNotify(const Entity& entity, Event event)
    {
        switch(event)
        {
            case EVENT_ENTITY_FELL:
                if(entity.isHero() && heroIsOnBridge_)
                {
                    unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
                }
                break;

                //handle other event...
        }
    }
private:
    void unlock(Achievement achievement)
    {
        //unlock achievement
    }
};

被观察者

class Subject
{
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);
        }
    }
private:
    Observer* observers_[MAX_OBSERVERS];
    int numObservers_;
};

  被观察者维护了一个观察者列表,同时定义了三个接口,分别是:添加观察者、删除观察者、通知观察者。添加和删除接口可以让外部的代码控制谁可以接收通知,而观察者列表则可以让多个观察者可以接收到同一个通知,而不会隐式的耦合在一起。

可被观察的物理模块

  如上所述,如果我们想要一个可被观察的物理系统,则只需要如下实现:

class Physics:public Subject
{
public:
    void updateEntity(Entity& entity)
    {
        //other stuff...
        //fall event handle
        if(!entity.isOnSurface())
        {
            notify(entity,EVENT_ENTITY_FELL);
        }
    }
}

  把notify方法声明为受保护的方法,则物理系统可以调用它而外部代码不能,当某一个事件发生时,notify方法会逐个通知观察者对象。

顾虑

  •   性能考虑。很多的游戏开发者会顾虑观察者模式太慢了而避免使用观察者模式,甚至可以说他们对设计模式就有一个默认的假设——设计模式会涉及大量的类并且会引入一些间接和其他形式的CPU时钟消耗。
  •   同步处理问题。在notity方法中,我们会逐个的调用观察者的onNotify的方法,这个过程极容易产生一个问题,即某一个观察者onNotify方法阻塞了,导致被观察者也被阻塞。在实践中,可能这个问题表现的可能没那么糟糕,但你必须考虑这个事情。对于一些很慢的操作,可以让它们在另一个工作线程或工作队列中执行,同时你要很小心的处理线程和显式锁,避免死锁的情况出现。
  •   动态内存分配问题。在上述例子中,我们使用了一个固定长度的数组来存储观察者,这样显得不够灵活,而且无法应对超过最大数量限制的观察者数量的情况,在实践中可能会使用vector等会动态分配内存的集合,但动态内存分配对某些人来说又是不能接受的,这种情况可以推荐一中做法——使用链表存储观察者。链表的节点存储观察者对象的指针,同时搭配链表节点池可以避免动态内存的分配(你可以自己分配一个固定大小的链表节点池,复用已删除的节点)。

余下的问题

  观察者模式虽然简单、快速,而且可以和内存管理很紧密的结合,但和所有的设计模式一样,它不是万能的,即使你准确且高效的实现了它,有时候也不是正确的解决方案。设计模式会遭人诟病,大部分是由于使用一个好的设计模式去处理错误的问题,所以事情会变得很糟糕。

  对于观察者模式,还有两个问题:一个是技术性问题,一个是可维护级别。首先我们看技术性的问题。

销毁观察者和被观察者

  在例子中,被观察者维护一个观察者列表,如果不小心删除的其中的一个观察者对象,观察者列表中的指针就会指向一个已被删除内存的地址,这个时候如果被观察者先这个指针发送消息,程序行为就会变得不可预料。而对于删除被观察者对象则会相对容易一些,因为观察者没有持有被观察者的引用。但如果不处理这种情况,也容易导致问题,因为被观察者删除之后,观察者也就不再是观察者了,但观察者们却不知道,它们还自以为是。这里对于销毁的情况由几种推荐的做法:

  1)销毁被观察者

    一种做法是被观察者持有观察者列表,所以可在析构函数中告知观察者取消注册自己;另一种则是被观察者被删除时,在析构函数发送一个“死亡消息”,让观察者自己处理这个消息即可。

  2)销毁观察者

    在观察者中维护一个被观察者的引用,然后在析构函数中调用被观察者的removeObserver的方法。

  很多现在的编程语言都有GC机制,很多人可能会觉得就不用对象的销毁了,一切由系统托管。但仔细想一想,现代的GC机制回收的时那些没有被其他对象引用的对象,假设我们的使用观察者模式注册了消息接收功能,这个时候被观察者即持有了一个观察者的引用,如果我们在某一个场景中把某个对象移除了,但并没有注销观察者(即被观察者中还持有引用),这个时候观察者不会被GC,还是会不断的接收消息,消耗CPU的时钟,比如角色转到下一个场景后,如果不注销本场景的UI界面元素的观察者,则这些UI元素还是会不断的接收的角色发送的消息,而且如果处理过程中还在做其它的事情,比如播放音乐,就会发生明显的错误问题。

  这个就是通知系统中常见的问题:失效的观察者。对于这个问题,我们学到的经验就是及时的删除观察者。

调试困难

  观察者模式很好的把两处代码进行了解耦,使我们能更专注的解决当前模块的问题。但同时也引入了一个问题,就是如果代码中有很多的bug,需要我们调试的时候,梳理其中的信息流将会变得异常的困难。通过一个显式的耦合,我们很容易理清方法调用的逻辑,而对于观察者模式,观察者的添加和删除时动态的行为,这使得我们不得不去梳理这些动态的、命令式的行为。

  对于这个问题的处理也非常的简单,如果你需要经常去理解程序的逻辑而去了解模块间的调用顺序,那就不要用观察者模式,而是用其它更好的方法。

  在现代程序中,会涉及很多的模块,我们通常使用“关注点分离”、“内聚和耦合”、“模块化”的手段把不想关的功能模块分离。观察者模式非常适用于不相关的模块间通信,不适合单个紧凑的模块内部的通信。

  

原文地址:https://www.cnblogs.com/xin-lover/p/10434846.html

时间: 2024-10-30 10:40:50

游戏编程模式--观察者模式的相关文章

Game Programming Patterns(游戏编程模式)

Game Programming Patterns(游戏编程模式) 大部分游戏开发者在他们游戏项目上总是一个巨大的挑战,总是东拼西凑,修修补补.很多游戏项目常常以失败告终,或者是被淹没在复杂而繁琐的代码中.如何解决这些问题? 各位看官,不管你是对游戏开发感兴趣,或者正在饱受代码不断增长带来的灾难,这本书将是你们的福音! 这本Game Programming Patterns 是由Bob Nystrom(一位在EA待过7年,有着20年游戏开发经历的工程师编写).本书将告诉你,什么模式能够帮你理清和

【游戏设计模式】之四 《游戏编程模式》读书笔记:全书内容梗概总结

本系列文章由@浅墨_毛星云 出品,转载请注明出处.   文章链接:http://blog.csdn.net/poem_qianmo/article/details/53240330 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 本文的Github版本:QianMo/Reading-Notes/<游戏编程模式>读书笔记 这是一篇超过万字读书笔记,总结了<游戏编程模式>一书中所有章节与内容的知识梗概. 我们知道,游戏行业其实一直很缺一本系

Game Programming Patterns(游戏编程模式)-架构,性能与游戏

游戏编程模式- 架构,性能与游戏 本系列博客是:Game Programming Patterns 的中文翻译版本. 翻译的github地址: cyh24. 如有兴趣,可联系博主共同翻译,一起造(wu)福(dao)他人. 博客虽然水分很足,但是也算是博主的苦劳了, 如需转载,请附上本文链接,不甚感激! 本系列博客 <游戏编程模式>– 目录,可点击进入. 架构,性能与游戏 ============================ 在我们埋头研究一堆的设计模式之前,我想先告诉你,对于软件架构,我个

Game Programming Patterns(游戏编程模式)-简介

游戏编程模式-简介 本系列博客是:Game Programming Patterns 的中文翻译版本. 翻译的github地址: cyh24. 如有兴趣,可联系博主共同翻译,一起造(wu)福(dao)他人. 博客虽然水分很足,但是也算是博主的苦劳了, 如需转载,请附上本文链接http://blog.csdn.net/cyh_24/article/details/46868419,不甚感激! 本系列博客 目录,可点击进入. 简介 ============================ 在我五年级

《游戏编程模式》(8)

<游戏编程模式>最后一篇,刚从英国玩了一圈,春节又要到啦 Chapter 19 对象池 使用固定的对象池重用对象,取代单独地分配和释放对象,达到提升性能和优化内存使用的目的. 使用情境: 频繁创建销毁对象: 对象大小基本一致: 堆上分配内存较慢或可能产生内存碎片: 粒子类: 用union节省内存:粒子使用时用live结构体,不使用时用next指针 1 class Particle 2 { 3 4 public: 5 Particle() 6 : framesLeft_(0) 7 {} 8 9

《游戏编程模式》记录

写在前面 这本书长这样 我还没有看过“GOF”,我所读到的设计模式都是这本书(游戏角度)给出的定义,害怕GOF中的定义过于抽象. 没有在项目代码晃来晃去经历的,或者没有工作至少半年的,不用着急买这本书,因为估计看不懂. 本文用来重点记录“我觉得XX设计模式是什么”,以及“当我在看XX设计模式时,我在想什么” 这本书的写作方式,属于我喜欢的“谈话口吻” 命令模式 书面定义:“将一个请求封装成一个对象,从而允许你使用不同的请求.队列或日志将客户端参数化,同时支持请求操作的撤销和恢复” 他的举例:按键

游戏编程模式-事件队列

“对消息或事件的发送与受理进行事件上的解耦.” 动机 如果你曾从事过用户界面编程,那肯定对“事件”不陌生了.每当你在界面中点击一个按钮或下拉菜单,系统都会生成一个事件,系统会把这个事件抛给你的应用程序,你的任务就是获取到这些事件并将其与你自定义的行为关联起来.那么为了获取到这些事件,你的代码通常都会由个事件循环.就像这样: while(running) { Event e = pollEvent(); //handle event } 可以看到,这段代码会不断的获取事件,然后处理.但如果期间再事

游戏编程模式--单例模式

单例模式 定义:确保一个类只有一个实例,并为其提供一个全局的访问入口. 那么什么情况下使用单例?最常见的情况就是一个类需要与一个维持自身状态的外部系统进行交互,比如说打印机.大多数情况下都是多人共用一个打印机,这意味着可能由多个人同时向这个打印机发送打印任务,这个时候管理打印机的类就必须熟悉打印机的当前状态并协调这些任务的执行.这个时候就不允许存在多个打印机的实例,因为实例无法知道其他的实例所做的操作,也就无法进行整体的管理. 我们先看看最常见的单例的实现方式: class FileSystem

游戏编程模式-对象池

“使用固定的对象池重用对象,取代单独的分配和释放对象,以此来达到提升性能和优化内存使用的目的.” 动机 假设我们正在致力于游戏的视觉效果优化.当英雄释放魔法时,我们想让一个火花在屏幕上炸裂.这通常需要一个粒子系统(一个用来生成大量小的图形并在它们生存周期产生动画的引擎)来实现.而这个粒子系统实现这个火花的时候会产生大量的粒子,我们需要非常快速的创建这些粒子同时在这些粒子“死亡”的时候释放这些粒子对象.在这里,我们会碰到一个严重的问题——内存碎片化. 碎片化地害处 为游戏和移动设备编程在很多方面都