Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / 2D

Making a 2D Physics Engine: Shapes, Worlds and Integration

5.00/5 (3 votes)
29 Jan 2018CPOL6 min read 17.3K  
Representing shapes, worlds, and integrating forces and velocities in a 2D physics engine

Making a 2D Physics Engine: The Series

This is the third article in the Making a 2D Physics Engine Series. If you haven't already read all the articles in the series before this one, I strongly recommend that you take a detour and skim through them.

  1. Making a 2D Physics Engine: The Math
  2. Making a 2D Physics Engine: Spaces and Bodies
  3. Making a 2D Physics Engine: Shapes, Worlds and Integration
  4. Making a 2D Physics Engine: Mass, Inertia and Forces

Introduction

This article focuses on a few of the most important aspects in a physics engine: shapes, which represent the space that an entity occupies; worlds, which manage bodies (and more) and is essentially the interface to the physics engine; and integration of forces and velocities, the fundamental building block of any physics simulations. By the end of this article, you should have on your screen an object which realistically falls under the force of gravity! It may not seem like much, but this is a big stepping stone towards completing our physics engine.

To get an idea of what you should have at the end of this article, check out this commit of the 2D physics engine I am building along with this article series in Rust.

Shapes

Shapes (or colliders or whatever you would like to call it) represent the physical space or extents that an object takes up in our world. In the previous article, we talked about a Body, and how it formed the base of the physics engine by representing objects. A Shape, however, forms the base of a Body. Without a Shape, a Body is nothing - a point object without any real physical presence. While point objects are incredibly useful in some areas of physics, we have no use for them.

A Shape should provide other functions of interest, like generating a bounding volume or performing ray intersections, but to reduce complexity, those will be covered when the need arises.

Circles

I am sure you must have heard of the humble circle. For the uninitiated, the Circle Wikipedia article provides a good introduction. The circle shape is quite popular; it is employed in almost all 2D games or physics simulations. You can use it to simulate a wheel, a ball, a puck - the possibilities are endless.

Representing a Circle

Since all shapes will be associated with a body, all we require for our Circle implementation is the radius.

struct Circle : Shape {
    float radius;
}

Convex Polygons

Convex polygons are another popularly used 2D shape. The convex polygon is arguably more versatile than the circle: usages include boxes, characters, walls, and whatnot. Concave polygons are used in physics engines too, but we shall talk about them later.

Representing a Convex Polygon

A ConvexPolygon has two main components: vertices and edge normals. An important thing to keep in mind is the ordering of vertices. Everything from edge normal calculation to detecting collisions and ray intersections is dependent on the order of the vertices for the convex polygon. For all intents and purposes, we will consider the vertices to be ordered counter-clockwise, as per convention. While edge normals can be (and are) calculated from the vertices themselves, they are used so frequently that it is easier and efficient to precalculate them at the time of creation.

Another convention that will be strictly followed is that the normal at index i corresponds to the edge between vertex i and (i + 1) % vertices.count.

C++
struct ConvexPolygon : Shape {
    Vec2[] vertices;
    Vec2[] normals;
}

Worlds

A world is the core of the engine - it manages and updates bodies, queries, joints, collision detection, and so on at every timestep. It can be considered as a collection of objects that are able to physically interact with each other. In a way, the Universe is a World; all objects within it interact with each other through physical forces.

For now, our World simply contains a list of bodies, but all additional engine features will in some way or the other be controlled by it.

C++
struct World {
    List<Body> bodies;
}

World Updates

Probably the most important function of a World is to update all physical entities that belong to it at every timestep. A lot happens during a world update, including collision detection and resolution and updating bodies and joints.

Our current world update model is simplistic, but it will expand along with the definition of World.

C++
void Update(deltaTime) {
     for body in this.bodies {
         body.Update(deltaTime);
     }
}

In this definition of the world update, we have encountered two new things: body.Update and deltaTime, both of which will be covered in the next section.

Integration

Now on to a topic that has more to do with physics than the entire previous two articles: integration of forces and velocities.

Equations of Motion

The three basic equations of motion (both translational and rotational), as you would have learned in high school, are as follows:

$\begin{aligned}
\vec{v} = \int {\vec{a}dt} &\hbox{ or } \vec{v} = \vec{a} \cdot t \\
\vec{x} = \int {\vec{v}dt} &\hbox{ or } \vec{x} = \vec{v} \cdot t \\
\vec{F} = m\vec{a} &\hbox{ or } \vec{a} = \frac{\vec{F}}{m} \hbox{(Newton's Second Law)} \\
\end{aligned}$

We will use these equations to calculate the position and rotation of a body at every timestep, but instead of symbolic integration, we will use numerical integration. Numerical integration is best suited for a physics engine because we are going to be processing everything at very small time steps (in the order of 0.01 seconds), and we cannot predict the variations in acceleration and velocity.

Semi-Implicit (Symplectic) Euler

There are quite a few numerical integration methods out there, but we shall use Semi-Implicit (Symplectic) Euler integration. It is intuitive and efficient while maintaining high accuracy. In our implementation of Symplectic Euler integration, we simply add the change in the variable (the integrand in the above equations) to the variable itself.

Recall the equations of motion defined above. Position (x) is dependent on velocity (v), which is dependent on acceleration (a). We therefore evaluate these in the reverse order every timestep: first acceleration, then velocity and lastly position. Since this step has to be performed for each Body, it will go inside Body.Update:

C++
Vec2 acceleration = force / mass + gravity;        // Gravity will always act on the body
velocity += acceleration * deltaTime;
position += velocity * deltaTime;

We now get to the importance of deltaTime. It serves as the differential ((dt) in our motion equations) for our numerical integration system.

Similarly, we can integrate for rotation:

C++
Vec2 angular_acc = torque / inertia;
angular_vel += angular_acc * deltaTime;
rotation += angular_vel * deltaTime;

Force and Torque Management

The main interaction mechanism of external agents (including the physics engine) with a body is going to be through forces (and indirectly torque). We make sure that no forces or torques "leak" into the next timestep, so at the end of the current timestep (Body.Update), we clear all forces and torques:

force = Vec2.Zero;
torque = 0;

Any and all interactions with a body should be on a per-frame or per-timestep basis. As such, all forces and torques applied to a body should be valid only for the current frame. This is how forces work in real life too, but it is not evident because our world is continuous, not discrete, and does not have any "timesteps". In a timestep, the bodies will always be updated at the very end, so all forces and torques should have been applied by that time.

Next Steps

By now, you should be able to realistically simulate motion (and gravity) for a body! It may not seem like much, but forces are the most important part of any physical simulation. Now that we have got the basics out of the way, we can move on to something much more advanced but still very essential: collisions.

History

  • 13 Nov 2017: Initial post
  • 18 Nov 2017: Add link to GitHub project

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)