c++ - 面向组件的系统中的灵活数据消息传递

标签 c++ visual-c++ components game-engine generic-programming

我正在为正在开发的小游戏创建面向组件的系统。基本结构如下:游戏中的每个对象都由一个“GameEntity”组成;一个容器,其中包含一个指向“Component”类中项目的指针的 vector 。

组件和实体通过在组件的父GameEntity类中调用send方法来相互通信。 send方法是一个具有两个参数的模板,一个命令(Command(一个枚举,包括诸如STEP_TIME之类的指令)和一个通用类型为“T”的数据参数)。 send函数遍历Component * vector 并调用每个组件的接收消息,由于模板的使用,该消息方便地调用了对应于数据类型T的重载接收方法。

然而,问题出在哪里(或者不便之处)是Component类是纯虚函数,并且将始终被扩展。由于不允许模板函数虚拟化的实际限制,我将不得不在头中为每个可以想象的组件使用的每种数据类型声明一个虚拟接收函数。这不是很灵活,也不是可扩展的,而且至少对我而言,这似乎违反了不重复代码的OO编程思想。

所以我的问题是,如何修改下面提供的代码存根,以使面向组件的对象结构尽可能灵活,而无需使用违反最佳编码实践的方法

这是每个类的相关头文件存根,以及使用扩展组件类以何种方式为我的问题提供背景的示例:

游戏实体类:

class Component;

class GameEntity
{

public: 
GameEntity(string entityName, int entityID, int layer);

~GameEntity(void){};

//Adds a pointer to a component to the components vector.
void addComponent (Component* component);

void removeComponent(Component*);

    //A template to allow values of any type to be passed to components
template<typename T>
void send(Component::Command command,T value){
       //Iterates through the vector, calling the receive method for each component
    for(std::vector<Component*>::iterator it =components.begin();  it!=components.end();it++){
        (*it)->receive(command,value);
    }
}
private:
     vector <Component*> components;    

};

组件类别:
#include“GameEntity.h”
类组件
{
public:
static enum Command{STEP_TIME, TOGGLE_ANTI_ALIAS, REPLACE_SPRITE};

Component(GameEntity* parent)
    {this->compParent=parent;};

virtual ~Component (void){};    

GameEntity* parent(){
    return compParent;
}
void setParent(GameEntity* parent){
    this->compParent=parent;
}

virtual void receive(Command command,int value)=0;
virtual void receive(Command command,string value)=0;
virtual void receive(Command command,double value)=0;
virtual void receive(Command command,Sprite value)=0;
    //ETC. For each and every data type


private:
GameEntity* compParent;

};

Component类的可能扩展:
#include "Sprite.h"
#include "Component.h"
class GraphicsComponent: Component{
    public:
          GraphicsComponent(Sprite sprite, string name, GameEntity* parent);
          virtual void receive(Command command, Sprite value){
                 switch(command){
                      case REPLACE_SPRITE: this->sprite=value; break
                 }
           }

    private:
          Spite sprite;


}

我应该使用空指针并将其转换为适当的类型吗?这可能是可行的,因为在大多数情况下,可以从命令中知道类型,但又不是很灵活。

最佳答案

这是类型擦除的完美案例!

当基于模板的通用编程和面向对象的编程发生冲突时,您会遇到一个简单但很难解决的问题:如何以安全的方式存储一个我不在乎类型而是在乎如何存储的变量我可以用吗?通用编程往往导致类型信息的激增,因为面向对象的编程取决于非常特定的类型。程序员该做什么?

在这种情况下,最简单的解决方案是使用某种具有固定大小的容器,可以存储任何变量,然后安全地检索/查询其类型。幸运的是,boost具有以下类型:boost::any

现在您只需要一个虚函数:

virtual void receive(Command command,boost::any val)=0;

每个组件都“知道”发送的内容,因此可以提取值,如下所示:
virtual void receive(Command command, boost::any val)
{
// I take an int!
    int foo = any_cast<int>(val);
}

这将成功转换int或引发异常。不喜欢异常(exception)吗?首先进行测试:
virtual void receive(Command command, boost::any val)
{
// Am I an int?
    if( val.type() == typeid(int) )
    {
        int foo = any_cast<int>(val);
    }
}

“但是哦!”您可能会说,急于不喜欢这种解决方案,“我想发送多个参数!”
virtual void receive(Command command, boost::any val)
{
    if( val.type() == typeid(std::tuple<double, char, std::string>) )
    {
        auto foo = any_cast< std::tuple<double, char, std::string> >(val);
    }
}

