DEV Community

Pavel Zabelin
Pavel Zabelin

Posted on

2D Physics Engine with c++

Introduction to the Project

About the Project

About three months ago, I came across a video about a homemade 2D physics engine and immediately got inspired to recreate the project myself. The idea was to build an engine in C++ that can render basic shapes and calculate their collisions. Along the way, I wanted to revisit 9th-grade physics concepts, gain a better understanding of the architecture behind such projects, and most importantly — have some fun.

Demo of the Project

In this article, I’ll describe the core functionality of my project and how it was implemented. The code is open-source, so feel free to dive into it right away. Here’s the GitHub link: 2DPhysicsEngine

Project Structure and Tools

For this project, I used a template from Cherno’s channel: TheCherno/ProjectTemplate. This template splits the program into two modules right from the start (in my case, ShowCase and Engine) and works out of the box with the Premake build system, which ensures compatibility across different platforms and IDEs. For rendering, I chose the SFML library as it’s one of the simplest and most popular solutions.

This template, with its two-module separation, essentially forces you to write cleaner and more architecturally logical code.

Interaction Between Modules (ShowCase and Engine)

As mentioned earlier, the project is divided into two modules:

  • ShowCase This module is responsible for the main loop, handling tasks such as rendering graphics, processing input, and calling the update() method on the engine side.
  • Engine This module handles all calculations related to rigid bodies and their collisions.

Each shape in the ShowCase module corresponds to a component in the Engine module that represents it in the physical world.

Engine Module

Engine Architecture: Main Loop

The engine stores an array of all physical bodies and iterates through them each frame to:

  1. Check for collisions.
  2. Resolve any collisions if present.
  3. Apply gravity.
  4. Call the Update(DeltaTime) method on each physical body.
  5. Remove marked objects.
