Tutorial 96: Physics Integration (Box2D / Bullet)

CNA — C++ XNA 4.0 reimplementation

CNA does not include physics

CNA provides math types (Vector2, Vector3, Matrix) and rendering, but no physics engine. Choose a physics library appropriate for your game: Box2D v3 for 2D games, Bullet Physics for 3D games. Both integrate cleanly through CNA's Update() method.

Box2D v3 integration (2D)

# Clone Box2D v3 (new C API)
git clone https://github.com/erincatto/box2d.git ../box2d
cmake -S ../box2d -B ../box2d/build
cmake --build ../box2d/build

# In your game's CMakeLists.txt:
add_subdirectory(../box2d ${CMAKE_BINARY_DIR}/box2d)
target_link_libraries(MyGame PRIVATE box2d)

Bullet Physics integration (3D)

git clone https://github.com/bulletphysics/bullet3.git ../bullet3
cmake -S ../bullet3 -B ../bullet3/build \
  -DBUILD_DEMOS=OFF -DBUILD_EXTRAS=OFF -DUSE_GRAPHICAL_BENCHMARK=OFF
cmake --build ../bullet3/build
find_package(Bullet REQUIRED)
target_link_libraries(MyGame PRIVATE ${BULLET_LIBRARIES})
target_include_directories(MyGame PRIVATE ${BULLET_INCLUDE_DIRS})

Physics step in Update()

Always step the physics world in Update(), not in Draw(). Use a fixed timestep for deterministic simulation.

// Box2D v3: world step
void Update(const GameTime& gt) override {
    float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());

    // Accumulate time and step in fixed increments
    accumulator_ += dt;
    const float FIXED_DT = 1.0f / 60.0f;
    while (accumulator_ >= FIXED_DT) {
        b2World_Step(worldId_, FIXED_DT, 4);   // 4 substeps
        accumulator_ -= FIXED_DT;
    }
}

Syncing physics bodies to CNA transforms

Box2D v3 integration: world step + body sync to Vector2 position:

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Rectangle.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "box2d/box2d.h"
#include <memory>
#include <vector>
#include <cmath>

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

struct PhysicsBody {
    b2BodyId  bodyId;
    Vector2   renderPos;   // updated from physics each frame
    float     renderAngle;
    int       width, height;

    void SyncFromPhysics() {
        b2Vec2 pos = b2Body_GetPosition(bodyId);
        renderPos.X  = pos.x;
        renderPos.Y  = pos.y;
        renderAngle  = b2Body_GetAngle(bodyId);
    }
};

class PhysicsDemo final : public Game {
public:
    PhysicsDemo() : graphics_(this) {
        graphics_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

protected:
    void Initialize() override {
        // Create Box2D world (gravity pointing down in screen space)
        b2WorldDef worldDef = b2DefaultWorldDef();
        worldDef.gravity = { 0.0f, 200.0f };   // pixels/sec² downward
        worldId_ = b2CreateWorld(&worldDef);

        // Ground body (static)
        b2BodyDef groundDef = b2DefaultBodyDef();
        groundDef.position = { 400.0f, 580.0f };
        b2BodyId groundId = b2CreateBody(worldId_, &groundDef);
        b2Polygon groundBox = b2MakeBox(400.0f, 10.0f);
        b2ShapeDef shapeDef = b2DefaultShapeDef();
        b2CreatePolygonShape(groundId, &shapeDef, &groundBox);

        // Falling box (dynamic)
        b2BodyDef boxDef = b2DefaultBodyDef();
        boxDef.type     = b2_dynamicBody;
        boxDef.position = { 400.0f, 100.0f };
        b2BodyId boxId  = b2CreateBody(worldId_, &boxDef);
        b2Polygon box   = b2MakeBox(20.0f, 20.0f);
        b2ShapeDef bsd  = b2DefaultShapeDef();
        bsd.density  = 1.0f;
        bsd.friction = 0.5f;
        b2CreatePolygonShape(boxId, &bsd, &box);
        bodies_.push_back({ boxId, {400,100}, 0.0f, 40, 40 });

        Game::Initialize();
    }

    void LoadContent() override {
        sb_    = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
        pixel_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
        Color w = Color::White;
        pixel_->SetData(&w, 1);
    }

    void Update(const GameTime& gt) override {
        float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());
        accumulator_ += dt;
        const float FDT = 1.0f / 60.0f;
        while (accumulator_ >= FDT) {
            b2World_Step(worldId_, FDT, 4);
            accumulator_ -= FDT;
        }
        for (auto& b : bodies_) b.SyncFromPhysics();
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color(30, 30, 60));
        sb_->Begin();
        // Ground
        sb_->Draw(*pixel_, Rectangle(0, 570, 800, 30), Color(100,80,50));
        // Physics bodies
        for (auto& b : bodies_) {
            sb_->Draw(*pixel_,
                      Rectangle((int)(b.renderPos.X - b.width/2),
                                 (int)(b.renderPos.Y - b.height/2),
                                 b.width, b.height),
                      Color::Orange);
        }
        sb_->End();
        gd.Present();
    }

    void UnloadContent() override {
        b2DestroyWorld(worldId_);
    }

private:
    GraphicsDeviceManager        graphics_;
    std::unique_ptr<SpriteBatch> sb_;
    std::unique_ptr<Texture2D>   pixel_;
    b2WorldId                    worldId_{};
    std::vector<PhysicsBody>     bodies_;
    float                        accumulator_ = 0.0f;
};

int main() { PhysicsDemo game; game.Run(); }

Debug draw of physics shapes

Box2D v3 provides a debug draw callback interface (b2DebugDraw). Wire it up to draw LineList primitives through CNA's GraphicsDevice to visualize physics colliders during development. Disable this in release builds with a preprocessor define.