处理对象并让它们相互交谈的好方法是什么?
到目前为止,我所有的游戏爱好/学生都很小,所以这个问题通常以一种相当丑陋的方式解决,导致紧密集成和循环依赖。对于我正在进行的项目规模来说这很好。
然而,我的项目在规模和复杂性方面都变得越来越大,现在我想开始重新使用代码,让我的头脑变得更简单。
我遇到的主要问题通常是Player
需要知道的Map
,而Enemy
也是如此,这通常会导致设置很多指针并且很多依赖,很快就会变得很乱。
我按照消息风格系统的思路思考。但是我真的不知道这会如何减少依赖性,因为我仍然会在任何地方发送指针。
PS:我想之前已经讨论过这个问题,但我不知道它的用途是什么。
答案 0 :(得分:43)
答案 1 :(得分:15)
用于避免紧密耦合的对象之间通信的通用解决方案:
答案 2 :(得分:4)
这可能不仅适用于游戏类,而且适用于一般意义上的类。您只需要MVC(模型 - 视图 - 控制器)模式以及您建议的消息泵。
“Enemy”和“Player”可能适合MVC的Model部分,它并不重要,但经验法则是让所有模型和视图通过控制器进行交互。因此,您希望将(几乎)所有其他类实例的引用(比指针更好)保留在这个“控制器”类中,让它命名为ControlDispatcher。添加消息泵(根据您编写的平台而有所不同),首先实例化它(在任何其他类之前并将其他对象作为其中的一部分)或最后(并将其他对象存储为ControlDispatcher中的引用)。
当然,可能必须将ControlDispatcher类进一步拆分为更专业的控制器,以保持每个文件的代码大约700-800行(这至少是我的限制),它甚至可能有更多线程根据您的需要抽取和处理消息。
干杯
答案 3 :(得分:3)
这是一个可以使用的为C ++ 11编写的简洁事件系统。它为代表使用模板和智能指针以及lambdas。它非常灵活。下面你还会找到一个例子。如果您对此有疑问,请发送电子邮件至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;
}
答案 4 :(得分:0)
小心“消息样式系统”,它可能取决于实现,但通常你会松开静态类型检查,然后可能会使一些错误很难调试。请注意,调用对象的方法已经类似于消息的系统。
可能你只是缺少一些抽象层次,例如导航,玩家可以使用导航器而不是了解地图本身。你还说this has usually descended into setting lots of pointers
,那些指针是什么?也许,你正在给他们一个错误的抽象?让对象直接了解其他人,而不需要通过接口和中间体,是获得紧密耦合设计的直接方法。
答案 5 :(得分:0)
消息传递绝对是一种很好的方式,但消息传递系统可能会有很多不同之处。如果你想让你的类保持干净整洁,那就把它们写成不知道消息传递系统,而是让它们依赖于像'ILocationService'这样简单的东西,然后可以实现它来发布/请求Map类等信息。虽然你最终会有更多的课程,但它们会变得小巧,简单并且鼓励干净的设计。
消息传递不仅仅是解耦,它还可以让您转向更加异步,并发和反应的架构。 Gregor Hophe的企业集成模式是一本很好的书,讲述了良好的消息传递模式。 Erlang OTP或Scala的Actor模式实现为我提供了很多指导。
答案 6 :(得分:-1)
@kellogs对MVC的建议是有效的,并且在一些游戏中使用,尽管它的很多在Web应用程序和框架中更常见。对此可能有点过分和过分。
我会重新考虑你的设计,为什么玩家需要和敌人交谈?难道他们都不能从Actor类继承吗?为什么演员需要与地图交谈?
当我读到我写的内容时,它开始适应MVC框架......我显然最近做了太多的rails工作。但是,我愿意打赌,他们只需要知道一些事情,比如他们正在和另一个演员碰撞,他们有一个位置,无论如何应该相对于地图。
以下是我工作的Asteroids的实现。你的游戏可能很复杂。