• 游戏设计模式系列(二)—— 适时使用观察者模式,解耦你的代码


      如果两块代码耦合,意味着你必须同时了解这两块代码。如果你让他们解耦,那么你只需要了解其一。观察者模式便是专为实现它而诞生的:“在对象间定义一种一对多的依赖关系,以便当某对象状态改变时,与它存在依赖关系的所有对象都能收到通知并自动进行更新”。大家一定都听说过一直很流行的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] 大话设计模式

     

  • 相关阅读:
    Java找N个数中最小的K个数,PriorityQueue和Arrays.sort()两种实现方法
    POJ 1661 Help Jimmy(C)动态规划
    LeetCode第8场双周赛(Java)
    Eclipse访问外部网站(比如:CSDN首页)
    LeetCode第151场周赛(Java)
    LeetCode第152场周赛(Java)
    Eclipse Block Selection(块选择)快捷键 Alt + Shift + A
    PAT(B) 1090 危险品装箱(Java)
    PAT(B) 1050 螺旋矩阵(Java:24分)
    PAT(B) 1045 快速排序(C)
  • 原文地址:https://www.cnblogs.com/SolarWings/p/6084051.html
Copyright © 2020-2023  润新知