3

想象以下情况:

我想创造各种怪物工厂。这些怪物工厂根据struct数组提供的数据创建怪物。怪物只是在这些统计数据上有所不同,因此为每个怪物创建一个子类是矫枉过正的。

struct monster_data
{
    int HP;
    int strength;
    int speed;
    // even more attributes
};

一个类monster可以处理基于 a 的怪物的所有行为monster_data

class monster
{
    public:
        monster(monster_data* initial_stats, int length);

    void attack();
    void walk();
    void die();
    // and so forth
};

到目前为止,一切都很好。现在我有一个monster_factory基于硬编码monster_data数组创建怪物的类:

const monster_data district1_monsters[]
{
    { 500, 20,  4 }, // monster1
    { 550,  5, 12 }, // monster2
    { 420,  8, 10 }, // monster3
    { 310, 30,  7 }, // monster4
    // 100 more monsters
};

class monster_factory
{
    public:
        monster_factory(monster_data* monster_to_create) ;
        monster* create_random_monster();
};

我的问题是我必须支持几个monster_factories地区的几个,列表中的差异很小:

const monster_data district1_monsters[]
{
    { 500, 20,  4 }, // monster1
    { 550,  5, 12 }, // monster2
    { 420,  8, 10 }, // monster3
    { 310, 30,  7 }, // monster4
    // 100 more monsters
};

const monster_data district2_monsters[]
{
    { 500, 20,  4 }, // monster1
    { 750,  5, 12 }, // MONSTER2B <<
    { 420,  8, 10 }, // monster3
    { 310, 30,  7 }, // monster4
    // monsters 5 - 80 from district 1
};

const monster_data district3_monsters[]
{
    { 500, 20,  4 }, // monster1
    { 550,  5, 12 }, // monster2
    { 720, 80, 10 }, // MONSTER3B <<
    { 310, 30,  7 }, // monster4
    // monsters 8 - 90 from district 1
};

我不想复制和粘贴数组数据,而是想以某种方式继承它,因为不同版本之间的数据基本保持不变。复制整个struct数组声明只是为了有一个稍微不同的变体似乎是错误的方式。太糟糕了,第 2 区和第 3 区只是附加数据,它们修改并省略了现有条目。当然,它们也改变了不止一个怪物。

此外,1区怪物数据的变化也应适用于2区和3区。

另一个问题是有些地区的怪物数据与地区 1,2 和 3 完全无关。

const monster_data district4_monsters[]
{
    { 100, 20, 10 }, // monster 401
    { 200, 50, 20 }, // monster 402
    { 300, 40,  5 }, // monster 403
    { 400, 30, 30 }, // monster 404
    // 20 more monsters unrelated to district 1,2 & 3
};

现在的问题是:如何更改概述的设计,以monster_data避免多余的声明,并且可以添加monster_data从现有声明派生或使用全新声明的地区?

奖励积分,如果您的设计确保怪物统计列表的每个变体只能有一个工厂实例。

4

6 回答 6

2

这可以通过装饰器模式优雅地解决,方法是用每一层的变化装饰“默认”表:

class MonsterTable
{
  public:
    virtual monster_data const* getMonsterForIndex(int i)=0;
};

class DefaultMonsterTable : public MonsterTable
{
  public:

    monster_data const* getMonsterForIndex(int i)
    {
      return district1_monsters+i;
    } 
};

class OverlayMonsterTable : public MonsterTable
{
public:
  //public for brevity, should be private in real code - can also be std::map
  std::unordered_map<int, monster_data> OverlayData;

  // Init this with the "previous layer", which is always the Default table in your examples
  MonsterTable* Decorated;

  monster_data const* getMonsterForIndex(int i)
  {
    typedef std::unordered_map<VGLindex, monster_data>::const_iterator Iterator;
    Iterator Overlay=OverlayData.find(i);
    if (Overlay!=OverlayData.end()) // Monster data was changed in this layer
      return &Overlay->second;

    return Decorated->getMonsterFromIndex(i); // Defer to next layer
  } 
};

然后,您会将较高区域中的所有“更改”添加到 OverlayData,并让 OverlayMonsterTable 引用默认表 (district1)。

