游戏对象互相交谈

39 c++ design-patterns circular-dependency tightly-coupled-code

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

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

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

我遇到的主要问题通常是Player需要知道的问题Map,因此Enemy,这通常会导致设置大量指针并具有大量依赖关系,这很快变得混乱.

我按照消息风格系统的思路思考.但我真的不知道这是如何减少依赖性的,因为我仍然会在各处发送指针.

PS:我想这之前已经讨论过,但我不知道它的所谓只是我需要的东西.

Jam*_*mes 43

编辑:下面我描述一个我一遍又一遍使用的基本事件消息系统.在我看来,两个学校项目都是开源的,并且在网上.你可以在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)
Run Code Online (Sandbox Code Playgroud)

现在有点解释.第一个对象TEventMessage是表示消息传递系统发送的数据的基础对象.默认情况下,它总是会发送消息的Id,所以如果你想确保收到了你期望的消息(通常我只在调试中这样做).

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

之后我们得到了我们的Callback typedef,简单地说它需要一个接口类类型的对象,并将传递一个TEventMessage指针...可选你可以使参数const但我之前使用了涓流处理堆栈调试等消息传递系统.

最后和核心是CEventMessagingSystem对象.此对象包含一组回调对象堆栈(或链接列表或队列,或者您希望存储数据).上面未示出的回调对象需要维护(并由其唯一定义)指向对象的指针以及调用该对象的方法.当您注册()时,在消息ID的数组位置下的对象堆栈上添加一个条目.取消注册()时,删除该条目.

基本上就是这样.现在这确实规定所有东西都需要了解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,而不需要进入View ......

例如,一个"死"的对象可能想要播放声音效果.你可以为声音系统制作一个结构,如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.

哦! 还有一件事指向了我.我在上面描述的系统在给出的另一个答案中符合观察者模式.所以如果你想要一个更一般的描述让我的意义更有意义,那么这篇文章就是一篇很好的描述.

希望这有助于和享受!


Ste*_*and 15

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

  1. 中介模式
  2. 观察者模式


Mar*_*tin 5

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

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

基本上,每个事件都派生自 IEventData 类(如果需要,您可以将其称为 IEvent)。您调用 ProcessEvents() 的每个“框架”,此时事件系统循环遍历所有委托,并调用已订阅每种事件类型的其他系统提供的委托。任何人都可以选择他们想要订阅的事件,因为每个事件类型都有一个唯一的 ID。您还可以使用 lambda 来订阅这样的事件: 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; 
}; 
Run Code Online (Sandbox Code Playgroud)

和 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--; 
    }
}
Run Code Online (Sandbox Code Playgroud)

为方便起见,我使用 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; 
}
Run Code Online (Sandbox Code Playgroud)