Click here to Skip to main content
15,881,840 members
Articles / Multimedia

Box2D and Direct2D

Rate me:
Please Sign up or sign in to vote.
4.00/5 (3 votes)
10 Oct 2013CPOL5 min read 30.1K   853   14  
Box2D DebugDraw Implemented using DirectX 2D and win32

Introduction

Click here for the GDI+ version.  

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 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: Meters, Kilograms, Seconds. It is meant for objects from about a few centimeters in size up to a few meters. 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.

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 a bare bones win32 DirectX 2D program. For an excellent Box2D tutorial click here.

Setup

The first thing to do is download and compile the Box2D library from http://box2d.org/. For our project you will want to make a couple of changes to the Properties of the Box2D project before compiling:

  • BOX2D
    • Configuration Properties
      • General 
        • Character set: Use Unicode character set

These settings assume you are going to link Box2D with a win32 project using Unicode. It is very important to have the character set options match or you will get LOTS of linker errors.

Now we can start our project. Fire up visual studio and create a new Win32 Application project (or download mine). Choose C++/Win32/Win32 Project. Be sure the set the Project Name. 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
  • Open the Linker section
    • Click on input
    • Additional Dependancies"
    • 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 DirectX 2D required headers. At the bottom of stdafx.h add:

    C++
    #include <d2d1.h>
    #include <Box2D/Box2D.h>

    In your main cpp file add the following to tell the compiler you are using DirectX 2D:

    C++
    #pragma comment(lib, "d2d1")

    That takes care of the housekeeping.

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 DirectX 2D. 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 DirectX 2D to correctly draw the Box2D data.

Box2D coordinates are in meters, x increases from left to right, and y increases as you go up. DirectX 2D 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 DirectX 2D window, the size of the Box2D world, and setup a transform matrix we can pass on to the renderTarget->SetTransform () function. Once we do this the transform handles all the coordinate mapping, and writing the six graphics becomes trivial.

Getting the win32 window size requires the HWND of the main window. To make life easier I add a global variable to save it in:

C++
HWND hWndGlobal;  // we will save the hwnd here for future use 

Then in the InitInstance() add this just before the call to ShowWindow():

C++
hWndGlobal=hWnd; // save a copy, it's handy to know 

Now we get the window size in the _twinMain() by:

C++
RECT rect;
GetClientRect(hWndGlobal, &rect);

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->top=(long )maxY;
    w->bottom=(long )minY;

}

Now that we know how big everything is we can calculate our transform:

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->top=(long )maxY;
    w->bottom=(long )minY;
 
}

Then in our main drawing loop we just have to set the transform, something like this: 

C++
renderTarget->SetTransform(matrixTransform);

Where renderTarget is a pointer to our current DirectX 2D render factory 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;
    ID2D1PathGeometry *geo;
    ID2D1GeometrySink *sink;
    ID2D1SolidColorBrush *brush;
    D2D1::ColorF dColor(color.r, color.g, color.b);
    D2D1_POINT_2F *points=new D2D1_POINT_2F [vertexCount+1];
    HRESULT hr;
 
    // create a direct2d pathGeometry
    hr=factory->CreatePathGeometry(&geo);
    hr=geo->Open(&sink);
    sink->SetFillMode(D2D1_FILL_MODE_WINDING);
    // first point
    sink->BeginFigure(D2D1::Point2F(vertices[0].x, vertices[0].y), D2D1_FIGURE_BEGIN_FILLED);
    // middle points
    vertices++;
    vertexCount--;
    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;
    sink->AddLines(points, vertexCount);
    // close it
    sink->EndFigure(D2D1_FIGURE_END_CLOSED);
    sink->Close();
    SafeRelease(&sink);
    
    renderTarget->CreateSolidColorBrush(dColor, &brush);
    renderTarget->FillGeometry(geo, brush);
 
    delete points;
    SafeRelease(&geo);
}

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 some animation and timing animation code. We have to change the default message loop so it doesn't just wait for messages. Not much animation happens if your drawing code only gets called when a key is pressed. Use this code to process messages:

C++
// prime the message structure
    PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE);
// run till completed
while (msg.message!=WM_QUIT) 
{
    // is there a message to process?
    if (PeekMessage( &msg, NULL, 0, 0, PM_REMOVE)) 
    {
        // dispatch the message
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    } 
    else 
    {
        // no windows messages, do our game stuff
        if (  ajrMain.MainLogic() )
        {
            ajrMain.MainDraw();
        }
    }
}

ajrMain.MainLogic() does all the moving and timing of objects, and ajMain.MainDraw() draws the current world. 

I created a new class to handle all the DirectX 2D stuff, timing, and logic. I added this, just above the message loop to create and initialize the object:<o:p>

CAjrMain ajrMain(&rect, hWndGlobal); // my main class to do everything
ajrMain.CreateBox2dWorld(); 

CAjrMain has code to initialize DirectX 2D, load a bitmap, do the simple game logic and timing. I put it all in a separate cpp file. I used the QueryPerformanceCounter(); to time the animation.

As well, the code to create the Box2D world, an instance of the DebugDraw class, etc… goes into the CAjrMain class.  

See the GDI+ version (link at top of article) for some simple collision detection code. Box2d has very good collision detection support using callback function.

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

 
-- There are no messages in this forum --