物理引擎的继承/接口决策

Inheritance/interface decisions for physics engine

本文关键字:接口 决策 继承 引擎      更新时间:2023-10-16

这是针对在MinGW/Windows上使用SDL的小型游戏项目。

正在开发一个物理引擎,我的想法是有一个Physics::Object,所有物理对象都应该从中派生出来,并且它将自己注册到全局Physics::System类(它是一个单状态模式(,这样用户就不需要跟踪物理计算中包含哪些对象,只需要调用像Physics::System::PerformTimestepCalculation(double dt)这样的函数。

这工作正常,我什至使用单个派生类Physics::Circle来实现它,这是一个 2d 圆。我对预测性碰撞检测非常满意,尽管我仍然需要优化它。

无论如何,当我开始添加其他基元以包含在计算中时,我遇到了麻烦,例如行。Physics::System::PerformTimestepCalculation(double dt)充斥着对Object::GetID()或类似功能的调用(可能是为了避免dynamic_cast<>(,但我觉得很脏。

我做了一些阅读,意识到我的层次结构中的元素是不可替代的(即两个圆之间的碰撞在两条线的碰撞之间非常不同(。

我喜欢我的Physics::Objects System类"自我注册"的方式,这样它们就会自动包含在计算中,我真的不想失去这一点。

必须有一些其他合理的设计路径。我怎样才能更好地重新设计东西,使不可替代的对象不会妨碍?

编辑仅供参考:最后,我打破了实体和形状属性,类似于接受答案中的描述,类似于实体-组件-系统模型。这意味着我仍然有"这是一条圆还是一条线,这是一条线还是一个圆?"的yuk逻辑,但我不再假装多态性在这里帮助了我。这也意味着我使用某种工厂,并且可以同时发生多个计算世界!

最成功的公开物理引擎并不十分注重"模式"或"面向对象设计"。

以下是支持我的,公认的大胆断言的摘要:

花栗鼠 - 用 C 写的,说得够多了。

Box2d - 用C++写的,这里有一些多态性。 有一个形状层次结构(基类 b2Shape(,其中包含一些虚拟函数。但是,这种抽象像筛子一样泄漏,并且您会在整个源代码中找到许多用于叶类的强制转换。还有一个"联系人"的层次结构,事实证明它更成功,尽管对于单个虚拟函数,在没有多态性的情况下重写它是微不足道的(我相信花栗鼠使用函数指针(。b2Body 是用于表示刚体的类,它是非虚拟的。

子弹 - 用C++编写,用于大量游戏。大量的功能,大量的代码(相对于其他两个(。实际上有一个刚体和软体表示扩展的基类,但只有一小部分代码可以使用它。基类的大部分虚函数都与序列化(保存/加载引擎状态(有关,剩下的两个虚函数软体无法实现一个带有 TODO 的 TODO 通知我们需要清理一些黑客。不完全是对物理引擎中多态性的响亮认可。

这是很多话,我什至还没有真正开始回答你的问题。我想强调的是,多态性并不是在现有物理引擎中有效应用的东西。这可能不是因为作者没有"得到"OO。

所以无论如何,我的建议是:放弃实体类的多态性。你最终不会得到100种不同的类型,你以后不可能重构,你的物理引擎的形状数据将是相当均匀的(凸多边形,盒子,球体等(,你的实体数据可能会更加同质(可能只是刚体开始(。

我觉得你犯的另一个错误是只支持一个物理::系统。能够相互独立地模拟身体是有用的(例如,对于双人游戏(,最简单的方法是支持多个物理::系统。

考虑到这一点,最干净的"模式"是工厂模式。当用户想要创建一个刚体时,他们需要告诉 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 );

呵,有点咆哮。希望这是有帮助的。至少我想指出你指向box2d。它是用非常简单的C++方言编写的,其中应用的模式将与您的引擎相关,无论是 3D 还是 2D。

层次结构的问题在于它们并不总是有意义的,试图将所有内容都塞进层次结构只会导致尴尬的决定和令人沮丧的工作。

可以使用的另一种解决方案是标记的联合解决方案,最好体现在 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);
}