为了支持省略数据,您可以添加另一个重新映射索引的装饰器“层”(例如,将 [0...80] 映射到 [0...10]、[30...100]),或者集成将此功能添加到现有的 OverlayMonsterTable 中。无论哪种方式,您都拥有充分的灵活性。例如:

class OmitMonsterTable : public MonsterTable
{
public:
  int OmitBegin, OmitEnd;
  MonsterTable* Decorated;

  monster_data const* getMonsterForIndex(int i)
  {
    if (i > OmitBegin)
      i += OmitEnd;

    return Decorated->getMonsterForIndex(i);
  } 
};

你的工厂只需要一个 MonsterTable 指针/引用。

于 2012-07-18T09:56:12.513 回答
1

你一直使用“继承”这个词,但我绝对不会在这里考虑继承,你只有一种行为,即一种工厂类,你只想用不同的数据初始化工厂。

我会有一个包含所有不同monster_data值的大数组:

const monster_data all_data[] = {
  // district1_monsters
  { 500, 20,  4 }, // monster1
  { 550,  5, 12 }, // monster2
  { 420,  8, 10 }, // monster3
  { 310, 30,  7 }, // monster4
  // 100 more monsters
  // ...
  // district 2 monsters (index 104)
  { 750,  5, 12 }, // MONSTER2B <<
  // district 3 monsters (index 105)
  { 720, 80, 10 }, // MONSTER3B <<
  // district4 monsters (index 106)
  { 100, 20, 10 },
  { 200, 50, 20 },
  { 300, 40,  5 },
  { 400, 30, 30 },
  // 20 more monsters unrelated to district 1,2 & 3
  // ...
};

然后创建包含正确序列的序列:

typedef std::vector<monster_data> data_seq;

data_seq district1_data(all_data, all_data + 104);

data_seq district2_data(all_data, all_data + 80);
district2_data[2] = all_data[104];

data_seq district3_data(all_data, all_data + 3);
district3_data.push_back( all_data[105] );
district3_data.insert(district3_data.end(), all_data+8, all_data+90);

data_seq district4_data(all_data+106, all_data + 126);

然后从这些序列创建工厂:

class monster_factory
{
public:
  monster_factory(const data_seq& monsters) ;
  monster* create_random_monster();
};

monster_factory district1_factory(district1_data);
monster_factory district2_factory(district2_data);
monster_factory district3_factory(district3_data);
monster_factory district4_factory(district4_data);

如果monster_data类型只有三个整数应该没问题。如果它是一个更大的类,那么你可以制作data_seq一个vector<const monster_data*>,所以它只保存指向all_data数组元素的指针。这避免了复制monster_data对象,它们只存在于主all_data数组中,其他所有内容都通过指针引用这些主副本。这将需要更多的工作来创建向量对象,因为您需要用数组元素的地址填充它,而不是元素的简单副本,但这是您只需要在程序启动时做一次的事情,所以写更多的代码来做正确的事情是值得的:

struct address_of {
  const monster_data* operator()(const monster_data& m) const
  { return &m; }
};

// ...

typedef std::vector<const monster_data*> data_seq;

data_seq district1_data;
std::transform(all_data, all_data + 104,
               std::back_inserter(district1_data), address_of());

data_seq district2_data;
std::transform(all_data, all_data + 80,
               std::back_inserter(district2_data), address_of());
district2_data[2] = &all_data[104];

data_seq district3_data;
std::transform(all_data, all_data + 3,
               std::back_inserter(district3_data), address_of());
district3_data.push_back( all_data[105] );
std::transform(all_data+8, all_data + 90,
               std::back_inserter(district3_data), address_of());

data_seq district4_data;
std::transform(all_data+106, all_data + 126,
               std::back_inserter(district4_data), address_of());

为每个地区初始化序列的另一种可能更易于维护的方法是为每个地区设置索引数组:

int district1_indices[] = { 0, 1, 2, 3, 4, ... 103 };
int district2_indices[] = { 0, 1, 104, 3, 4, ... 79 };
int district3_indices[] = { 0, 1, 2, 105, 7, 8, 9, 10 ... 89 };
int district4_indices[] = { 106, 107, 108, 109 ... 125 };

