Tutorial 96: Physics Integration (Box2D / Bullet)
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.