43

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

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

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

我遇到的主要问题通常是关于Player需要了解MapEnemy,这通常会导致设置大量指针并具有大量依赖项,这很快就会变得一团糟。

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

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

4

7 回答 7

48

编辑:下面我描述了一个我反复使用的基本事件消息传递系统。我突然想到,这两个学校项目都是开源的并且是在网络上的。您可以在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,因此如果您想确保收到了您期望的消息(通常我只在调试中这样做)。

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

之后我们有我们的回调类型定义,简单地说它需要一个接口类类型的对象,并将传递一个 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,然后明智地设计每个 MV 和 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。

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

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

于 2011-01-02T20:06:24.283 回答
15

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

  1. 中介者模式
  2. 观察者模式
于 2011-01-01T12:48:35.303 回答
5

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

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

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

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

#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; 
}
于 2014-03-28T02:44:11.527 回答
4

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

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

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

干杯

于 2011-01-01T12:32:27.560 回答
0

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

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

于 2011-01-01T15:21:23.143 回答
0

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

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

于 2011-01-04T06:58:45.683 回答
-1

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

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

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

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

于 2011-01-01T12:43:32.213 回答