我目前正在将游戏引擎作为大学模块的一部分组合在一起,其中一部分是消息处理系统。我希望改进讲师的实施,所以对我可能的变化提出任何建议或批评都是最受欢迎的。
当前消息结构
struct Message
{
Entity* entity;
std::string message;
void* data;
Message(Entity* entity, std::string message, void* data):
entity(entity), message(message), data(data) {}
};
初始实现只将消息发送给实体,但我计划使用“接口”允许游戏引擎的任何组件能够接收消息。
class IMessageReceiver
{
public:
virtual void handleMessage(const Message& message) {}
};
struct Message
{
IMessageReceiver* receiver;
std::string message;
void* data;
};
//examples
class Entity : public IMessageReceiver { };
class Game : public IMessageReceiver { };
我对当前系统的一个问题是数据的void *(我只是不喜欢使用void *)。然后将其转换为它需要在handleMessage函数内部的类型,因为我知道它应该接收哪些数据(取决于消息字符串) - 通常它至少是3D矢量或实体,但是如果我有它发送消息到实体以外的东西然后这可能会改变。
我想将其更改为使用模板;但是,我不确定我该怎么做。
template <typename T>
struct Message
{
IMessageReceiver* receiver;
std::string message;
T* data;
};
我在基础层面上对模板有点熟悉,但我可能缺乏对模板技巧的深入了解。我知道我可以在创建新消息时将其传递给数据类型。
//example
Entity* entity;
vec3 someVec;
Message<vec3> message(entity, "Fire", someVec);
MessageHandler::sendMessage(message);
但是,我如何在IMessageReceiver中编写handleMessage函数声明,因为不同的实现类在发送给它们的消息中会有不同的数据?
virtual void handleMessage(const Message<?>& message) {}
我想过让IMessageReceiver成为一个模板类,所以当一个类继承它时,他们会设置他们收到的消息的数据类型。
template <typename T>
class IMessageReceiver
{
public:
virtual void handleMessage(const Message<T>& message) {}
};
class Entity : IMessageReceiver<vec3> {};
然而,这意味着消息接收者只能接收一种类型的信息,但是可能存在我可能不得不将不同类型的数据发送到同一接收者的情况 - 例如,实体的派生类通常会收到vec3作为消息的数据部分,但新场景要求它接收另一个实体。 我发现的另一个问题是,只有抽象基类Entity继承自IMessageReceiver(不是每个派生的Entity类,并且它们接收不同类型的不同信息 - 或者可能),这意味着使Entity成为模板类,所以这可以通过传递它来设置。但我仍然会有这种不灵活的系统,其中某些东西只能接收一种数据的消息。
也许void *是'最好'的方法,也许使用接口类不是。我不知道。
请随意提出您能想到的任何建设性批评。我不是要求某人为我写这篇文章,我只是在寻求一些实现我正在尝试的方法的建议。基本上,我希望能够使引擎的任何组件能够接收消息;消息中发送的数据未预定义;并且最好不使用void *。
感谢您抽出宝贵时间阅读本文。如果您需要任何其他信息,请告诉我。
加文
答案 0 :(得分:0)
虚函数需要在编译时知道确切的签名,因此您无法直接使用“模板技巧”来实现您的目标。
查看Boost.Any代替void* Message::data
。它仍然需要一系列的转换才能将其转换为正确的类型,但它是以类型安全的方式完成的。此外,这种消息“拥有”数据,因此您不必担心生命周期问题。
此外,请考虑使用IMessageReceiver
,而不是std::function<void(const Message&)>
。这样,您可以将消息传递给任何具有void operator()(const Message&)
的对象,并且您不必担心层次结构的复杂性。
boost::any
和std::function
内部都使用了一种称为 type erasure 的技术,它将继承从接口中取出,并允许您将事物视为值,这往往会使代码更容易推理。
答案 1 :(得分:0)
您的模式表明发送方知道接收方并将消息直接发送到单个接收方,因此您可以让发送方直接调用接收方的方法,并且该方法可能会被“消息”特定参数重载(其中还解决了哪个对象“拥有”动态分配的消息数据的问题。)
创建消息传递系统而不是直接调用目标上的方法的一个常见原因是减少对象之间的耦合。观察者或发布/订阅模式通过使消息的发送者(大多数)不知道消息的接收者来减少耦合。在这种情况下,您需要一种方法来抽象消息细节,这似乎是问题所在。
您可以拥有消息的虚拟基类,并从中派生特定于消息的类型。然后handleMessage
方法可以获取指向基类的指针,然后使用dynamic_cast
将其向下转换为具体的消息类型。这比传递void*
和执行任意转换更加略微,因为您至少会进行一些运行时类型检查。
另一种选择是在IMessageReceiver
接口中有多个重载(都具有无操作实现),并让消息发送者直接调用特定方法。这为您提供了编译时类型检查,但只要您发明新的“消息”类型,就需要重新编译。 (我将 message 放在引号中,因为在此方案中,您不再需要消息对象。)