3

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?

Luca
  • 181
  • 2
  • 7
  • 1
    Start here: [Double dispatch](https://en.wikipedia.org/wiki/Double_dispatch). See also [Understanding double dispatch in C++](https://stackoverflow.com/questions/12582040/understanding-double-dispatch-c) – Doc Brown Apr 06 '19 at 09:01
  • What is a "collision component"? – D Drmmr Apr 06 '19 at 12:07
  • Is it not possible to genericise the collision? Surely you just need contact point and normal? – Ewan Apr 06 '19 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 Apr 06 '19 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 Apr 06 '19 at 18:10

1 Answers1

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);
}
D Drmmr
  • 290
  • 2
  • 6
  • 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::variant – Luca Apr 06 '19 at 16:20
  • @Luca the lambda turns a set of overloads into a function object. `std::optional` and `std::variant` are tagged unions; optional having an empty type as the other member; variant having any number of types. `std::visit` calls the appropriate `operator()` – Caleth Jan 02 '20 at 09:59