Introduction
Box2d is a 2D physics engine. It realistically models the interactions between moving and colliding rigid bodies in two dimensions. Box2D does the math required to depict boxes, balls, and polygons moving, colliding, and bouncing across a virtual world. But Box2D doesn't draw anything. To turn this information into a game, you have to get information from Box2d, map it to coordinates useful for drawing, and draw your world.
Box2D uses floating point math and defines objects in MKS: Metres, Kilograms, Seconds. It is meant for objects from about a few centimetres in size up to a few metres. It isn't really meant to model aircraft
carriers or molecules. This means you will create a world in Box2d, set it in motion, then scale and translate (transform) the Box2D information to information suitable for using to draw a game: pixels.
A very useful tool Box2D has is the DebugDraw
facility. By proving a handful of simple drawing routines to box2D, you can quickly get a prototype up and running that shows Box2D
objects as simple vector drawings.
DebugDraw
is also handy to see exactly what is going on inside Box2D
. Even if you have a fancy graphics front end, it could be handy to turn on debugDraw
for debugging.
This project shows how to use set up a very simple box2d
world, and use DebugDraw
to view this world in an MFC form using C++ and GDI+.
For an excellent Box2D
tutorial, see this link.
Setup
(The first thing to do is download and compile the Box2D
library from here. For our project, you will want to make a couple of changes to the Properties of the Box2D
project before compiling:
BOX2D
Project
- Use of MFC: using in a shared DLL
- Character set: Use Unicode character set
These settings assume you are going to link Box2D
with a MFC project using Unicode. It is very important to have the MFC and character set options match or you will get LOTS of link errors. Now we can start our project. Fire up Visual Studio and create a new C++ MFC Application project (or download mine). First thing to do is tell our project we are using the Box2d.lib:
- Open the project configuration properties
- Check that our new project default to using Unicode and MFC in a shared DLL
- Open the Linker section
- Click on input
- Edit "Additional Dependencies"
- Add Box2d.lib (don't add a path)
- Open the VC++ directories
- Edit "Include Directories"
- Add the
Box2D
library path, for example:
- C:\Users\somename\Documents\Visual Studio 20xx\Projects\box2d
- Edit "Library directories"
- Add the
Box2D
path, for example:
- C:\Users\somename\Documents\Visual Studio 2010\Projects\box2d\Build\vs20xx\bin\Debug
- Note you have to add the debug version to your debug configuration, and the release version to your release version.
That takes care of the Project configuration. Next, we have to add the GDU+ required code.
Add this code to stdafx.h:
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
In your Dialog InitInstance
function, add:
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
ExitInstance
isn't normally created by default, but you have to add one to shutdown GDI+.
int Cbox2DTestApp::ExitInstance()
{
GdiplusShutdown(gdiplusToken);
return CWinApp::ExitInstance();
}
And finally, add this to the yourApp::CWinApp
class definition, in the public
section:
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
That takes care of the housekeeping.
Using the Code
Now onto the code. To implement Box2D DebugDraw
, you have to implement a class based on the Box2d
class b2draw
.
Here is the minimum that has to be implemented:
class DebugDrawGDI : public b2Draw
{
public:
DebugDrawGDI();
~DebugDrawGDI();
virtual void DrawPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color);
virtual void DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color);
virtual void DrawCircle(const b2Vec2& center, float32 radius, const b2Color& color);
virtual void DrawSolidCircle(const b2Vec2& center,
float32 radius, const b2Vec2& axis, const b2Color& color);
virtual void DrawSegment(const b2Vec2& p1,
const b2Vec2& p2, const b2Color& color);
virtual void DrawTransform(const b2Transform& xf);
};
By implementing these six drawing functions, Box2D
can draw any world, in a simple debug drawing mode. So all we have to do is code up these six functions in GDI+. Writing these six functions is straightforward; the wrinkle is that Box2D
is going to be sending in box2D
world coordinates, and we have to convert them to Windows pixel coordinates. Most of our code is going to be devoted to setting up the transform that will allow GDI+ to correctly draw the Box2D
data.
Box2D
coordinates are in meters, x
increases from left to right, and y
increases as you go up. GDI+ coordinates are in pixels, x
increases from left to right, but y
increases as you go down.
The technique we will use is to get the size of the GDI+ window, the size of the Box2D
world, and setup a transform matrix we can pass on the graphics.SetTransform()
function. Once we do this, the transform handles all the coordinate mapping, and writing the six graphics becomes trivial.
Getting the GDI+ window is simple:
RECT r;
this->GetClientRect(&r);
Getting the BoxD2
world size takes a bit more work. First, you have to create a Box2D
world, add some"bodies" to it, then query the "world" to see how big it is. For testing, I make a world with rectangle as ground, roof, and walls, and add a couple of dynamic objects inside these walls. Once we have created the world, we can call a few Box2d
functions to iterate over all the bodies and get the world size:
void DebugDrawGDI::GetBoundBox2DBounds(RECT *w, b2World *world)
{
b2Body *b;
b2Fixture *fix;
b2AABB bound;
float minX, maxX, minY, maxY;
minX=minY=1000000.0;
maxX=maxY=-1000000.0;
b=world->GetBodyList();
while ( b )
{
fix=b->GetFixtureList();
while ( fix )
{
bound=fix->GetAABB(0);
if ( bound.lowerBound.x < minX )
minX=bound.lowerBound.x;
if ( bound.upperBound.x > maxX )
maxX=bound.upperBound.x;
if ( bound.lowerBound.y < minY )
minY=bound.lowerBound.y;
if ( bound.upperBound.y > maxY )
maxY=bound.upperBound.y;
fix=fix->GetNext();
}
b=b->GetNext();
}
maxX+=2.0;
maxY+=2.0;
minX-=2.0;
minY-=2.0;
w->left=(long )minX;
w->right=(long )maxX;
w->bottom=(long )minY;
w->top=(long )maxY;
}
The Transform!
Now that we know how big everything is, we can calculate our transform:
void DebugDrawGDI::ScaleWorldCalculate(RECT *r, RECT *w)
{
int outputWidth = r->right - r->left;
int outputHeight = r->bottom - r->top;
int boundsWidth = w->right - w->left;
int boundsHeight = w->top - w->bottom;
scaleX = (float )outputWidth / (float )boundsWidth;
scaleY = (float )outputHeight / (float )boundsHeight;
scale = scaleX > scaleY ? scaleY : scaleX;
offsetX=r->left - (int )((float )w->left * scaleX);
offsetY=r->top - (int )((float )w->bottom * scaleY);
yAdjust=r->bottom;
matrixTransform.Reset();
matrixTransform.Scale(scaleX, -scaleY, MatrixOrderAppend);
matrixTransform.Translate((float )offsetX, (float )(yAdjust-offsetY), MatrixOrderAppend);
}
Then in our main drawing loop, we just have to set the transform, something like this:
g->SetTransform(&matrixTransform);
Where "g
" is a pointer to our current Graphics
object. With the transform in place, our drawing functions look like this:
void DebugDrawGDI::DrawSolidPolygon(const b2Vec2* vertices,
int32 vertexCount, const b2Color& color)
{
int i;
PointF *points=new PointF[vertexCount+1];
Color clr(255, (int )(color.r*255), (int )(color.g*255), (int )(color.b*255));
SolidBrush sb(clr);
for (i = 0; i < vertexCount; ++i, ++vertices)
{
points[i].X = vertices->x;
points[i].Y = vertices->y;
}
points[vertexCount].X = points[0].X;
points[vertexCount].Y = points[0].Y;
gdi->FillPolygon(&sb, points, vertexCount + 1);
delete points;
}
Note that we do no scaling or mapping in the drawing function. The transform takes care of it all for us.
All that is left is to setup the usual MFC animation code.
We have to add a timer to trigger our updates, and add an on_paint
event handler to draw everything.
Code like this to the InitDialog
function to setup a timer:
timerMilliseconds=16;
stepSeconds=(float )timerMilliseconds / 1000.0f;
SetTimer(1234567890, timerMilliseconds, NULL);
As well, the code to create the Box2D
world, an instance of the DebugDraw
class, etc. goes into InitInstance
.
All the logical code goes into the On_Timer
event handler, and all the drawing code goes into the On_Paint
event handler. Download the project and have a look at the code to see the rest of the details.
Collisions
Wouldn't it be nice if Box2D
could tell you when something collided? Happily, it does, and you don't have to look at everybody in the world each frame, you can be notified of collisions with a callback function. Here is the definition of the Collision Listener class:
class MyContactListener : public b2ContactListener
{
public:
void BeginContact(b2Contact* contact);
void EndContact(b2Contact* contact);
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);
};
Our simple example code only implements BeginContact
and saves some data so we can draw an "explosion" each time our falling box bounces off (Collides) with something. We'll want to save some data about the collision:
typedef struct
{
int id;
int isContact;
float x, y;
float r;
int countDown;
}
t_bodyData;
In InitDialog
we tell Box2D
to use our callback. "world
" is the Box2D
object we created.
world->SetContactListener(ContactListener);
And some simple code to see what collided, and if we are interested in the collision.
void MyContactListener::BeginContact(b2Contact* contact)
{
t_bodyData *bd;
bd=(t_bodyData *)(contact->GetFixtureA()->GetBody()->GetUserData());
if ( bd && bd->id == BALL_ID )
{
bd->isContact=true;
bd->r=5;
b2Vec2 v;
v=contact->GetFixtureA()->GetBody()->GetPosition();
bd->x=v.x;
bd->y=v.y;
bd->countDown=30;
}
else
{
bd=(t_bodyData *)(contact->GetFixtureB()->GetBody()->GetUserData());
if ( bd && bd->id == BALL_ID )
{
bd->isContact=true;
bd->r=1.5;
b2Vec2 v;
v=contact->GetFixtureB()->GetBody()->GetPosition();
bd->x=v.x;
bd->y=v.y;
bd->countDown=30;
}
}
}
Then we add code to the timer event handler to animate our explosion, and to paint to draw our explosion which is just an expanding and contracting circle. That covers the highlights of this project. Download the complete project to play with a very simple, but complete, Box2D
DebugDraw
implementation for GDI+ under MFC.
Draw a Bitmap on Top of a Box
You might want to draw a bitmap on top of a box2d
item. This code shows the basic technique. In the initialization code, save a pointer to the box body called b2box
. Then add this code to MainDraw()
.
In CreateBox2dWorld
:
...
def.type=b2_dynamicBody;
def.position.Set(0.0, 45.0);
b2box=body=world->CreateBody(&def);
...
In Maindraw
, add variable declarations at the top, the rest of the code just after world->DrawDebugData();
Bitmap *boxImage;
Matrix m;
boxImage=new Bitmap(_T("test.bmp"));
PointF p;
float angle=b2box->GetAngle();
b2Vec2 pos=b2box->GetPosition();
angle*=-RADTODEGREES; p.X=DebugDraw->ScaleXF(pos.x);
p.Y=DebugDraw->ScaleYF(pos.y);
p.X+=boxImage->GetWidth()/2; p.Y+=boxImage->GetHeight()/2;
m.Reset();
m.Translate((float )(-((int)boxImage->GetWidth())/2),
(float )(-((int )boxImage->GetHeight())/2));
m.RotateAt(angle, p);
graphics->SetTransform(&m);
p.X-=boxImage->GetWidth()/2;
p.Y-=boxImage->GetHeight()/2;
graphics->DrawImage(boxImage, p);
Next Steps
It flickers pretty bad. There are standard techniques to avoid flicker and they could be used to stop the flicker. I'm not sure a dialog is the best thing for a game, but it was easy to get working. It might be better to setup a plain Win32 application for a full screen game. There are also C# ports of the Box2D
library which you could use with XNA.
Linker Errors
If you get linker errors, make sure that you compiled the box2d
project with the same settings as your project: language, MFC, and ATL. Details are in the article, but I mention it again in case you download everything and just try to compile it. The default box2d
project has different settings from a default MFC project.
History