Как написать элегантный механизм обработки столкновений? - PullRequest
11 голосов
/ 05 сентября 2010

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

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

Как бы вы написали такой код?Я могу придумать несколько невероятных способов, например, иметь виртуальные функции в глобальном классе 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 ().Лично это мой наименее любимый (хотя все они далеки от симпатии), потому что это кажется самым громоздким в обслуживании.

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

Ответы [ 5 ]

11 голосов
/ 05 сентября 2010

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

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

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

// etc. for each object

Обычно в дизайне ООП виртуальные функции являются единственным «правильным» решением для таких случаев:

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

EDIT :После вашего разъяснения вам нужно будет немного откорректировать вышесказанное.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 голосов
/ 05 сентября 2010

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

1 голос
/ 23 сентября 2010

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

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

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() { ... }

  ...

Этот стиль, хотя многословный,

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

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

1 голос
/ 05 сентября 2010

Сделайте так, чтобы все объекты, которые могут быть заблокированы, реализовали интерфейс (скажем, «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.*/);
}

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

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

0 голосов
/ 05 сентября 2010

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

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
}
...