Двойная отправка в C ++ не работает - PullRequest
0 голосов
/ 24 мая 2018

Я пишу мошенническую игру на 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]);

Игрок сталкиваетсяс базовым классом, но не со стеной.Что я делаю не так?

Ответы [ 3 ]

0 голосов
/ 24 мая 2018

Ваш базовый класс должен быть:

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);}
};
0 голосов
/ 24 мая 2018

Это сложнее, чем просто решить вашу проблему.Вы делаете двойную ручную отправку, и у вас есть ошибки.Мы можем исправить ваши ошибки.

Но проблема у вас не в ваших ошибках, а в том, что вы делаете ручную двойную отправку.

Ручная двойная отправка подвержена ошибкам.

Каждый раз, когда вы добавляете новый тип, вы должны написать O (N) новый код, где N - количество существующих типов.Этот код основан на копировании и вставке, и если вы делаете ошибки, они молча продолжают неправильно отправлять некоторые угловые случаи.

Если вы продолжите выполнять двойную ручную диспетчеризацию, вы будете иметь ошибки каждый раз, когда вы или кто-либо другойelse изменяет код.

C ++ не предоставляет своего собственного механизма двойной отправки.Но с помощью мы можем автоматизировать написание этого


Вот система, которая требует линейной работы для управления двойной диспетчеризацией плюс работа для каждого столкновения.

Для каждого типа в двойной отправке вы добавляете тип к pMapType.Вот и все, остальная часть рассылки написана для вас автоматически.Затем наследуйте ваш новый тип карты X от collide_dispatcher<X>.

Если вы хотите, чтобы два типа имели код столкновения, напишите free function do_collide(A&,B&).Проще в варианте pMapType должно быть A.Эта функция должна быть определена до того, как A и B определены для работы диспетчера.

Этот код запускается, если выполняется либо a.collide(b), либо b.collide(a), где A и B - это динамические типы a и b соответственно.

Вы также можете сделать 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!

Вы также можете создать центральную typedef для std::variant<Player*, Wall*, Monster*> и сделать так, чтобы map_type_index использовал этот центральный typedef для определения его порядка, сократив работу по добавлению нового типа в систему двойной диспетчеризации до добавления типа в одном месте, реализуя новыйвведите и отправьте код столкновения, который должен что-то делать.

Более того, этот код двойной отправки можно сделать наследуемым;производный тип от Wall может отправлять на Wall перегрузки.Если вы хотите это, вы должны сделать перегрузки метода collide_dispatcher не final, что позволит SpecialWall перегрузить их.

Это , но в текущих версияхкаждого крупного компилятора теперь поддерживает то, что ему нужно.Все может быть сделано в или даже , но это становится намного более многословным и может потребовать .

Хотя для определения того, что происходит, требуется линейный объем кода, компилятор сгенерирует квадратичный объем кода или данных статической таблицы для реализации двойной диспетчеризации.Поэтому будьте осторожны, прежде чем в вашей таблице двойной отправки будет более 10 000+ типов.


Если вы хотите, чтобы MapObject был конкретным, отделите от него интерфейс, удалите final из диспетчера и добавьте MapObject до pMapType

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 */
};

живой пример .

при желании Player спуститься с MapObject вы должны использовать 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";
  }
};
0 голосов
/ 24 мая 2018

Похоже на небольшую ошибку.

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

И игрок ударит в стену.

...