3

我正在用 C++ 编写一个类似流氓的游戏,并且遇到双重调度问题。

   class MapObject {
        virtual void collide(MapObject& that) {};
        virtual void collide(Player& that) {};
        virtual void collide(Wall& that) {};
        virtual void collide(Monster& that) {};
    };

然后在派生类中:

void Wall::collide(Player &that) {
    that.collide(*this);
}

void Player::collide(Wall &that) {
    if (that.get_position() == this->get_position()){
        this->move_back();
    }
}

然后我尝试使用代码:

vector<vector<vector<shared_ptr<MapObject>>>> &cells = ...

创建单元格的位置如下:

objs.push_back(make_shared<Monster>(pnt{x, y}, hp, damage)); //and other derived types
...
cells[pos.y][pos.x].push_back(objs[i]);

当我尝试碰撞播放器和墙壁时:

cells[i][j][z]->collide(*cells[i][j][z+1]);

玩家与基类发生碰撞,但没有与墙发生碰撞。我究竟做错了什么?

4

3 回答 3

3

这比仅仅解决您的问题更复杂。您正在执行手动双重调度,并且您有错误。我们可以修复您的错误。

但是您遇到的问题不是您的错误,而是您正在执行手动双重调度的事实。

手动双重调度容易出错。

每次添加新类型时,都必须编写 O(N) 新代码,其中 N 是现有类型的数量。这段代码是基于复制粘贴的,如果你犯了错误,他们会默默地继续错误发送一些极端情况。

如果您继续进行手动双重调度,那么无论何时您或其他任何人修改代码,您都会继续遇到错误。

C++ 不提供自己的双重调度机制。但是使用我们可以自动编写它


这是一个需要线性工作来管理双重调度的系统,加上每次碰撞的工作。

对于双分派中的每种类型,您添加一个类型到pMapType. 就是这样,调度的其余部分是为您自动编写的。X然后从继承您的新地图类型collide_dispatcher<X>

如果你想让两种类型有冲突代码,写一个自由函数 do_collide(A&,B&)。变体中更容易的一个pMapType应该是A. 此函数必须在两者之前定义,AB为调度工作而定义。

a.collide(b)如果运行或运行,则该代码将b.collide(a)运行,其中A和分别是和B的动态类型。ab

您也可以结交do_collide一种或另一种类型的朋友。

无需再费周折:

struct Player;
struct Wall;
struct Monster;

using pMapType = std::variant<Player*, Wall*, Monster*>;

namespace helper {
  template<std::size_t I, class T, class V>
  constexpr std::size_t index_in_variant() {
    if constexpr (std::is_same<T, std::variant_alternative_t<I, V>>{})
      return I;
    else
      return index_in_variant<I+1, T, V>();
  }
}
template<class T, class V>
constexpr std::size_t index_in_variant() {
  return helper::index_in_variant<0, T, V>();
}

template<class Lhs, class Rhs>
constexpr bool type_order() {
  return index_in_variant<Lhs*, pMapType>() < index_in_variant<Rhs*, pMapType>();
}

template<class Lhs, class Rhs>
void do_collide( Lhs&, Rhs& ) {
  std::cout << "Nothing happens\n";
}

struct MapObject;
template<class D, class Base=MapObject>
struct collide_dispatcher;

struct MapObject {
  virtual void collide( MapObject& ) = 0;

protected:
  template<class D, class Base>
  friend struct collide_dispatcher;
  virtual void collide_from( pMapType ) = 0;

  virtual ~MapObject() {}
};

template<class D, class Base>
struct collide_dispatcher:Base {
  D* self() { return static_cast<D*>(this); }
  virtual void collide( MapObject& o ) final override {
    o.collide_from( self() );
  }
  virtual void collide_from( std::variant<Player*, Wall*, Monster*> o_var ) final override {
    std::visit( [&](auto* o){
      using O = std::decay_t< decltype(*o) >;
      if constexpr( type_order<D,O>() ) {
        do_collide( *self(), *o );
      } else {
        do_collide( *o, *self() );
      }
    }, o_var );
  }
};

