Как написать элегантный механизм обработки столкновений?

У меня немного рассол: скажем, я делаю простую, двумерную, похожую на Zelda игру. Когда сталкиваются два объекта, каждый из них должен иметь результирующее действие. Однако, когда главный герой сталкивается с чем-то, его реакция зависит исключительно от типа объекта, с которым он сталкивался. Если это монстр, он должен оправиться, если это стена, ничего не должно произойти, если это волшебная синяя коробка с лентами, он должен лечить и т.д. (Это только примеры).

Я также должен заметить, что ОБОСТЬ является частью столкновения, то есть события столкновения должны происходить как для символа AND, так и для монстра, а не только для одного или другого.

Как бы вы пишете такой код? Я могу придумать несколько невероятно неэлегантных способов, например, иметь виртуальные функции в глобальном классе WorldObject, идентифицировать атрибуты - например, функцию GetObjectType() (возвращает ints, char * s, все, что идентифицирует объект как Monster, Box или Wall), то в классах с большим количеством атрибутов, скажем, Monster, может быть больше виртуальных функций, например GetSpecies().

Однако это становится раздражающим для поддержания и приводит к большому каскадному переключателю (или If) в обработчике столкновений

MainCharacter::Handler(Object& obj)
{
   switch(obj.GetType())
   {
      case MONSTER:
         switch((*(Monster*)&obj)->GetSpecies())
         {
            case EVILSCARYDOG:
            ...
            ...
         }
      ...
   }

}

Там также можно использовать файлы, и файлы будут иметь такие вещи, как:

Object=Monster
Species=EvilScaryDog
Subspecies=Boss

И тогда код может извлекать атрибуты без необходимости того, чтобы виртуальные функции загромождали все. Однако это не решает проблему каскадирования If.

И ТОГДА есть возможность иметь функцию для каждого случая, например CollideWall(), CollideMonster(), CollideHealingThingy(). Это лично мое наименее любимое (хотя они все далеки от симпатичного), потому что кажется, что это очень громоздко поддерживать.

Может кто-нибудь, пожалуйста, дать представление о более элегантных решениях этой проблемы? Спасибо за любую помощь!

Ответ 1

Я бы сделал это наоборот - потому что, если символ сталкивается с объектом, объект сталкивается с символом. Таким образом, вы можете иметь объект базового класса, например:

class Object  {
  virtual void collideWithCharacter(MainCharacter&) = 0;
};

class Monster : public Object  {
  virtual void collideWithCharacter(MainCharacter&) { /* Monster collision handler */ }
};

// etc. for each object

Как правило, в OOP-дизайне виртуальные функции являются единственным "правильным" решением для таких случаев:

switch (obj.getType())  {
  case A: /* ... */ break;
  case B: /* ... */ break;
}

РЕДАКТИРОВАТЬ:
После вашего уточнения вам нужно будет немного отрегулировать это. MainCharacter должен иметь перегруженные методы для каждого из объектов, с которыми он может столкнуться:

class MainCharacter  {
  void collideWith(Monster&) { /* ... */ }
  void collideWith(EvilScaryDog&)  { /* ... */ }
  void collideWith(Boss&)  { /* ... */ }
  /* etc. for each object */
};

class Object  {
  virtual void collideWithCharacter(MainCharacter&) = 0;
};

class Monster : public Object  {
  virtual void collideWithCharacter(MainCharacter& c)
  {
    c.collideWith(*this);  // Tell the main character it collided with us
    /* ... */
  }
};

/* So on for each object */

Таким образом вы уведомляете главного героя о столкновении и можете предпринять соответствующие действия. Также, если вам нужен объект, который не должен уведомлять главного героя о столкновении, вы можете просто удалить вызов уведомления в этом конкретном классе.

Этот подход называется двойной отправкой.

Я также хотел бы сделать сам MainCharacter Object, переместить перегрузки на Object и использовать collideWith вместо collideWithCharacter.

