Click here to Skip to main content
15,885,435 members
Articles / Multimedia / GDI+

Box2D DebugDraw with MFC

Rate me:
Please Sign up or sign in to vote.
4.71/5 (8 votes)
6 Oct 2013CPOL7 min read 27.8K   750   18   1
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
  • 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:

C++
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")

In your Dialog InitInstance function, add:

C++
// 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+.

C++
// 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:

C++
// 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:

C++
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:

C++
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:

C++
 // 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:

C++
// 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:

C++
g->SetTransform(&matrixTransform); 

Where "g" is a pointer to our current Graphics object. With the transform in place, our drawing functions look like this:

C++
/// 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:

C++
// 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:

C++
// 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:

C++
// 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.

C++
 // 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:

C++
 // 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();

C++
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Canada Canada
Professional Programmer living in Beautiful Vancouver, BC, Canada.

Comments and Discussions

 
QuestionFixed a bug in the function void DebugDrawGDI::ScaleWorldCalculate(RECT *r, RECT *w) Pin
arussell1-Oct-13 14:23
professionalarussell1-Oct-13 14:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.