c++ 游戏对象互相交谈[关闭]

ggazkfy8  于 2023-11-19  发布在  其他
关注(0)|答案(7)|浏览(97)

已关闭。此问题为opinion-based。目前不接受回答。
**要改进此问题吗?**更新此问题,以便editing this post可以使用事实和引文来回答。

4年前关闭。
Improve this question
什么是处理对象并让它们彼此交谈的好方法?
到目前为止,我所有的游戏爱好/学生都很小,所以这个问题通常是以一种相当丑陋的方式解决的,这导致了紧密的集成和循环依赖。这对于我正在做的项目的规模来说很好。
然而,我的项目越来越大,越来越复杂,现在我想开始重用代码,让我的头脑变得更简单。
我遇到的主要问题通常是沿着Player的行需要知道MapEnemy也需要知道,这通常会导致设置大量指针并具有大量依赖关系,这很快就会变得一团糟。
我已经沿着消息样式系统的思路进行了思考,但我真的看不出这是如何减少依赖性的,因为我仍然会到处发送指针。
PS:我想这之前已经讨论过了,但我不知道这叫什么,只是我有需要。

nbysray5

nbysray51#

编辑:下面我描述了一个基本的事件消息传递系统,我已经使用了一遍又一遍。我突然想到,这两个学校的项目都是开源的,在网络上。你可以在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()发送到消息传递系统。
之后,我们有了回调typedef,简单地说,它需要一个接口类类型的对象,并将传递沿着一个TEventMessage指针...可选地,你可以使参数const,但我以前使用过涓流处理,用于堆栈调试等消息传递系统。
最后也是最核心的是CEventMessagingSystem对象。(或链表或队列,或者您希望存储数据的任何方式)。回调对象,上面没有显示,需要保持(并且由)一个指向对象的指针以及在该对象上调用的方法唯一定义。你在对象栈中的消息id的数组位置下添加一个条目。当你Unregister()的时候,你删除了那个条目。
基本上就是这样。现在这确实有一个规定,即一切都需要知道关于IEventMessagingSystem和TEventMessage对象的信息...但是这个对象不应该经常改变,并且只传递对被调用的事件所指示的逻辑至关重要的信息部分。这样,玩家就不需要知道Map或敌人的信息就可以直接向它发送事件。一个托管对象也可以将API调用到更大的系统,而不需要了解它。
举例来说,您可以:当敌人死亡时,你希望它播放声音效果。假设你有一个继承IEventMessagingSystem接口的声音管理器,您将为消息传递系统设置一个回调,该回调将接受TEventMessagePlaySoundEffect或类似的东西。然后,声音管理器将在启用声音效果时注册此回调(或者当你想静音所有的声音效果以方便打开/关闭功能时取消注册回调)。接下来,你将让敌人对象也从IEventMessagingSystem继承,将TEventMessagePlaySoundEffect对象放在一起(需要MSG_PlaySound作为其消息ID,然后是要播放的声音效果的ID,可以是int ID或声音效果的名称),并简单地调用Post(&oEventMessagePlaySoundEffect)。
这只是一个非常简单的设计,没有实现。(我主要在控制台游戏中使用)。如果你在多线程环境中,那么这是一种定义良好的方式,可以让运行在不同线程中的对象和系统相互通信,但您可能希望保留TEventMessage对象,以便在处理时数据可用。
另一个改变是对于只需要Post()数据的对象,您可以在IEventMessagingSystem中创建一组静态方法,这样它们就不必继承它们(这是为了方便访问和回调功能,而不是Post()调用直接需要的)。
对于所有提到MVC的人来说,这是一个非常好的模式,但是你可以用很多不同的方式和在不同的级别实现它。我目前正在专业工作的项目是一个MVC设置大约3倍,有整个应用程序的全局MVC,然后设计明智的每个MV和C也是一个自包含MVC模式。所以我在这里试图做的是解释如何使一个C足够通用,可以处理几乎任何类型的M,而不需要进入一个视图。

