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.
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:
- Check for collisions.
- Resolve any collisions if present.
- Apply gravity.
- Call the
Update(DeltaTime)
method on each physical body. - 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;
}
}
}
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.
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;
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);
}
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);
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);
}
}
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);
}
}
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();
}
}
In this article, I don't delve into the physics of these processes. In this case, we use conclusions drawn from basic dynamics 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;
};
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.
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();
}
}
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);
}
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)