void do_collide( Player& lhs, Wall& rhs );
void do_collide( Player& lhs, Monster& rhs );
struct Player : collide_dispatcher<Player> {
  friend void do_collide( Player& lhs, Wall& rhs ) {
    std::cout << "Player hit a Wall\n";
  }
  friend void do_collide( Player& lhs, Monster& rhs ) {
    std::cout << "Player fought a Monster\n";
  }
};

void do_collide( Wall& lhs, Monster& rhs );
struct Wall : collide_dispatcher<Wall> {
  friend void do_collide( Wall& lhs, Monster& rhs ) {
    std::cout << "Wall blocked a Monster\n";
  }
};
void do_collide( Monster& lhs, Monster& rhs );
struct Monster : collide_dispatcher<Monster> {
  friend void do_collide( Monster& lhs, Monster& rhs ) {
    std::cout << "Monster Match!\n";
  }
};

活生生的例子

虽然这里的管道很复杂,但这确实意味着您不需要手动进行任何双重调度。您只是在编写端点。这减少了可能出现极端情况拼写错误的地方的数量。

测试代码:

int main() {
  MapObject* pPlayer = new Player();
  MapObject* pWall = new Wall();
  MapObject* pMonster = new Monster();

  std::cout << "Player:\n";
  pPlayer->collide(*pPlayer);
  pPlayer->collide(*pWall);
  pPlayer->collide(*pMonster);

  std::cout << "Wall:\n";
  pWall->collide(*pPlayer);
  pWall->collide(*pWall);
  pWall->collide(*pMonster);

  std::cout << "Monster:\n";
  pMonster->collide(*pPlayer);
  pMonster->collide(*pWall);
  pMonster->collide(*pMonster);
}

输出是:

Player:
Nothing happens
Player hit a Wall
Player fought a Monster

Wall:
Player hit a Wall
Nothing happens
Wall blocked a Monster

Monster:
Player fought a Monster
Wall blocked a Monster
Monster Match!

您还可以创建一个中央 typedefstd::variant<Player*, Wall*, Monster*>map_type_index使用该中央 typedef 来确定其顺序,从而减少向双调度系统添加新类型的工作,以在单个位置添加类型、实现新类型并转发声明应该做某事的碰撞代码。

更重要的是,这种双重调度代码可以使继承友好;派生类型 fromWall可以调度到Wall重载。如果你想要这个,你必须使collide_dispatcher方法重载为非final,允许SpecialWall重新重载它们。

这是,但每个主要编译器的当前版本现在都支持它需要的东西。一切都可以在甚至中完成,但它变得更加冗长并且可能需要

虽然需要线性数量的代码来定义发生的事情,但编译器将生成二次数量的代码或静态表数据来实现双重调度。所以在你的双调度表中有 10,000 多种类型之前要小心。


如果您想MapObject具体一点,请将接口从中分离出来并final从调度程序中删除并添加MapObjectpMapType

struct Player;
struct Wall;
struct Monster;
struct MapObject;

using pMapType = std::variant<MapObject*, Player*, Wall*, Monster*>;

namespace helper {
  template<std::size_t I, class T, class V>
  constexpr std::size_t index_in_variant() {
    if constexpr (std::is_same<T, std::variant_alternative_t<I, V>>{})
      return I;
    else
      return index_in_variant<I+1, T, V>();
  }
}
template<class T, class V>
constexpr std::size_t index_in_variant() {
  return helper::index_in_variant<0, T, V>();
}

template<class Lhs, class Rhs>
constexpr bool type_order() {
  return index_in_variant<Lhs*, pMapType>() < index_in_variant<Rhs*, pMapType>();
}

template<class Lhs, class Rhs>
void do_collide( Lhs&, Rhs& ) {
  std::cout << "Nothing happens\n";
}

struct collide_interface;
template<class D, class Base=collide_interface>
struct collide_dispatcher;

struct collide_interface {
  virtual void collide( collide_interface& ) = 0;

protected:
  template<class D, class Base>
  friend struct collide_dispatcher;
  virtual void collide_from( pMapType ) = 0;

  virtual ~collide_interface() {}
};