“好吧”,您可能会思考:“如何允许传递任意类型,就像我想一次 float 而又一次int一样?”对此,先生,您会被打败,因为那是一个坏主意。而是将两个入口点 bundle 到同一内部对象:
// Inside Object A
virtual void receive(Command command, boost::any val)
{
    if( val.type() == typeid(std::tuple<double, char, std::string>) )
    {
        auto foo = any_cast< std::tuple<double, char, std::string> >(val);
        this->internalObject->CallWithDoubleCharString(foo);
    }
}

// Inside Object B
virtual void receive(Command command, boost::any val)
{
    if( val.type() == typeid(std::tuple<float, customtype, std::string>) )
    {
        auto foo = any_cast< std::tuple<float, customtype, std::string> >(val);
        this->internalObject->CallWithFloatAndStuff(foo);
    }
}

那里有。通过使用boost::any删除类型中令人讨厌的“有趣”部分,我们现在可以安全可靠地传递参数。

有关类型擦除的更多信息,以及有关在不需要的对象上擦除类型部分的好处的信息,以便它们与通用编程更好地结合,请参见this article

如果您喜欢字符串操作,则另一个想法是:
// Inside Object A
virtual void receive(Command command, unsigned int argc, std::string argv)
{
   // Use [boost::program_options][2] or similar to break argv into argc arguments
   //    Left as exercise for the reader
}

这具有奇特的优雅。程序以相同的方式解析它们的参数,因此您可以将数据消息化概念化为正在运行的“子程序”,然后打开大量的隐喻,这样可能会导致有趣的优化,例如将部分数据分拆出去消息传递等

但是,成本很高:与简单的强制转换相比,字符串操作可能会非常昂贵。另请注意,boost::any不会以零成本提供;与仅传递固定数量的参数所需的零查找相比,每个any_cast都需要RTTI查找。灵活性和间接性需要成本;在这种情况下,这是值得的。

如果您希望完全避免任何此类费用,则有一种可能性会获得必要的灵活性,并且不存在依赖关系,甚至是一种更可口的语法。但是,尽管这是一个标准功能,但可能非常不安全:
// Inside Object A
virtual void receive(Command command, unsigned int argc, ...)
{
   va_list args;
   va_start ( args, argc );

   your_type var = va_arg ( args, your_type );
   // etc

   va_end( args );
}

例如,在printf中使用的可变参数功能允许您传递任意多个参数。显然,您将需要告诉被调用函数传递了多少个参数,所以这是通过argc提供的。但是请记住,被调用方函数无法判断是否传递了正确的参数。它会很乐意接受您提供的所有内容,并将其解释为正确的。因此,如果您不小心传递了错误的信息,将没有编译时支持来帮助您找出问题所在。垃圾进垃圾出。

另外,还有一些关于va_list的事情要记住,例如所有浮点数都被上转换为double,结构是通过指针传递的(我认为),但是如果您的代码正确且精确,那么就不会有问题,您将有效率,缺乏依赖关系且易于使用。对于大多数用途,我建议将va_list等包装到宏中:
#define GET_DATAMESSAGE_ONE(ret, type) \
    do { va_list args; va_start(args,argc); ret = va_args(args,type); } \
    while(0)

然后是两个参数的版本,然后是三个的版本。遗憾的是,此处无法使用模板或内联解决方案,但是大多数数据包的参数不得超过1-5个,而且大多数参数都是原语(几乎可以肯定,尽管您的用例可能有所不同),所以设计一些丑陋的宏可以帮助您的用户解决大部分不安全的问题。

我不建议您采用这种策略,但是在某些平台上,它可能是最快,最简单的策略,例如甚至不允许编译时间相关的平台或不允许进行虚拟调用的嵌入式系统。

关于c++ - 面向组件的系统中的灵活数据消息传递,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15929769/

相关文章:

delphi - 为 TCustomControl Delphi 捕获/创建 OnGetFocus/OnLostFocus 事件

c++ - 如何在鼠标点处找到 mfc 控件的句柄但位于其他控件下?

c++ - 知道用户是否选择了子菜单项 MFC

java - Java 中的连通分量标记

C++11 是否所有的控制路径都需要返回值?

vb.net - 如何根据平台加载Dll

reactjs - 检测SVG(React组件)路径元素的左侧或右侧部分被单击

c++ - 为什么 std::copy_n 采用模板参数而不是 std::size_t?

c++ - 使用无效 IP 创建 IPv6 套接字失败 - 似乎被截断了

c++ - 即使库排序正确,命令行也缺少 DSO