Box2D DebugDraw with MFC
Implement the Box2D DebugDraw functions in MFC
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- Configuration Properties
- General
- Configuration Properties
- 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
- Add Box2d.lib (don't add a path)
- 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.
- Add the
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:
// gdi plus required startup code
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
ExitInstance
isn't normally created by default, but you have to add one to shutdown GDI+.
// must add this override to shutdown gdiplus
int Cbox2DTestApp::ExitInstance()
{
// required gdiplus shutdown code
GdiplusShutdown(gdiplusToken);
return CWinApp::ExitInstance();
}
And finally, add this to the yourApp::CWinApp
class definition, in the public
section:
// gdi stuff, required for startup and shutdown
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();
// these are the box2d virtual functions we have to implement
/// Draw a closed polygon provided in CCW order.
virtual void DrawPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color);
/// Draw a solid closed polygon provided in CCW order.
virtual void DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color);
/// Draw a circle.
virtual void DrawCircle(const b2Vec2& center, float32 radius, const b2Color& color);
/// Draw a solid circle.
virtual void DrawSolidCircle(const b2Vec2& center,
float32 radius, const b2Vec2& axis, const b2Color& color);
/// Draw a line segment.
virtual void DrawSegment(const b2Vec2& p1,
const b2Vec2& p2, const b2Color& color);
/// Draw a transform. Choose your own length scale.
/// @param xf a transform.
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:
// set w to the box2D world AABB
// use this to help scale/transform our world
void DebugDrawGDI::GetBoundBox2DBounds(RECT *w, b2World *world)
{
// iterate over ALL the bodies, and set the w to max/min
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:
// r is the rect for the windows drawing window, w is the extent of the box2d world
// r is assumed to have y increasing down, w is assumed to have y increasing up
// r is in pixels, w is in metres
// you can use this function to select any part of the box2d world
// to scale instead of the whole thing, which is the default
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;
// ratio of the windows size to the world size
scaleX = (float )outputWidth / (float )boundsWidth;
scaleY = (float )outputHeight / (float )boundsHeight;
scale = scaleX > scaleY ? scaleY : scaleX;
// move things over if required
offsetX=r->left - (int )((float )w->left * scaleX);
offsetY=r->top - (int )((float )w->bottom * scaleY);
// used to flip the y values
yAdjust=r->bottom;
// make a transform matrix
matrixTransform.Reset();
// scale (-y as part of our y flip)
matrixTransform.Scale(scaleX, -scaleY, MatrixOrderAppend);
// translate (+yAdjust is part of the y flip)
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:
/// Draw a solid closed polygon provided in CCW order.
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:
// animation timer, note box2d will not be realistic if you run the timer too slow
// 16 is about 60Hz, 32 is about 30Hz
timerMilliseconds=16;
// make the box2d world run in sync with our timer
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:
// the collision callback 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:
// some data about a collision,
// and the simple explosion we will draw
// at the collision point
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.
// setup the collision callback world->SetContactListener(ContactListener);
And some simple code to see what collided, and if we are interested in the collision.
// the box2d callback function. boxd will call this if there is a collision
void MyContactListener::BeginContact(b2Contact* contact)
{
// let's see which object is our ball.
// A & B collided, our ball could be either
t_bodyData *bd;
bd=(t_bodyData *)(contact->GetFixtureA()->GetBody()->GetUserData());
if ( bd && bd->id == BALL_ID )
{
// this is our ball, save some data about where it collided
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 )
{
// this is our ball, save some data about where it collided
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
:
// a falling box
...
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"));
// draw the bitmap on top of the box --- ---
PointF p;
float angle=b2box->GetAngle();
b2Vec2 pos=b2box->GetPosition();
// move our bitmap to where the box is and rotate it to match
angle*=-RADTODEGREES; // negative so it rotates the right direction
p.X=DebugDraw->ScaleXF(pos.x);
p.Y=DebugDraw->ScaleYF(pos.y);
p.X+=boxImage->GetWidth()/2; // make rotation in centre of our box
p.Y+=boxImage->GetHeight()/2;
m.Reset();
// box2d position is at the center of its object, move our object to match
m.Translate((float )(-((int)boxImage->GetWidth())/2),
(float )(-((int )boxImage->GetHeight())/2));
// rotate to match box2d
m.RotateAt(angle, p);
graphics->SetTransform(&m);
// move the point back because the translation handles that adjustment
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
- Initial version