template<class D, class Base>
struct collide_dispatcher:Base {
  D* self() { return static_cast<D*>(this); }
  virtual void collide( collide_interface& o ) override {
    o.collide_from( self() );
  }
  virtual void collide_from( pMapType o_var ) override {
    std::visit( [&](auto* o){
      using O = std::decay_t< decltype(*o) >;
      if constexpr( type_order<D,O>() ) {
        do_collide( *self(), *o );
      } else {
        do_collide( *o, *self() );
      }
    }, o_var );
  }
};

struct MapObject:collide_dispatcher<MapObject>
{
    /* nothing */
};

活生生的例子

因为你想PlayerMapObject你的后裔,你必须使用的Base论点collide_dispatcher

void do_collide( Player& lhs, Wall& rhs );
void do_collide( Player& lhs, Monster& rhs );
struct Player : collide_dispatcher<Player, MapObject> {
  friend void do_collide( Player& lhs, Wall& rhs ) {
    std::cout << "Player hit a Wall\n";
  }
  friend void do_collide( Player& lhs, Monster& rhs ) {
    std::cout << "Player fought a Monster\n";
  }
};

void do_collide( Wall& lhs, Monster& rhs );
struct Wall : collide_dispatcher<Wall, MapObject> {
  friend void do_collide( Wall& lhs, Monster& rhs ) {
    std::cout << "Wall blocked a Monster\n";
  }
};
void do_collide( Monster& lhs, Monster& rhs );
struct Monster : collide_dispatcher<Monster, MapObject> {
  friend void do_collide( Monster& lhs, Monster& rhs ) {
    std::cout << "Monster Match!\n";
  }
};
于 2018-05-24T13:54:55.493 回答
2

你的基类应该是:

class MapObject {
public:
    virtual ~MapObject () = default;
    virtual void collide(MapObject& that) = 0; // Dispatcher

    virtual void collide(Player& that) = 0;  // Both types known
    virtual void collide(Wall& that) = 0;    // Both types known
    virtual void collide(Monster& that) = 0; // Both types known
};

你的 Player 类应该是这样的:

class Player {
public:
    void collide(MapObject& that) override { that.collide(*this); } // dispatch done,
                                                        // call true virtual collision code

    // Real collision code, both types are known
    void collide(Player& that) override  { PlayerPlayerCollision(*this, that);}
    void collide(Wall& that) override    { PlayerWallCollision(*this, that);}
    void collide(Monster& that) override { MonterPlayerCollision(that, this);}
};

如果您的基类不是抽象的并且意味着有自己的冲突,那么您必须重命名以区分调度程序和真正的冲突代码:

class MapObject {
public:
    virtual ~MapObject () = default;
    virtual void collide_dispatcher(MapObject& that) { that.collide(*this); }

    // Real collision code, both types are known
    virtual void collide(MapObject& that) { MapObjectMapObjectCollision(*this, that);}
    virtual void collide(Player& that)    { MapObjectPlayerCollision(*this, that);}
    virtual void collide(Wall& that)      { MapObjectWallCollision(*this, that); }
    virtual void collide(Monster& that)   { MapObjectMonsterCollision(*this, that); }
};

你的 Player 类应该是这样的:

class Player {
public:
    virtual void collide_dispatcher(MapObject& that) { that.collide(*this); }

    // Real collision code, both types are known
    void collide(MapObject& that) override { MapObjectPlayerCollision(that, *this);}
    void collide(Player& that) override    { PlayerPlayerCollision(*this, that);}
    void collide(Wall& that) override      { PlayerWallCollision(*this, that);}
    void collide(Monster& that) override   { MonterPlayerCollision(that, this);}
};
于 2018-05-24T13:00:09.267 回答
-1

看起来像小错误。

if (that.get_position() == this->get_position())

这从来都不是真的。更重要的是,我认为您不需要它,但这不是问题。我认为你需要改变这条线

cells[i][j][z]->collide(*cells[i][j][z+1]);

进入

player[i][j][z+1]->collide(*cells[i][j][z+1]);

玩家会撞墙。

于 2018-05-24T12:57:54.177 回答