例如,一个对象在“死亡”时可能想要播放一个声音效果。您可以为Sound System创建一个结构体,如TEventMessageSoundEffect,它继承自TEventMessage并添加一个声音效果ID(无论是预加载的Int,还是sfx文件的名称,无论它们在您的系统中如何跟踪)然后所有对象只需要将TEventMessageSoundEffect对象与适当的Death噪声放在一起并调用Post(&oEventMessageSoundEffect); object..假设声音未静音(您希望取消注册声音管理器。
编辑:为了澄清这一点,关于下面的评论:任何发送或接收消息的对象只需要知道IEventMessagingSystem接口,这是EventMessagingSystem需要知道的所有其他对象中唯一的对象。这就是给你的分离。任何想要接收消息的对象只需注册(MSG,Object,Callback)s。然后当一个对象调用Post(MSG,Data)它通过它知道的接口将其发送到EventMessagingSystem,EMS然后将通知每个注册的事件对象。您可以执行其他系统处理的MSG_PlayerDied,或者玩家可以调用MSG_PlaySound,MSG_Respawn等来让监听这些消息的事物对它们进行操作。可以将Post(MSG,Data)看作是游戏引擎中不同系统的抽象API。
哦,我的天啊!还有一件事是向我指出的。我上面描述的系统符合另一个答案中的观察者模式。所以如果你想要一个更一般的描述,使我的更有意义一点,这是一个简短的文章,给它一个很好的描述。

mrfwxfqh

mrfwxfqh2#

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

  1. Mediator pattern
  2. Observer pattern
gr8qqesn

gr8qqesn3#

这里有一个为C++11编写的整洁的事件系统,你可以使用。它使用模板和智能指针以及代理的指针。它非常灵活。下面你还可以找到一个例子。
这些类为您提供了一种发送带有任意数据的事件的方法,以及一种直接绑定函数的简单方法,这些函数接受已转换的参数类型,系统在调用委托之前强制转换并检查正确的转换。
基本上,每个事件都派生自IEventData类(如果你愿意,你可以称之为IEvent)。你调用ProcessEvents()的每一个“帧”,在这个点上,事件系统循环通过所有的委托,并调用已经订阅每个事件类型的其他系统提供的委托。任何人都可以选择他们想要订阅的事件,因为每个事件类型都有一个唯一的ID。您还可以使用AddIdas订阅这样的事件:AddIdas(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--; 
    }
}


为了方便起见,我使用了一个EventNode类作为任何想要监听事件的类的基类。如果你从这个类派生你的监听类,并将它提供给你的事件管理器,您可以使用非常方便的OnEvent函数(..)注册你的事件。当你的派生类被销毁时,基类会自动取消订阅你的派生类的所有事件。这是非常方便的,因为当你的类被销毁时,忘记从事件管理器中删除一个委托几乎肯定会导致你的程序崩溃。
一种简单的方法来获得一个事件的唯一类型id,只需在类中声明一个静态函数,然后将它的地址转换为int。由于每个类在不同的地址上都有这个方法,它可以用于唯一标识类事件。如果你愿意,你也可以将typeObject()转换为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; 
}

x8diyxa7

x8diyxa74#

这可能不仅适用于游戏类,也适用于一般意义上的类。MVC(模型-视图-控制器)模式以及您建议的消息泵是您所需要的。
“Enemy”和“Player”可能适合MVC的Model部分,这并不重要,但经验法则是所有模型和视图都通过控制器进行交互。(比指针更好)指向这个“控制器”类的(几乎)所有其他类示例,我们将其命名为ControlDispatcher。向其添加消息泵(取决于您编码的平台),首先示例化它(在任何其他类之前,并将其他对象作为它的一部分)或最后示例化它(并将其他对象作为引用存储在ControlDispatcher中)。
当然,ControlDispatcher类可能必须进一步拆分为更专门的控制器,以保持每个文件的代码在700-800行左右(至少对我来说是限制),它甚至可能有更多的线程泵和处理消息,这取决于你的需要。

zfciruhq

zfciruhq5#

小心“消息风格的系统”,它可能依赖于实现,但通常你会放松静态类型检查,然后可能会产生一些很难调试的错误。请注意,调用对象的方法它已经是一个类似消息的系统。
也许你只是缺少了一些抽象层次,例如,对于导航,Player可以使用Navigator而不是知道Map本身的所有信息。你还说this has usually descended into setting lots of pointers,那些指针是什么?也许,你把它们给了一个错误的抽象?..让对象直接知道其他对象,而不通过接口和中间体,是获得紧耦合设计的直接方法。

e4eetjau

e4eetjau6#

消息传递绝对是一种很好的方式,但是消息传递系统可能有很多不同之处。如果你想保持你的类漂亮和干净,那么就把它们写得不知道消息传递系统,而是让它们依赖于一些简单的东西,比如“ILocationService”,然后可以实现它来发布/请求来自Map类之类的东西的信息。虽然你最终会有更多的类,它们会很小,很简单,并鼓励干净的设计。
消息传递不仅仅是解耦,它还可以让你转向一个更加异步,并发和React式的架构。Gregor Hophe的《企业集成模式》是一本谈论好的消息传递模式的好书。Erlang OTP或Scala对Actor Pattern的实现为我提供了很多指导。

tquggr8v

tquggr8v7#

@kellogs对MVC的建议是有效的,并在一些游戏中使用,尽管它在Web应用程序和框架中更常见。这可能是矫枉过正,太多了。
我会重新考虑你的设计,为什么玩家需要和敌人对话?他们不能都继承Actor类吗?为什么Actor需要和Map对话?
当我读我写的东西时,它开始适合MVC框架.
这是我开发的Asteroids的一个实现。你的游戏可能很复杂,也可能是很复杂的。

相关问题