Ответ 2

Как насчет того, чтобы вывести все скользящие объекты из одного общего абстрактного класса (назовем его Collidable). Этот класс может содержать все свойства, которые могут быть изменены с помощью коллизии и одной функции HandleCollision. Когда сталкиваются два объекта, вы просто вызываете HandleCollision для каждого объекта с другим объектом в качестве аргумента. Каждый объект манипулирует другим для обработки столкновения. Ни один объект не должен знать, к какому другому типу объекта он просто подпрыгнул, и у вас нет больших операторов switch.

Ответ 3

Сделать все блокируемые объекты реализующими интерфейс (скажем, "Collidable" ) с помощью метода collideWith (Collidable). Затем, по вашему алгоритму обнаружения столкновений, если вы обнаружите, что A сталкивается с B, вы вызываете:

A->collideWith((Collidable)B);
B->collideWith((Collidable)A);

Предположим, что A является MainCharacter и B монстром и оба реализуют интерфейс Collidable.

A->collideWith(B);

Вызов:

MainCharacter::collideWith(Collidable& obj)
{
   //switch(obj.GetType()){
   //  case MONSTER:
   //    ...
   //instead of this switch you were doing, dispatch it to another function
   obj->collideWith(this); //Note that "this", in this context is evaluated to the
   //something of type MainCharacter.
}

Это, в свою очередь, вызовет метод Monster:: collideWith (MainCharacter), и вы можете реализовать там все поведение монстра:

Monster::CollideWith(MainCharacter mc){
  //take the life of character and make it bounce back
  mc->takeDamage(this.attackPower);
  mc->bounceBack(20/*e.g.*/);
}

Дополнительная информация: Одиночная отправка

Надеюсь, что это поможет.

Ответ 4

То, что вы называете "раздражающим оператором switch", я бы назвал "отличной игрой", чтобы вы оказались на правильном пути.

Наличие функции для каждого правила взаимодействия/игры - именно то, что я хотел бы предложить. Это позволяет легко находить, отлаживать, изменять и добавлять новые функции:

void PlayerCollidesWithWall(player, wall) { 
  player.velocity = 0;
}

void PlayerCollidesWithHPPotion(player, hpPoition) { 
  player.hp = player.maxHp;
  Destroy(hpPoition);
}

...

Итак, вопрос в том, как определить каждый из этих случаев. Предполагая, что у вас есть какое-то обнаружение столкновения, которое приводит к тому, что X и Y сталкиваются (так же просто, как тесты перекрытия N ^ 2 (эй, это работает на растения против зомби, и это очень много продолжается!) Или столь же сложно, как и подметание и обрезка + gjk)

void DoCollision(x, y) {
  if (x.IsPlayer() && y.IsWall()) {   // need reverse too, y.IsPlayer, x.IsWall
     PlayerCollidesWithWall(x, y);    // unless you have somehow sorted them...
     return;
  }

  if (x.IsPlayer() && y.IsPotion() { ... }

  ...

Этот стиль, а подробный -

  • легко отлаживать
  • легко добавлять случаи
  • показывает, когда вы логические/конструктивные несоответствия или упущения ", а что если X - это игрока и стены из-за Способность "PosessWall", что тогда!?! " (а затем позволяет просто добавлять случаи для их обработки)

Этап ячейки Spore использует именно этот стиль и имеет приблизительно 100 проверок, что приводит к примерно 70 различным результатам (не считая разворотов парамагментов). Это всего лишь десятиминутная игра, это 1 новое взаимодействие каждые 6 секунд для всего этапа - теперь это значение игрового процесса!

Ответ 5

Если я правильно понял вашу проблему, мне бы понравилось

Class EventManager {
// some members/methods
handleCollisionEvent(ObjectType1 o1, ObjectType2 o2);
// and do overloading for every type of unique behavior with different type of objects.
// can have default behavior as well for unhandled object types
}