I'm writing a simple game engine and after a lot of rethinking/refactoring I settled with sort of a component based architecture (not strictly ECS, but it isn't inheritance based anymore either). So everything in my world is an entity, and each entity has got a bunch of components. Every system/subsystem in my game scans an entity for a series of components it's interested in, and performs some relevant computations.
So far so good. The engine basic architecture can be seen here:
Now, every entity that is collidable with has a collision component (along with position/movement/rigidbody components), so the physics system needs to get that component and use it to feed its collision detection algorithms, in order to generate contact data to be used to resolve the collision.
I'm stuck on the following issue: the collison detection algorithms deal with different geometries: boxes,spheres,planes and rays (as of now), but I don't want to have a spherecollisioncomponent and a boxcollisioncomponent, at least I don't want them to be unrelated but I'd like them to share some common base class.
class Sphere
{
public:
Sphere(float radius);
~Sphere();
float GetRadius() { return mRadius; }
private:
float mRadius;
};
class Box : public BoundingVolume
{
public:
Box(const XMFLOAT3 &halfSize);
~Box();
XMFLOAT3 const &GetHalfSize() const { return mHalfSize; }
private:
XMFLOAT3 mHalfSize;
};
Obviously each component has a different interface (boxes have halfsizes, spheres have a radius and so on), and the different collision detection functions deal very differently with each of them (box-box, box-sphere, sphere-sphere..).
void CollisionSystem::BoxAndBoxCollision(const Box &box1, const Box &box2)
{
// contact data
XMFLOAT3 contactPoint;
XMFLOAT3 contactNormal;
float minOverlap = 100.0f;
// get axes for SAT test
std::vector<XMFLOAT3> axes = GetSATAxes(box1, box2);
int axisIndex = 0;
int index = 0;
for (XMFLOAT3 axis : axes)
{
if (XMVectorGetX(XMVector3Length(XMLoadFloat3(&axis))) < 0.01f)
{
index++;
continue;
}
float overlap = PerformSAT(axis, box1, box2);
if (overlap < 0) // found separating axis - early out
return;
if (overlap < minOverlap)
{
minOverlap = overlap;
axisIndex = index;
}
index++;
}
// other collision detection/generation code.....
// store contact
mContacts.push_back(new Contact(box1->GetRigidBody(), box2->GetRigidBody(), contactPoint, contactNormal, minOverlap, coefficientOfRestitution));
}
So how can I solve this in an elegant and robust way?
-
1Start here: Double dispatch. See also Understanding double dispatch in C++Doc Brown– Doc Brown04/06/2019 09:01:41Commented Apr 6, 2019 at 9:01
-
What is a "collision component"?D Drmmr– D Drmmr04/06/2019 12:07:30Commented Apr 6, 2019 at 12:07
-
Is it not possible to genericise the collision? Surely you just need contact point and normal?Ewan– Ewan04/06/2019 13:13:37Commented Apr 6, 2019 at 13:13
-
@Ewan even if the contact data consists of contact point, normal and interpenetration, I still use different algoritms to generate the contact data, depending on the primitives involved (box-box, sphere-box, triangle mesh-spere etc.)Luca– Luca04/06/2019 16:17:52Commented Apr 6, 2019 at 16:17
-
@DDrmmr now is merely a tag interface that's needed to classify a collidable sub class (geometry, a sphere, a box) as a collision component in my "ECS"Luca– Luca04/06/2019 18:10:45Commented Apr 6, 2019 at 18:10
1 Answer 1
I've found that the best way to do this in C++ is to use a variant (e.g. std::variant
or boost::variant
) to store any type of shape that you support. Then use overloaded functions to implement the collision detection algorithms. Using lambda functions (especially generic lambdas) and a little bit of clever machinery this design allows you to do single and multiple dispatching with very little boilerplate code and without the need for coupling unrelated code.
typedef std::variant<Box, Sphere> VariantShape;
std::optional<Contact> collision(const Box&, const Box&);
std::optional<Contact> collision(const Box&, const Sphere&);
std::optional<Contact> collision(const Sphere&, const Box&);
std::optional<Contact> collision(const Sphere&, const Sphere&);
std::optional<Contact> collision(const VariantShape& a, const VariantShape& b)
{
std::visit ([](const auto& a, const auto& b)
{
return collision(a, b);
}, a, b);
}
-
Ok, I need to digest this answer, I know lambdas (I'm reading about generic lambdas in Effective Modern C++), but I don't know std::optional and std::variantLuca– Luca04/06/2019 16:20:40Commented Apr 6, 2019 at 16:20
-
@Luca the lambda turns a set of overloads into a function object.
std::optional
andstd::variant
are tagged unions; optional having an empty type as the other member; variant having any number of types.std::visit
calls the appropriateoperator()
Caleth– Caleth01/02/2020 09:59:47Commented Jan 2, 2020 at 9:59
Explore related questions
See similar questions with these tags.