void Engine::Update(float DeltaTime)
{
    CheckCollisions();
    for (auto it = RigidBodies.begin(); it != RigidBodies.end(); )
    {
        auto& Body = *it;

        Body->ApplyGravity(Gravity);
        Body->Update(DeltaTime);

        if (Body->IsMarkedForDeletion())
        {
            it = RigidBodies.erase(it);
        }
        else
        {
            ++it;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's break down how this process works in detail

2.2. Collision System

For each pair of physical objects, separate collision checks are used. For two circles, it's enough to compare the distance between them with the sum of their radii, while for two AABBs (simplified rectangles), the formula will be different.

Circle Collision Example

The first interesting challenge I encountered was determining which pair of objects needed a collision check. Since the engine's array stores a pointer to the base class RigidBodyComponent, and collision pair checks will occur many times every tick, the solution I chose was the double dispatch system. Let me explain how this works.

The base class for physical bodies has a pure virtual overloaded method that can accept a reference to either a base class object or a reference to any of its derived classes in the project.

virtual void CheckCollision(RigidBodyComponent& other) = 0;
virtual void CheckCollision(CircleComponent& other) = 0;
virtual void CheckCollision(AABBComponent& other) = 0;
Enter fullscreen mode Exit fullscreen mode

Now, let's examine how the derived classes should override these methods using the circle as an example:

void CircleComponent::CheckCollision(RigidBodyComponent& other)
{
    other.CheckCollision(*this);
}

void CircleComponent::CheckCollision(CircleComponent& other)
{
    Engine::HandleCollision(*this, other);
}

void CircleComponent::CheckCollision(AABBComponent& other)
{
    Engine::HandleCollision(*this, other);
}
Enter fullscreen mode Exit fullscreen mode

As you can see, if we know which class we need to check for collision with, we call a static method in the engine, passing a reference to the circle and a reference to the second object. However, if we receive a reference to the base class, we pass that object a reference to itself, thus invoking the CheckCollision(CircleComponent& other) method on the second object.

For this to work on the engine side, we also need methods to handle collision checks for all pairs of objects:

static void HandleCollision(CircleComponent& Circle, AABBComponent& aabb);

static void HandleCollision(CircleComponent& Circle1, CircleComponent& Circle2);

static void HandleCollision(AABBComponent& aabb1, AABBComponent& aabb2);
Enter fullscreen mode Exit fullscreen mode

As I understand it, this works because in order to call the CheckCollision method with a reference to the base class from within the object, an upcast must be performed. This results in the method that accepts a reference to the derived class taking priority.

2.3. Collision Response

In the case of a "successful" collision, we calculate the collision normal and the depth of penetration between the objects, then call the ResolveCollision method, where we calculate and apply the impulse to the objects based on their velocities.

void Engine::ResolveCollision(RigidBodyComponent& BodyA, RigidBodyComponent& BodyB, const FVector2D& CollisionNormal)
{
    if (!BodyA.IsDynamic() && !BodyB.IsDynamic())
    {
        return;
    }

    float MassA = BodyA.IsDynamic() ? BodyA.GetMass() : FLT_MAX;
    float MassB = BodyB.IsDynamic() ? BodyB.GetMass() : FLT_MAX;

    FVector2D RelativeVelocity = BodyA.GetVelocity() - BodyB.GetVelocity();

    float VelocityAlongNormal = RelativeVelocity.Dot(CollisionNormal);

    if (VelocityAlongNormal > 0)
    {
        return;
    }

    float ImpulseMagnitude = -VelocityAlongNormal / (1.0f / MassA + 1.0f / MassB);

    FVector2D Impulse = CollisionNormal * ImpulseMagnitude;

    if (BodyA.IsDynamic())
    {
        BodyA.SetVelocity(BodyA.GetVelocity() + Impulse / MassA);
    }

    if (BodyB.IsDynamic())
    {
        BodyB.SetVelocity(BodyB.GetVelocity() - Impulse / MassB);
    }
}
Enter fullscreen mode Exit fullscreen mode

Then, we call the PositionalCorrection method to fix any objects getting stuck inside each other, which can happen due to high velocities or during spawning.

void Engine::PositionalCorrection(RigidBodyComponent& BodyA, RigidBodyComponent& BodyB, const FVector2D& CollisionNormal, float PenetrationDepth)
{
    const float CorrectionFactor = 0.5f;
    FVector2D Correction = CollisionNormal * PenetrationDepth * CorrectionFactor;

    if (!BodyA.IsDynamic() || !BodyB.IsDynamic())
    {
        Correction = Correction * 2.f;
    }

    if (BodyA.IsDynamic())
    {
        BodyA.SetPosition(BodyA.GetPosition() + Correction);
    }

    if (BodyB.IsDynamic())
    {
        BodyB.SetPosition(BodyB.GetPosition() - Correction);
    }
}
Enter fullscreen mode Exit fullscreen mode

2.4. The Update Method for Physical Bodies

Every tick, the engine calls the Update method for all physical bodies, where the velocity and position are updated, acceleration is reset, and the object is marked for destruction if necessary.

void RigidBodyComponent::Update(float DeltaTime)
{
    if (!bIsDynamic)
    {
        return;
    }

    Velocity += Acceleration * DeltaTime;

    Velocity = Velocity * (1.0f - EngineConfig::DAMPING_FACTOR * DeltaTime);

    Position += Velocity * DeltaTime;

    Acceleration = FVector2D(0.f, 0.f);

    NotifyPositionObservers();

    if (IsOutOfBounds())
    {
        MarkForDeletion();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this article, I don't delve into the physics of these processes. In this case, we use conclusions drawn from basic dynamics formulas.

Screenshot with formulas

2.5. Observers

You might have already noticed the call to the NotifyPositionObservers() method in your project. To notify about changes on the engine side, I decided to use the Observer pattern. In fact, I even have an article on this topic: Delegates & Observer Pattern Implementation in Unreal Engine.

Let's break it down with the position observer example. Anyone who wants to receive information about the position of a physical object must implement the following interface:

class IRigidBodyPositionObserver
{
public:
    virtual void OnRigidBodyPositionUpdated(const FVector2D& NewPosition) = 0;
};
Enter fullscreen mode Exit fullscreen mode

To "subscribe" to updates, you need to call the method void AddPositionObserver(IRigidBodyPositionObserver* Observer) on the object you're interested in and pass this (the current instance) as the observer.

Meanwhile, the physical body, when updating its position, will loop through the list of subscribers and call the interface method on each of them.

ShowCase Module

3.1. Component Initialization

I prefer a minimalist main, so everything related to initialization (including the physics engine, window creation, and font loading) is contained within the ShowCase class.

Screenshot of the main function

3.2. Main Loop of ShowCase

After initialization, we enter the main loop, where rendering, updating, and processing of input and other events occur.

void ShowCase::Run() 
{
    SpawnLevel();

    while (Window.isOpen()) 
    {
        ProcessEvents();
        Update();
        Render();
    }
}
Enter fullscreen mode Exit fullscreen mode

ShowCase also has an array of all the shapes that need to be rendered, and their Draw method is called every frame.

In the Update method, we calculate the DeltaTime variable and pass it to the engine by calling PhysicsEngine.Update(DeltaTime).

We also clean our array of shapes to be rendered, similar to how it's done in the engine, and handle shape spawning here as well. Since spawning occurs 20 times per second, in the input processing, we only change boolean variables that indicate whether mouse buttons are held down.

void ShowCase::Update()
{
    float DeltaTime = Clock.restart().asSeconds();
    PhysicsEngine.Update(DeltaTime);
    ClearMarkedShapes();

    TryToSpawnShape(DeltaTime);
}
Enter fullscreen mode Exit fullscreen mode

4. Conclusion

4.1. Future Plans

In the future, I plan to optimize collision calculations between objects. There's no need to calculate collisions for every object with every other object. For example, we could divide the area into a grid and then determine which objects need collision checks with each other. Additionally, multithreading could be used to improve performance. Another idea is to add new shapes or combinations of shapes, such as chains of multiple bodies.

4.2. Recommendations to Readers

I highly recommend writing your own project with the implementation of some basic game engine module, whether it's physics, rendering, or something else.

Aside from improving your skills as a developer, it brings a lot of satisfaction. Seriously, I had almost forgotten the magic of development—when you first draw something on the screen or two circles start bouncing off each other correctly. I hope my article helps you take your first steps.

Top comments (0)