【问题标题】:Game Objects Talking To Each Other [closed]游戏对象互相交谈[关闭]
【发布时间】:2011-06-02 05:18:09
【问题描述】:

处理对象并让它们相互交谈的好方法是什么?

到目前为止,我所有的游戏爱好/学生都很小,所以这个问题通常以一种相当丑陋的方式解决,这导致紧密集成和循环依赖。对于我正在做的项目的规模来说,这很好。

但是我的项目在规模和复杂性上变得越来越大,现在我想开始重用代码,让我的头脑变得更简单。

我遇到的主要问题通常是Player需要了解Map,Enemy也是如此,这通常归结为设置大量指针并具有大量依赖项,而这很快就变得一团糟。

我已经按照消息样式系统的思路进行了思考。但我真的看不出这如何减少依赖关系,因为我仍然会到处发送指针。

PS:我想这个之前已经讨论过,但我不知道它叫什么只是我的需要。

【问题讨论】:

    标签: c++ design-patterns circular-dependency tightly-coupled-code


    【解决方案1】:

    编辑:下面我描述了一个我反复使用的基本事件消息系统。我突然想到,这两个学校项目都是开源的并且是在网络上的。你可以在http://sourceforge.net/projects/bpfat/ 找到这个消息系统的第二个版本(还有更多)。享受吧,并阅读下面的系统更全面的描述!

    我编写了一个通用的消息传递系统,并将其引入到少数已在 PSP 上发布的游戏以及一些企业级应用软件中。消息传递系统的要点是仅传递处理消息或事件所需的数据,具体取决于您要使用的术语,以便对象不必相互了解。

    用于完成此任务的对象列表的简要说明如下:

    struct TEventMessage
    {
        int _iMessageID;
    }
    
    class IEventMessagingSystem
    {
        Post(int iMessageId);
        Post(int iMessageId, float fData);
        Post(int iMessageId, int iData);
        // ...
        Post(TMessageEvent * pMessage);
        Post(int iMessageId, void * pData);
    }
    
    typedef float(*IEventMessagingSystem::Callback)(TEventMessage * pMessage);
    
    class CEventMessagingSystem
    {
        Init       ();
        DNit       ();
        Exec       (float fElapsedTime);
    
        Post       (TEventMessage * oMessage);
    
        Register   (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback* fpMethod);
        Unregister (int iMessageId, IEventMessagingSystem* pObject, FObjectCallback * fpMethod);
    }
    
    #define MSG_Startup            (1)
    #define MSG_Shutdown           (2)
    #define MSG_PlaySound          (3)
    #define MSG_HandlePlayerInput  (4)
    #define MSG_NetworkMessage     (5)
    #define MSG_PlayerDied         (6)
    #define MSG_BeginCombat        (7)
    #define MSG_EndCombat          (8)
    

    现在有点解释。第一个对象 TEventMessage 是表示消息系统发送的数据的基础对象。默认情况下,它将始终具有正在发送的消息的 Id,因此,如果您想确保收到了您期望的消息(通常我只在调试中这样做)。

    接下来是接口类,它为消息传递系统提供一个通用对象,用于在执行回调时进行转换。此外,这还提供了一个“易于使用”的界面,用于将不同数据类型的 Post() 发送到消息传递系统。

    之后我们有了 Callback typedef,简单地说,它需要一个接口类类型的对象,并将传递一个 TEventMessage 指针......您可以选择将参数设为 const,但我之前使用过涓流处理用于诸如堆栈调试之类的消息系统。

    最后也是核心是 CEventMessagingSystem 对象。此对象包含一组回调对象堆栈(或链表或队列,或者您想要存储数据)。上面未显示的回调对象需要维护(并且由其唯一定义)指向对象的指针以及调用该对象的方法。当您 Register() 时,您在消息 id 的数组位置下的对象堆栈上添加一个条目。当您 Unregister() 时,您会删除该条目。

    基本上就是这样。现在,这确实规定了一切都需要了解有关 IEventMessagingSystem 和 TEventMessage 对象的规定……但是该对象不应该经常更改,并且只传递对被调用事件所指示的逻辑至关重要的信息部分。这样,玩家无需直接了解地图或敌人即可向其发送事件。托管对象也可以将 API 调用到更大的系统,而无需了解任何相关信息。

    例如:当敌人死亡时,您希望它播放音效。假设您有一个继承 IEventMessagingSystem 接口的声音管理器,您将为接受 TEventMessagePlaySoundEffect 或类似的消息系统设置回调。然后,声音管理器将在启用声音效果时注册此回调(或在您想要静音所有声音效果以轻松打开/关闭功能时取消注册回调)。接下来,您将让敌人对象也从 IEventMessagingSystem 继承,将 TEventMessagePlaySoundEffect 对象放在一起(需要 MSG_PlaySound 作为其消息 ID,然后是要播放的音效的 ID,无论是 int ID 还是声音的名称效果)并简单地调用 Post(&oEventMessagePlaySoundEffect)。

    现在这只是一个非常简单的设计,没有实现。如果您立即执行,则无需缓冲 TEventMessage 对象(我主要在控制台游戏中使用)。如果您处于多线程环境中,那么这是一种非常明确的方式,可以让在不同线程中运行的对象和系统相互通信,但您需要保留 TEventMessage 对象,以便在处理时数据可用。

    另一个改变是对于只需要 Post() 数据的对象,您可以在 IEventMessagingSystem 中创建一组静态方法,这样它们就不必从它们继承(用于方便访问和回调功能, Post() 调用不直接需要)。

    对于所有提到 MVC 的人来说,这是一个非常好的模式,但是您可以以多种不同的方式和不同的级别来实现它。我正在专业从事的当前项目是一个 MVC 设置大约 3 倍,有整个应用程序的全局 MVC,然后设计明智的每个 M V 和 C 也是一个自包含的 MVC 模式。所以我在这里尝试做的是解释如何制作一个足够通用的 C 来处理几乎任何类型的 M 而无需进入视图...

    例如,当一个对象“死”时,它可能想要播放声音效果。您可以为声音系统创建一个结构,例如 TEventMessageSoundEffect,它继承自 TEventMessage 并添加声音效果 ID(无论是预加载的Int 或 sfx 文件的名称,但它们在您的系统中被跟踪)。然后所有对象只需将 TEventMessageSoundEffect 对象与适当的死亡噪声放在一起并调用 Post(&oEventMessageSoundEffect);对象.. 假设声音没有静音(您想要取消注册声音管理器。

    编辑:为了澄清这一点,关于下面的评论: 任何发送或接收消息的对象只需要知道 IEventMessagingSystem 接口,这是 EventMessagingSystem 需要知道所有其他对象的唯一对象。这就是给你超脱的原因。任何想要接收消息的对象只需为其注册(MSG,对象,回调)。然后,当一个对象调用 Post(MSG,Data) 时,它会通过它知道的接口将其发送到 EventMessagingSystem,然后 EMS 将通知每个已注册的对象该事件。您可以执行其他系统处理的 MSG_PlayerDied,或者播放器可以调用 MSG_PlaySound、MSG_Respawn 等,让监听这些消息的事物对它们采取行动。将 Post(MSG,Data) 视为游戏引擎中不同系统的抽象 API。

    哦!向我指出的另一件事。我上面描述的系统在给出的其他答案中符合观察者模式。因此,如果您想要一个更笼统的描述以使我的描述更有意义,那是一篇简短的文章,可以很好地描述它。

    希望这会有所帮助并享受!

    【讨论】:

    • +1 以获得详尽的解释,但我也有一个评论:您说玩家不需要了解地图即可向其发送事件,但是您的示例意味着垂死的敌人必须知道需要通知的程序的所有其他部分。我原以为它会简单地发送“我刚刚死去”之类的消息,然后让您的消息系统通知对此事件感兴趣的听众(播放声音、更新分数等)。这样看起来任何实体都需要为单个事件(播放声音,增加分数)发送一堆消息。还是我弄错了?
    • @Groo 我无法缩短我的回复,所以我将其编辑到我上面的答案中。
    • 嗨,伙计,距离您的回答已经 5 年多了,但是当我在寻找一个简单的 pubsub 想法时,这个帖子出现了,我不得不说,我已经下载了源代码,并且除了我不习惯的编码标准以及自 2005 年以来 c++ 已经进步了一点的事实之外,代码的研究非常有趣,并且我在我的 C# 游戏中使用了一些 EMS 框架。你们三个人所做的事情看起来真的很神奇也很困难,我希望我能从中学到更多!
    • @GioraGuttsait 语言确实在发展,现在确实可以对此进行改进。我很高兴,但这些年来它仍然可以帮助人们......我在学校时所做的事情。 . 谁知道?
    • 我的意思是,我猜你所说的学校是指 uni,因为虽然代码可能更干净,但我认为游戏的规模相当大,而且那里有一些有据可查的东西(看其中你花了很多时间在那些 cmets 上哈哈)。我是 cpp 的新手,我很幸运能成为军队单位的程序员,干净和良好的代码是标准。
    【解决方案2】:

    这是一个为 C++11 编写的简洁的事件系统,您可以使用。它使用模板和智能指针以及代表的 lambda。它非常灵活。您还将在下面找到一个示例。如果您对此有任何疑问,请发送电子邮件至 info@fortmax.se。

    这些类为您提供了一种发送带有任意数据的事件的方法,以及一种直接绑定函数的简单方法,这些函数接受系统强制转换的已转换参数类型,并在调用委托之前检查转换是否正确。

    基本上,每个事件都派生自 IEventData 类(如果您愿意,可以将其称为 IEvent)。您调用 ProcessEvents() 的每个“框架”,此时事件系统循环遍历所有委托并调用已订阅每种事件类型的其他系统提供的委托。任何人都可以选择他们想要订阅的事件,因为每种事件类型都有一个唯一的 ID。您还可以使用 lambdas 订阅这样的事件:AddListener(MyEvent::ID(), [&](shared_ptr ev){ 做你的事}..

    无论如何,这里是所有实现的类:

    #pragma once
    
    #include <list>
    #include <memory>
    #include <map>
    #include <vector>
    #include <functional>
    
    class IEventData {
    public:
        typedef size_t id_t; 
        virtual id_t GetID() = 0; 
    }; 
    
    typedef std::shared_ptr<IEventData> IEventDataPtr; 
    typedef std::function<void(IEventDataPtr&)> EventDelegate; 
    
    class IEventManager {
    public:
        virtual bool AddListener(IEventData::id_t id, EventDelegate proc) = 0;
        virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) = 0; 
        virtual void QueueEvent(IEventDataPtr ev) = 0; 
        virtual void ProcessEvents() = 0; 
    }; 
    
    
    #define DECLARE_EVENT(type) \
        static IEventData::id_t ID(){ \
            return reinterpret_cast<IEventData::id_t>(&ID); \
        } \
        IEventData::id_t GetID() override { \
            return ID(); \
        }\
    
    class EventManager : public IEventManager {
    public:
        typedef std::list<EventDelegate> EventDelegateList; 
    
        ~EventManager(){
        } 
        //! Adds a listener to the event. The listener should invalidate itself when it needs to be removed. 
        virtual bool AddListener(IEventData::id_t id, EventDelegate proc) override; 
    
        //! Removes the specified delegate from the list
        virtual bool RemoveListener(IEventData::id_t id, EventDelegate proc) override; 
    
        //! Queues an event to be processed during the next update
        virtual void QueueEvent(IEventDataPtr ev) override; 
    
        //! Processes all events
        virtual void ProcessEvents() override; 
    private:
        std::list<std::shared_ptr<IEventData>> mEventQueue; 
        std::map<IEventData::id_t, EventDelegateList> mEventListeners; 
    
    }; 
    
    //! Helper class that automatically handles removal of individual event listeners registered using OnEvent() member function upon destruction of an object derived from this class. 
    class EventListener {
    public:
        //! Template function that also converts the event into the right data type before calling the event listener. 
        template<class T>
        bool OnEvent(std::function<void(std::shared_ptr<T>)> proc){
            return OnEvent(T::ID(), [&, proc](IEventDataPtr data){
                auto ev = std::dynamic_pointer_cast<T>(data); 
                if(ev) proc(ev); 
            }); 
        }
    protected:
        typedef std::pair<IEventData::id_t, EventDelegate> _EvPair; 
        EventListener(std::weak_ptr<IEventManager> mgr):_els_mEventManager(mgr){
    
        }
        virtual ~EventListener(){
            if(_els_mEventManager.expired()) return; 
            auto em = _els_mEventManager.lock(); 
            for(auto i : _els_mLocalEvents){
                em->RemoveListener(i.first, i.second); 
            }
        }
    
        bool OnEvent(IEventData::id_t id, EventDelegate proc){
            if(_els_mEventManager.expired()) return false; 
            auto em = _els_mEventManager.lock(); 
            if(em->AddListener(id, proc)){
                _els_mLocalEvents.push_back(_EvPair(id, proc)); 
            }
        }
    private:
        std::weak_ptr<IEventManager> _els_mEventManager; 
        std::vector<_EvPair>        _els_mLocalEvents; 
        //std::vector<_DynEvPair> mDynamicLocalEvents; 
    }; 
    

    还有Cpp文件:

    #include "Events.hpp"
    
    using namespace std; 
    
    bool EventManager::AddListener(IEventData::id_t id, EventDelegate proc){
        auto i = mEventListeners.find(id); 
        if(i == mEventListeners.end()){
            mEventListeners[id] = list<EventDelegate>(); 
        }
        auto &list = mEventListeners[id]; 
        for(auto i = list.begin(); i != list.end(); i++){
            EventDelegate &func = *i; 
            if(func.target<EventDelegate>() == proc.target<EventDelegate>()) 
                return false; 
        }
        list.push_back(proc); 
    }
    
    bool EventManager::RemoveListener(IEventData::id_t id, EventDelegate proc){
        auto j = mEventListeners.find(id); 
        if(j == mEventListeners.end()) return false; 
        auto &list = j->second; 
        for(auto i = list.begin(); i != list.end(); ++i){
            EventDelegate &func = *i; 
            if(func.target<EventDelegate>() == proc.target<EventDelegate>()) {
                list.erase(i); 
                return true; 
            }
        }
        return false; 
    }
    
    void EventManager::QueueEvent(IEventDataPtr ev) {
        mEventQueue.push_back(ev); 
    }
    
    void EventManager::ProcessEvents(){
        size_t count = mEventQueue.size(); 
        for(auto it = mEventQueue.begin(); it != mEventQueue.end(); ++it){
            printf("Processing event..\n"); 
            if(!count) break; 
            auto &i = *it; 
            auto listeners = mEventListeners.find(i->GetID()); 
            if(listeners != mEventListeners.end()){
                // Call listeners
                for(auto l : listeners->second){
                    l(i); 
                }
            }
            // remove event
            it = mEventQueue.erase(it); 
            count--; 
        }
    }
    

    为了方便起见,我使用 EventListener 类作为任何想要监听事件的类的基类。如果您从此类派生监听类并将其提供给您的事件管理器,您可以使用非常方便的函数 OnEvent(..) 来注册您的事件。当基类被销毁时,它会自动取消对派生类的所有事件的订阅。这非常方便,因为当您的类被销毁时忘记从事件管理器中删除委托几乎肯定会导致您的程序崩溃。

    通过简单地在类中声明一个静态函数,然后将其地址转换为 int 来获取事件的唯一类型 id 的一种简洁方法。由于每个类在不同的地址上都有这个方法,它可以用于类事件的唯一标识。如果需要,您还可以将 typename() 转换为 int 以获得唯一的 id。有不同的方法可以做到这一点。

    所以这里有一个如何使用它的例子:

    #include <functional>
    #include <memory>
    #include <stdio.h>
    #include <list>
    #include <map>
    
    #include "Events.hpp"
    #include "Events.cpp"
    
    using namespace std; 
    
    class DisplayTextEvent : public IEventData {
    public:
        DECLARE_EVENT(DisplayTextEvent); 
    
        DisplayTextEvent(const string &text){
            mStr = text; 
        }
        ~DisplayTextEvent(){
            printf("Deleted event data\n"); 
        }
        const string &GetText(){
            return mStr; 
        }
    private:
        string mStr; 
    }; 
    
    class Emitter { 
    public:
        Emitter(shared_ptr<IEventManager> em){
            mEmgr = em; 
        }
        void EmitEvent(){
            mEmgr->QueueEvent(shared_ptr<IEventData>(
                new DisplayTextEvent("Hello World!"))); 
        }
    private:
        shared_ptr<IEventManager> mEmgr; 
    }; 
    
    class Receiver : public EventListener{
    public:
        Receiver(shared_ptr<IEventManager> em) : EventListener(em){
            mEmgr = em; 
    
            OnEvent<DisplayTextEvent>([&](shared_ptr<DisplayTextEvent> data){
                printf("It's working: %s\n", data->GetText().c_str()); 
            }); 
        }
        ~Receiver(){
            mEmgr->RemoveListener(DisplayTextEvent::ID(), std::bind(&Receiver::OnExampleEvent, this, placeholders::_1)); 
        }
        void OnExampleEvent(IEventDataPtr &data){
            auto ev = dynamic_pointer_cast<DisplayTextEvent>(data); 
            if(!ev) return; 
            printf("Received event: %s\n", ev->GetText().c_str()); 
        }
    private:
        shared_ptr<IEventManager> mEmgr; 
    }; 
    
    int main(){
        auto emgr = shared_ptr<IEventManager>(new EventManager()); 
    
    
        Emitter emit(emgr); 
        {
            Receiver receive(emgr); 
    
            emit.EmitEvent(); 
            emgr->ProcessEvents(); 
        }
        emit.EmitEvent(); 
        emgr->ProcessEvents(); 
        emgr = 0; 
    
        return 0; 
    }
    

    【讨论】:

      【解决方案3】:

      避免紧密耦合的对象之间通信的通用解决方案:

      1. Mediator pattern
      2. Observer pattern

      【讨论】:

      • 中介者模式就在 MVC 中(控制器是中介者)。 +1 观察者模式。在某些平台上大量使用。
      • 嗯.. 从你链接的文章看,Relationship Manager 乍一看有点臭,好像是神物。它应该是某种单例,它知道每个人的一切。文章展示了单个对象(Customer.AddOrder、Customer.RemoveOrder)的成员方法,它们将它们的内部暴露给“管理器”,然后允许管理器为它们完成工作。 OOP 去了哪里?此外,为了测试向客户添加单个订单,您应该模拟整个经理类。我希望你只保留前两个链接。
      • 你说得很好。我删除了链接;-)。
      【解决方案4】:

      消息传递绝对是一种不错的方式,但消息传递系统可能有很多不同之处。如果您想让您的类保持整洁,请将它们编写为对消息传递系统一无所知,而是让它们依赖于诸如“ILocationService”之类的简单事物,然后可以将其实现为从诸如 Map 类之类的事物中发布/请求信息.虽然您最终会得到更多类,但它们会更小、更简单并鼓励简洁的设计。

      消息传递不仅仅是解耦,它还可以让您转向更加异步、并发和反应式的架构。 Gregor Hophe 的《企业集成模式》是一本很棒的书,它讨论了良好的消息传递模式。 Erlang OTP 或 Scala 的 Actor Pattern 实现为我提供了很多指导。

      【讨论】:

        【解决方案5】:

        小心“消息样式系统”,它可能取决于实现,但通常你会放松静态类型检查,然后会使一些错误很难调试。请注意,调用对象的方法已经是一个类似消息的系统。

        您可能只是缺少一些抽象级别,例如,对于导航,玩家可以使用导航器而不是了解地图本身的所有信息。你也说this has usually descended into setting lots of pointers,那些指针是什么?可能,您将它们赋予错误的抽象?.. 让对象直接了解其他对象,而不通过接口和中间体,是获得紧密耦合设计的直接方法。

        【讨论】:

        • 是的,我一直在直接分配它们,我想这是我的问题。
        【解决方案6】:

        @kellogs 对 MVC 的建议是有效的,并在一些游戏中使用,尽管它在网络应用程序和框架中非常更常见。这可能有点矫枉过正,而且太过分了。

        我会重新考虑你的设计,为什么玩家需要与敌人交谈?他们不能都继承自一个 Actor 类吗?为什么 Actor 需要与地图对话?

        当我阅读我所写的内容时,它开始适合 MVC 框架......我显然最近做了太多的 Rails 工作。但是,我愿意打赌,他们只需要知道他们正在与另一个 Actor 发生碰撞,并且他们有一个位置,无论如何都应该相对于地图。

        这是我工作的Asteroids 的实现。你的游戏可能很复杂,也很可能很复杂。

        【讨论】:

        • 玩家和敌人需要知道地图才能导航,这只是一个粗略的简化示例。
        【解决方案7】:

        这可能不仅适用于游戏类,也适用于一般意义上的类。 MVC(模型-视图-控制器)模式以及您建议的消息泵就是您所需要的。

        “Enemy”和“Player”可能会适合 MVC 的 Model 部分,这并不重要,但经验法则是让所有模型和视图通过控制器进行交互。因此,您可能希望保留对(几乎)来自此“控制器”类的所有其他类实例的引用(比指针更好),我们将其命名为 ControlDispatcher。向它添加一个消息泵(取决于您正在编码的平台),首先实例化它(在任何其他类之前并让其他对象成为它的一部分)或最后(并将其他对象作为引用存储在 ControlDispatcher 中)。

        当然,为了将每个文件的代码保持在 700-800 行左右(这至少对我来说是限制),它可能必须进一步分解为更专业的控制器,它甚至可能有更多线程根据您的需要抽取和处理消息。

        干杯

        【讨论】:

        • +1 没有必要重新发明东西,我同意。
        猜你喜欢
        • 2016-01-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多