Tutorial 96: Physics Integration (Box2D / Bullet)
What you’ll learn
- Why CNA ships no physics engine, and what that means for your architecture.
- Integrating Box2D v3 for 2D and Bullet for 3D.
- Stepping the simulation from
Update()and syncing bodies back to CNA transforms. - Drawing physics shapes for debugging.
Before you start — Tutorial 48: Fixed vs Variable Timestep (physics wants a fixed step) and Tutorial 16: Basic Collision Detection (the built-in collision this replaces).
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.
Third-party APIs move. The Box2D and Bullet calls on this page are third-party code that CNA neither ships nor tests. Box2D v3 in particular is a young C API whose function names and structures have changed between releases (later releases, for example, read a body’s angle through its rotation rather than b2Body_GetAngle), so check the calls below against the headers of the revision you clone. Only the CNA side — the game loop, the transform sync and the drawing — is verified against CNA itself.
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. CNA’s default game loop already is one: with
IsFixedTimeStep left at true, every Update receives exactly
TargetElapsedTime (1/60 s), except the game’s very first Update, whose
ElapsedGameTime is 0. So the simplest correct integration is to step the physics world once per
Update with a constant step (see Tutorial 48). The accumulator below is for the
other cases: a variable-step game (IsFixedTimeStep = false), or a physics rate that differs from the update rate.
// Box2D v3: world step
void Update(GameTime& gt) override {
Game::Update(gt);
float dt = static_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty());
// Accumulate time and step in fixed increments (dt is 0 on the very first Update)
accumulator_ += std::min(dt, 0.25f); // clamp so one long stall cannot cause a burst of steps
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/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Rectangle.hpp"
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "box2d/box2d.h"
#include <algorithm>
#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_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
}
// The physics world is not a GPU resource, so free it in the destructor. (UnloadContent() runs
// only when the game is disposed with Dispose(), not when a stack-allocated game simply goes out of scope.)
~PhysicsDemo() {
if (worldCreated_) b2DestroyWorld(worldId_);
}
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);
worldCreated_ = true;
// 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(GameTime& gt) override {
Game::Update(gt);
float dt = static_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty());
accumulator_ += std::min(dt, 0.25f);
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& gt) 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();
Game::Draw(gt); // Game presents the frame afterwards (no gd.Present())
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> sb_;
std::unique_ptr<Texture2D> pixel_;
b2WorldId worldId_{};
bool worldCreated_ = false;
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 collect line segments and draw them as
PrimitiveType::LineList through CNA's GraphicsDevice to visualize physics
colliders during development. Disable this in release builds with a preprocessor define. The CNA half looks like this:
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/PrimitiveType.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionColor.hpp"
#include "CNA/GraphicsCapability.hpp"
// Two vertices per segment, in pixel coordinates; your b2DebugDraw callbacks call this.
void AddDebugLine(std::vector<VertexPositionColor>& v, Vector2 a, Vector2 b, Color c) {
v.push_back(VertexPositionColor(Vector3(a.X, a.Y, 0), c));
v.push_back(VertexPositionColor(Vector3(b.X, b.Y, 0), c));
}
void DrawDebugLines(GraphicsDevice& gd, BasicEffect& fx,
const std::vector<VertexPositionColor>& lineVerts) {
// User primitives need a 3D-capable renderer (see the note below)
if (lineVerts.empty() || !gd.SupportsCapability(CNA::GraphicsCapability::ThreeD)) return;
const int w = gd.getViewportProperty().getWidthProperty();
const int h = gd.getViewportProperty().getHeightProperty();
fx.VertexColorEnabled = true;
fx.setWorldProperty(Matrix::getIdentityProperty());
fx.setViewProperty(Matrix::getIdentityProperty());
fx.setProjectionProperty(Matrix::CreateOrthographicOffCenter( // pixel space, Y down
0.0f, (float)w, (float)h, 0.0f, 0.0f, 1.0f));
for (auto& pass : fx.getCurrentTechniqueProperty()->getPassesProperty()) {
pass.Apply();
gd.DrawUserPrimitives(PrimitiveType::LineList, lineVerts, 0,
static_cast<int>(lineVerts.size() / 2));
}
}
The 2D-only renderer (SDL_RENDERER) cannot draw user primitives; on those, draw each segment as a thin rotated rectangle with
SpriteBatch instead. The helper above therefore checks GraphicsCapability::ThreeD first, and the effect needs
a BasicEffect created in LoadContent().