Getting started

Maul2D is a deterministic 2D physics engine for games, written in C. It builds with CMake, installs as a CMake package and a pkg-config module, and needs nothing but a C compiler and the C standard library.

Requirements

The public headers are plain C17 and C++17, so a program that includes them needs neither a C23 compiler nor C at all.

Build and install

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build
cmake --install build --prefix /path/to/prefix

Then, from CMake:

find_package(maul2d REQUIRED CONFIG)
target_link_libraries(game PRIVATE maul2d::maul2d)

or through pkg-config, pkg-config --cflags --libs maul2d. A project can also add the source tree with add_subdirectory; the tests, samples and benchmarks are then off by default. Every tagged release carries the source archive with its checksum, an SBOM and a build attestation; like every Maul library, maul2d ships as source and builds with the steps above.

Build options

Option Default Effect
MAUL2D_SIMD auto auto uses AVX2 and FMA on x64 (Haswell, 2013, and later) and NEON on arm64; scalar builds portable kernels. Every backend gives the same bits.
MAUL2D_BUILD_SHARED OFF A shared library instead of a static one.
MAUL2D_VALIDATE OFF In a build with assertions (Debug, sanitizers), checks every invariant of the world after each step and stops at the first broken one.
MAUL2D_BUILD_TESTBED OFF The interactive testbed, which fetches raylib.
MAUL2D_BUILD_TESTS, _SAMPLES, _BENCH on at top level The test suites, samples and benchmarks.

An AVX2 build started on a CPU without AVX2 refuses to create worlds with m2_errorUnsupported instead of crashing; m2IsCpuBackendSupported tells a program before it tries.

A first world

m2WorldDef worldDef = m2DefaultWorldDef();
m2World* world = NULL;
if (m2CreateWorld(&worldDef, &world) != m2_success)
{
    return; // the CPU, the def or the allocator refused
}

m2BodyDef groundDef = m2DefaultBodyDef();
m2BodyId ground = m2_nullBodyId;
m2ShapeDef shapeDef = m2DefaultShapeDef();
m2Polygon floor = m2MakeBox(10.0f, 0.5f);
m2ShapeId floorShape = m2_nullShapeId;

m2BodyDef ballDef = m2DefaultBodyDef();
ballDef.type = m2_dynamicBody;
ballDef.position = (m2Pos2){0.0, 4.0};
m2BodyId ball = m2_nullBodyId;
m2Circle circle = {{0.0f, 0.0f}, 0.5f};
m2ShapeId ballShape = m2_nullShapeId;

bool built =
    m2CreateBody(world, &groundDef, &ground) == m2_success &&
    m2CreatePolygonShape(world, ground, &shapeDef, &floor, &floorShape) == m2_success &&
    m2CreateBody(world, &ballDef, &ball) == m2_success &&
    m2CreateCircleShape(world, ball, &shapeDef, &circle, &ballShape) == m2_success;

bool stepped = built;
for (int32_t i = 0; stepped && i < 120; ++i)
{
    stepped = m2World_Step(world, 1.0f / 60.0f, 4) == m2_success;
}
m2Pos2 resting = m2Body_GetPosition(world, ball);
m2DestroyWorld(world);

Step at a fixed timestep, always: determinism starts with feeding the same dt every step, and 1/60 with 4 substeps is the tuned default. A step takes 1 to M2_MAX_SUBSTEPS (64) substeps and a dt from M2_MIN_STEP to M2_STEP_LIMIT (1e-6 to 10 s); anything else is refused with a status, changes nothing and counts one misuse (Limits). Check every step's status: a refused step does not advance the frame.

The model

Each header documents every function: what it does, each argument, each status it returns and from which threads it may be called. The API reference is generated from them.

What it leaves out

Some features of other engines are absent by decision. Pre-solve callbacks run program code in the middle of a step, which breaks replay by construction; chains, filters, sensors and the filter joint cover the usual cases deterministically. Solver settings (contact stiffness, speculative distance, warm starting) stay fixed, because the physics is part of the determinism contract. There is no way to turn off determinism, the sleep bookkeeping or the pairing of begin and end events.

Samples and the testbed

The samples build with the library and print something to check:

The testbed runs in the browser (try it) or natively with -DMAUL2D_BUILD_TESTBED=ON. Its twelve scenes include a car, a platformer on the character mover, a machinery hall, water, jelly, and ragdolls whose time runs backward while R is held.