Наследование / интерфейсные решения для физического движка - PullRequest
6 голосов
/ 18 февраля 2012

Это для небольшого игрового проекта с SDL на MinGW / Windows.

Я работаю над физическим движком, и моя идея заключалась в том, чтобы иметь Physics::Object, из которого должны извлекаться все физические объекты ион регистрируется с помощью глобального класса Physics::System (это шаблон с одним состоянием), так что пользователю не нужно отслеживать, какие объекты включены в физические вычисления, а просто нужно вызвать функцию, подобную Physics::System::PerformTimestepCalculation(double dt).

* 1007.* Это прекрасно работает, и я даже реализовал это, используя единственный производный класс Physics::Circle, который является двумерным кругом.Я был очень доволен прогнозирующим обнаружением столкновений, хотя мне все еще нужно его оптимизировать.

В любом случае, я столкнулся с неприятностью , когда начал добавлять другие примитивы для включения в вычисления, напримерлиния.Physics::System::PerformTimestepCalculation(double dt) стал замусорен вызовами Object::GetID() или подобными функциями (может быть, это способ избежать dynamic_cast <>), но я чувствую себя грязным.

Я немного прочитал и понялчто мои элементы моей иерархии не могут быть заменяемыми (т. е. столкновение между двумя окружностями очень сильно отличается от столкновения двух линий).

Мне нравится, как мой Physics::Objects "саморегистрация" с System класс, чтобы они автоматически включались в расчеты, и я не хочу этого терять.

Должны быть и другие разумные пути проектирования.Как я могу лучше изменить дизайн, чтобы не мешать незаменимым объектам?

Редактировать К вашему сведению: В конце я отделил свойства сущности и формы, похожие на те, что былиописан в принятом ответе и аналогичен модели сущность-компонент-система.Это означает, что у меня все еще есть логика юка «это круг или линия, и это линия или круг?», Но я больше не притворяюсь, что полиморфизм помогает мне здесь.Это также означает, что я использую какую-то фабрику и могу иметь несколько вычислительных миров одновременно

Ответы [ 2 ]

5 голосов
/ 18 февраля 2012

Наиболее успешные общедоступные физические движки не очень сильно относятся к «шаблонам» или «объектно-ориентированному дизайну».

Вот краткое изложение, подтверждающее мое, по общему мнению, смелое утверждение:

Бурундук - написано на С, достаточно сказано.

Box2d - Написано на C ++, и здесь есть некоторый полиморфизм. есть иерархия фигур (базовый класс b2Shape) с несколькими виртуальными функциями. Однако эта абстракция протекает как сито, и вы найдете множество приведения к листовым классам по всему исходному коду. Существует также иерархия «контактов», которая оказывается более успешной, хотя с одной виртуальной функцией было бы тривиально переписать это без полиморфизма (я думаю, что chipmunk использует указатель на функцию). b2Body - это класс, используемый для представления твердых тел, и он не является виртуальным.

Bullet - Написан на C ++, используется во многих играх. Тонны функций, тонны кода (относительно двух других). На самом деле существует базовый класс, который расширяет представления твердого тела и мягкого тела, но только небольшая часть кода может его использовать. Большинство виртуальных функций базового класса относятся к сериализации (сохранение / загрузка состояния механизма), из двух оставшихся виртуальных функций мягкое тело не может реализовать одну из них с TODO, информирующим нас о том, что некоторый хак необходимо очистить. Не совсем убедительное подтверждение полиморфизма в физических движках.

Это много слов, и я даже не начал отвечать на ваш вопрос. Все, что я хочу сказать, - это то, что полиморфизм - это не то, что эффективно применяется в существующих физических движках. И это, вероятно, не потому, что авторы не «получили» ОО.

Так что в любом случае мой совет: канавный полиморфизм для вашего класса сущностей. У вас не будет 100 различных типов, которые вы не сможете реорганизовать позднее, данные формы вашего физического движка будут довольно однородными (выпуклые полики, прямоугольники, сферы и т. Д.), И ваши данные о сущностях, скорее всего, будут еще более однородный (вероятно, просто твердые тела).

Еще одна ошибка, которую, я чувствую, вы совершаете, - это поддержка только одной Physics :: System. Полезно иметь возможность моделировать тела независимо друг от друга (например, для игры на двоих), и самый простой способ сделать это - поддерживать несколько Physics :: Systems.

Имея это в виду, самый чистый «образец», которым нужно следовать, будет фабричным шаблоном. Когда пользователи хотят создать твердое тело, они должны сказать Physics :: System (действующей как фабрика) сделать это для них, поэтому в вашей Physics :: System:

// returning a smart pointer would not be unreasonable, but I'm returning a raw pointer for simplicity:
rigid_body_t* AddBody( body_params_t const& body_params );

А в коде клиента:

circle_params_t circle(1.f /*radius*/);
body_params_t b( 1.f /*mass*/, &circle /*shape params*/, xform /*system transform*/ );
rigid_body_t* body = physics_system.AddBody( b );

Anyhoo, вид напыщенной речи. Надеюсь, это полезно. По крайней мере, я хочу указать вам на box2d. Он написан на довольно простом диалекте C ++, и применяемые в нем шаблоны будут иметь отношение к вашему движку, будь то 3D или 2D.

3 голосов
/ 18 февраля 2012

Проблема иерархий заключается в том, что они не всегда имеют смысл, а попытка втиснуть все в иерархию просто приводит к неловким решениям и срыву работы в будущем.

Другое решение, которое можно использовать, - это теговое объединение , наилучшим образом воплощенное в boost::variant.

Идея заключается в создании объекта, который может содержать один экземпляр данного типа (среди предварительно выбранного списка) в любой момент времени:

typedef boost::variant<Ellipsis, Polygon, Blob> Shape;

И затем вы можете предоставить функциональность, переключив список типов:

struct AreaComputer: boost::static_visitor<double> {
  template <typename T>
  double operator()(T const& e) { return area(a); }
};

void area(Shape const& s) {
  AreaComputer ac;
  return boost::apply_visitor(s, ac);
}

Производительность такая же, как у виртуальной диспетчеризации (обычно не слишком много), но вы получаете большую гибкость:

void func(boost::variant<Ellipsis, Blob> const& eb);
void bar(boost::variant<Ellipsis, Polygon> const& ep);
// ...

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

А по поводу бинарного посещения:

struct CollisionComputer: boost::static_visitor<CollisionResult> {
   CollisionResult operator()(Circle const& left, Circle const& right);
   CollisionResult operator()(Line const& left, Line const& right);

   CollisionResult operator()(Circle const& left, Line const& right);
   CollisionResult operator()(Line const& left, Circle const& right);
};

CollisionResult collide(Shape const& left, Shape const& right) {
  return boost::apply_visitor(CollisionComputer(), left, right);
}
...