游戏对象互相交谈
处理物体并让它们相互交谈的好方法是什么?
到目前为止,我的所有游戏爱好/学生都很小,所以这个问题通常是以相当难看的方式解决的,这导致了tight integration
和circular dependencies
。 对于我正在做的项目的规模来说,这是很好的。
但是我的项目在规模和复杂性方面都变得越来越大,现在我想开始重新使用代码,并让我的头变得更简单。
我所遇到的主要问题一般是根据Player
需要了解的Map
, Enemy
也是如此,这通常会降低到设置很多的指针,并且有很多的依赖关系,这很快就会变成一团糟。
我曾经思考过一个消息风格系统。 但我真的不知道这是如何减less依赖,因为我仍然会发送指针到处。
谢谢。
PS:我猜这是以前讨论过的,但是我不知道它叫做什么,只是我有这个需要。
编辑:下面我描述了一个基本的事件消息系统,我一遍又一遍地使用。而且这两个学校的项目都是开源的和在网上发生的。 你可以在http://sourceforge.net/projects/bpfat/find这个消息传递系统的第二个版本(还有更多)。享受,阅读下面的系统更多的描述!;
我写了一个通用的消息系统,并将其引入到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被发送,所以如果你想确保你已经收到了你期望的消息(通常我只在debugging时这么做)。
接下来是Interface类,为消息传递系统提供一个通用对象,用于在执行callback时进行投射。 另外,这也为Post()提供了一个“易于使用”的界面,将不同的数据types传递给消息系统。
之后,我们有我们的callbacktypedef,简单地说,它期望一个接口类的types的对象,并将传递一个TEventMessage指针…或者,你可以使参数const,但我曾经使用涓stream处理之前,像堆栈debugging和这样的消息传递系统。
最后,核心是CEventMessagingSystem对象。 此对象包含一个callback对象堆栈(或链接列表或队列,或者您想要存储数据)的数组。 上面未显示的callback对象需要维护(并由其唯一定义)指向对象的指针以及调用该对象的方法。 当您注册()时,您在消息ID的数组位置下的对象堆栈上添加一个条目。 当你取消注册()你删除该条目。
这基本上是..现在这确实有一个规定,一切都需要知道的IEventMessagingSystem和TEventMessage对象…但这个对象不应该经常改变,只传递信息的重要部分的逻辑口述由事件被调用。 这样玩家不需要直接知道地图或敌人发送的事件。 一个被pipe理的对象也可以调用一个API到一个更大的系统,而不需要知道任何事情。
例如:当一个敌人死亡时,你想让它发挥音效。 假设你有一个可以inheritanceIEventMessagingSystem接口的声音pipe理器,你可以设置一个消息系统的callback函数来接受一个TEventMessagePlaySoundEffect或者类似的东西。 声音pipe理器会在启用声音效果时注册此callback(或者当您想要静音所有声音效果以便轻松开启/closures时,取消注册callback)。 接下来,你将有敌方对象也从IEventMessagingSysteminheritance,放在一起TEventMessagePlaySoundEffect对象(需要MSG_PlaySound为其消息ID,然后声音效果的ID播放,无论是一个int ID或声音的名称效果),只需调用Post(&oEventMessagePlaySoundEffect)。
现在这只是一个非常简单的devise,没有实现。 如果你有立即执行,那么你不需要缓冲TEventMessage对象(我主要用在控制台游戏中)。 如果您处于multithreading环境中,那么这是一个在单独的线程中运行的对象和系统彼此交谈的非常明确的方式,但是您将需要保留TEventMessage对象,以便在处理时可以使用数据。
另一个变化是对于只需要Post()数据的对象,你可以在IEventMessagingSystem中创build一组静态方法,所以它们不必从它们inheritance(这是为了方便访问和callback能力,而不是直接使用 – Post()调用需要)。
对于所有提到MVC的人来说,这是一个非常好的模式,但是你可以以不同的方式和层次来实现它。 目前我正在专业工作的项目是一个约3倍的MVC设置,有整个应用程序的全球MVC,然后devise明智的每个MV和C也是一个自包含的MVC模式..所以我已经试图做这里是解释如何使一个C是足够通用的,以处理任何types的M,无需进入一个视图…
例如,一个对象在“死亡”时可能需要播放声音效果。您可以为声音系统创build一个结构,例如从TEventMessageinheritance的TEventMessageSoundEffect,并添加声音效果ID(无论是预加载的Int,还是sfx文件的名称,但是它们在系统中被跟踪)。 然后,所有的对象只需要把一个TEventMessageSoundEffect对象与适当的死亡噪声放在一起,并调用Post(&oEventMessageSoundEffect); 对象..假设声音没有被静音(你想要取消注册声音pipe理器。
编辑:澄清这一点关于下面的评论:发送或接收消息的任何对象只需要知道IEventMessagingSystem接口,这是EventMessagingSystem需要知道的所有其他对象的唯一对象。 这是什么让你的分离。 任何想要接收消息的对象只需注册(MSG,Object,Callback)就可以了。 然后,当一个对象调用Post(MSG,Data)时,它通过它所知道的接口将它发送给EventMessagingSystem,然后EMS会通知事件的每个注册对象。 你可以做其他系统处理的MSG_PlayerDied,或者玩家可以调用MSG_PlaySound,MSG_Respawn等来让这些消息监听它们。 将Post(MSG,Data)视为一个抽象的API,用于游戏引擎中的不同系统。
哦! 还有一件事是我指出的。我上面描述的系统在给出的其他答案中符合Observer模式。 所以,如果你想要一个更概括的描述,使我更有意义,这是一个短文,给它一个很好的描述。
希望这有助于和享受!
对象之间通信的通用解决scheme避免紧密耦合:
- 调解员模式
- 观察者模式
这可能不仅适用于游戏类,而且适用于一般意义上的类。 MVC(模型 – 视图 – 控制器)模式以及您build议的消息泵就是您所需要的。
“敌人”和“玩家”很可能适合MVC的模型部分,没什么关系,但是经验法则是所有的模型和视图都通过控制器进行交互。 所以,你会希望从这个“控制器”类中引用(几乎)所有其他类实例的引用(比指针更好),我们将其命名为ControlDispatcher。 为它添加一个消息泵(根据你编码的平台不同而不同),首先实例化(在任何其他类之前,其他对象是其中的一部分)或者最后(在ControlDispatcher中存储其他对象作为引用)。
当然,ControlDispatcher类可能需要进一步分解成更专用的控制器,以保持每个文件的代码大约在700-800行(这对我来说至less是一个限制),甚至可能有更多的线程抽取和处理消息取决于您的需求。
干杯
这里有一个为C ++ 11编写的整洁的事件系统,你可以使用。 它使用模板和智能指针以及代表的lambdaexpression式。 这非常灵活。 下面你还会find一个例子。 如果您对此有任何疑问,请发送邮件至info@fortmax.se。
这些类为您提供了一种发送附有任意数据的事件的方法,以及一种简单的方法来直接绑定函数,这些函数接受已转换的参数types,然后在调用委托之前转换并检查正确的转换。
基本上,每个事件都是从IEventData类派生的(如果你愿意的话,你可以称它为IEvent)。 您调用ProcessEvents()的每个“框架”,此时事件系统将循环遍历所有委托,并调用已订阅每个事件types的其他系统提供的委托。 任何人都可以select他们想要订阅的事件,因为每个事件types都有唯一的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类作为任何想要监听事件的类的基类。 如果你从这个类派生你的监听类,并提供给你的事件pipe理器,你可以使用非常方便的函数OnEvent(..)来注册你的事件。 基类将在所有事件被销毁时自动取消订阅派生类。 这是非常方便的,因为当你的类被销毁时忘记从事件pipe理器中移除一个委托几乎肯定会导致你的程序崩溃。
通过简单地在类中声明一个静态函数,然后将其地址转换为一个int来获得唯一的typesid。 由于每个class级在不同的地址上都有这种方法,因此可以用于class级活动的唯一标识。 如果需要,您还可以将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; }
注意“消息types系统”,它可能取决于实现,但通常你会松开静态types检查,然后可能会使一些错误很难debugging。 请注意,调用对象的方法已经是一个消息类的系统。
也许你只是缺less一些抽象层次,例如导航一个播放器可以使用导航器,而不是全部了解地图本身。 你也可以这样说, this has usually descended into setting lots of pointers
,这些指针是什么? 也许,你是在给他们一个错误的抽象?让对象直接了解他人,而不需要通过接口和中间体,是一种直接获得紧密耦合devise的方法。
消息传递绝对是一个很好的方法,但消息传递系统可能有很多不同之处。 如果你想保持你的类很好干净,写他们对消息系统一无所知,而是让他们依赖像'ILocationService'这样简单的东西,然后可以实现发布/请求像Map类的信息。 虽然你会得到更多的课程,他们会变小,简单,鼓励干净的devise。
消息不仅仅是解耦,它还可以让你走向一个更加asynchronous,并发和反应的架构。 Gregor Hophe的企业集成模式是一本讨论良好消息模式的好书。 Erlang OTP或者Scala的Actor模式的实现为我提供了很多的指导。
@kellogs对MVC的build议是有效的,并且在一些游戏中使用,尽pipe它在networking应用程序和框架中更常见。 这可能是矫枉过正,太多了。
我会重新考虑你的devise,为什么玩家需要和敌人交谈? 他们不能从一个Actor类inheritance吗? 为什么演员需要和地图交谈?
当我读到我写的东西时,就开始融入到MVC框架中了……我明显做了太多的rails工作。 然而,我愿意打赌,他们只需要知道,他们正在与另一个演员相撞,他们有一个位置,这应该是相对于地图反正。
这是我工作的小行星的实现。 你的游戏可能是,也可能是复杂的。