2

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

组件和实体通过调用组件的父 GameEntity 类中的 send 方法相互通信。send 方法是一个模板,它有两个参数,一个 Command(它是一个枚举,包括 STEP_TIME 等指令)和一个泛型类型“T”的数据参数。send 函数循环遍历 Component* 向量并调用每个组件的接收消息,由于模板使用方便地调用对应数据类型 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;


}

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

4

1 回答 1

4

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

当基于模板的泛型编程和面向对象的编程发生冲突时,你会遇到一个简单但难以解决的问题:我如何以安全的方式存储一个变量,而我不关心类型而是关心如何我可以用吗?泛型编程往往会导致类型信息的爆炸式增长,而面向对象的编程依赖于非常特定的类型。程序员要做什么?

在这种情况下,最简单的解决方案是某种具有固定大小的容器,可以存储任何变量,并安全地检索它/查询它的类型。幸运的是, 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,或引发异常。不喜欢例外?先做一个测试:

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);
    }
}

“好吧”,您可能会思考,“我如何允许传递任意类型,就像我想要一次浮点数和另一次整数一样?” 对此,先生,你会被打败,因为那是个坏主意。相反,将两个入口点捆绑到同一个内部对象:

// 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 删除类型中讨厌的“有趣”部分,我们现在可以安全可靠地传递参数。

有关类型擦除的更多信息,以及擦除对象上不需要的类型部分的好处,以便它们与泛型编程更好地结合,请参阅这篇文章

如果你喜欢字符串操作,另一个想法是:

// 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,还有很多需要记住的事情,例如所有浮点数都上转换为双精度,结构体通过指针传递(我认为),但如果你的代码是正确和精确的,就不会有问题,你会有效率,缺乏依赖,易于使用。对于大多数用途,我建议将 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 个,而且大部分都是原语(几乎可以肯定,尽管您的用例可能不同),所以设计一些帮助您的用户的丑陋宏将主要处理不安全方面。

我不推荐这种策略,但它很可能是某些平台上最快和最简单的策略,例如甚至不允许编译时依赖的平台或嵌入式系统,其中可能不允许虚拟调用。

于 2014-01-26T19:00:56.453 回答