然后用其中一个数组(及其长度)构造一个工厂。工厂可以从列表中选择一个索引,然后使用它来索引all_data以获得一个monster_data.

于 2012-07-18T09:20:40.090 回答
1

将数据存储在二进制文件中通常是不好的做法,并且无法扩展,尤其是在数据量很大的情况下。定义自己的支持简单数据继承的迷你语言,然后将其解析为包含unordered_map. 如果您需要的话,这将使您还能够实现简单的数据共享和更复杂的属性系统。

于 2012-07-18T11:34:36.387 回答
0

为了完整起见,我将在 ltjax 发布他的答案之前发布我想出的设计,尽管我的效果较差。由于它采用不同的方法,因此其他人可能会感兴趣。

它把工厂和它的桌子结合在一起,因为桌子本身没有什么意义。表格的填充是在工厂的构造函数中完成的。这样其他工厂可以继承构造函数并对表进行更改。缺点是每个工厂都会创建自己的完整表,因此在运行时存储冗余数据。至少维护变得更容易。

这可以通过移动辅助方法来改进,addreplace移动remove到单独的表类以正确封装它们。但monster_factory_abstract在这种情况下基本上是空的,IMO。

class monster_factory_abstract
{
    private:
        monster_data* table; // or map with sequential indices
        int table_length;

        protected:
        // add monster to table
        void add(int HP, int strength, int speed, etc.); 

        // index starts with one to match monster names in this example
        void replace(int index, int HP, int strength, int speed, etc.); 
        void remove(int index); // nulls an entry
        void remove(int from, int to);

    public:
        virtual monster* create_random_monster();
}

class monster_factory_district1 : public monster_factory_abstract
{
    public:
        monster_factory_district1()
        {
            table_length = 0;

            add( 500, 20,  4 ); // monster1
            add( 550,  5, 12 ); // monster2
            add( 420,  8, 10 ); // monster3
            add( 310, 30,  7 ); // monster4
            // add 100 more monsters
        }
}; 

class monster_factory_district2 : public monster_factory_district1
{
    public:
        monster_factory_district2() : monster_factory_district1
        {
            replace( 2, 750,  5, 12 ); // MONSTER2B <<
            remove(81, 100);
        }
};

class monster_factory_district3 : public monster_factory_district1
{
    public:
        monster_factory_district3() : monster_factory_district1
        {
            replace( 3, 720, 80, 10 ); // MONSTER3B <<
            remove(5, 8);
            remove(91, 100);
        }
};

class monster_factory_district4 : public monster_factory_abstract
{
    public:
        monster_factory_district4() : monster_factory_abstract
        {
            table_length = 0;

            add( 100, 20, 10 ); // monster 401
            add( 200, 50, 20 ); // monster 402
            add( 300, 40,  5 ); // monster 403
            add( 400, 30, 30 ); // monster 404
        }
};
于 2012-07-18T10:46:14.360 回答
0

一种解决方案可能是有一个包含“标准”怪物数据的基表,然后对于每个区域,您都有一个仅包含修改后怪物列表的表。

像这样的东西:

const monster_data base_monsters[] = {
    { 500, 20,  4 }, // monster1
    { 550,  5, 12 }, // monster2
    { 420,  8, 10 }, // monster3
    { 310, 30,  7 }, // monster4
    // 100 more monsters
};

struct monster_change_data
{
    int monster;               /* Index into base table */
    struct monster_data data;  /* Modified monster data */
};

const struct monster_change_data district2_monsters[] = {
    { 1, { 750,  5, 12 } }, // MONSTER2B
};

const struct monster_change_data district3_monsters[] = {
    { 2, { 720, 80, 10 } }, // MONSTER3B
};

这样你只需要列出改变的怪物。

于 2012-07-18T09:23:11.463 回答
0

当我要一个怪物时,我会在一个工厂经过该地区。然后我可以做类似的事情(仅限伪代码)

getMonster(int district)
{
    monster_data dat = getRandomBaseMonster();
    // dat has to be a copy so we don't stomp in the base data
    if (district == 2) {
        dat.HP += 10;
    }
    return dat;
}
于 2012-07-18T08:33:21.523 回答