When I first started integrating Box2D into my Cocos2D project, I found the whole process a little jarring. While I’d used C++ before, this was the first time I’d seen it mixed with Objective-C. This code snippet simply wraps the standard Box2D Debug calls from GLES-Render.h (included with Cocos2D) in it’s own CCLayer, so that you can implement it quickly and easily. It also allows you to turn the layer on an off easily as you would any other CCLayer.
Please note, that this is not an introduction to using Box2D with Cocos2D. This is intended as a drop-in class that you can use to simplify the process of displaying debug information from Box2D. For more information about using Box2D - see Ray Wenderlich’s great post on the subject.
Note: This class has now been included in Kobold2D (since v1.0.1). Kobold2D is a beginner friendly version of Cocos2D which includes a number of additional, commonly used libraries already integrated with it. If you just want to use this class out of the box without any setup and/or if you’re just starting out with Cocos2D, it’s definitely worth checking out Kobold2D.
In order to implement this class, you will need to be ensure that the relevant Box2D files are included in your project, as well as the default GLES-Render.h that comes packaged with the Cocos2D Box2D project template. GLES-Render.h provides all of the low-level calls required by Box2D to draw the objects within a Box2D world to the screen.
So, to create our CCLayer which draws the Box2D debug information, we create the class BoxDebugLayer, which inherits from CCLayer. This layer needs to maintain an instance of the GLESDebugDraw class (from GLES-Render - used to do the actual rendering). We also keep hold of a pointer to the world and the PTM ratio for good measure.
BoxDebugLayer.h (Download)
#import "cocos2d.h"
#import "Box2D.h"
#import "GLES-Render.h"
@interface BoxDebugLayer : CCLayer {
GLESDebugDraw* _debugDraw; // The DebugDraw instance from GLES-Render
b2World *_boxWorld; // The Box2D world this is attached to
int _ptmRatio; // The PTM Ratio used in this world
}
/** Return an autoreleased debug layer */
+(BoxDebugLayer *)debugLayerWithWorld:(b2World *)world ptmRatio:(int)ptmRatio;
+(BoxDebugLayer *)debugLayerWithWorld:(b2World *)world ptmRatio:(int)ptmRatio flags:(uint32)flags;
/** Initialise a debug layer with the given parameters. */
-(id)initWithWorld:(b2World *)world ptmRatio:(int)ptmRatio;
-(id)initWithWorld:(b2World *)world ptmRatio:(int)ptmRatio flags:(uint32)flags;
@end
BoxDebugLayer.mm (Download)
#import "BoxDebugLayer.h"
@implementation BoxDebugLayer
/** Create a debug layer with the given world and ptm ratio */
+(BoxDebugLayer *)debugLayerWithWorld:(b2World *)world ptmRatio:(int)ptmRatio
{
return [[[BoxDebugLayer alloc] initWithWorld:world ptmRatio:ptmRatio] autorelease];
}
/** Create a debug layer with the given world, ptm ratio and debug display flags */
+(BoxDebugLayer *)debugLayerWithWorld:(b2World *)world ptmRatio:(int)ptmRatio flags:(uint32)flags
{
return [[[BoxDebugLayer alloc] initWithWorld:world ptmRatio:ptmRatio flags:flags] autorelease];
}
/** Create a debug layer with the given world and ptm ratio */
-(id)initWithWorld:(b2World*)world ptmRatio:(int)ptmRatio
{
return [self initWithWorld:world ptmRatio:ptmRatio flags:b2DebugDraw::e_shapeBit];
}
/** Create a debug layer with the given world, ptm ratio and debug display flags */
-(id)initWithWorld:(b2World*)world ptmRatio:(int)ptmRatio flags:(uint32)flags
{
if ((self = [self init])) {
_boxWorld = world;
_ptmRatio = ptmRatio;
_debugDraw = new GLESDebugDraw( ptmRatio );
_boxWorld->SetDebugDraw(_debugDraw);
_debugDraw->SetFlags(flags);
}
return self;
}
/** Clean up by deleting the debug draw layer. */
-(void)dealloc
{
_boxWorld = NULL;
if ( _debugDraw != NULL ) {
delete _debugDraw;
}
[super dealloc];
}
/** Tweak a few OpenGL options and then draw the Debug Layer */
-(void)draw
{
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glPushMatrix();
glScalef( CC_CONTENT_SCALE_FACTOR(), CC_CONTENT_SCALE_FACTOR(), 1.0f);
_boxWorld->DrawDebugData();
glPopMatrix();
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}
@end
Using this class
Using this class in your code is as simple as just adding this layer as a child to your game layer. You need to inform the BoxDebugLayer class of which box2DWorld you would like it to attach to, and what your PTM ratio is for this world. Optionally, you can also provide the flags for which debug information you would like in the init method.
Adding this layer to your project is as simple as…
[self addChild:[BoxDebugLayer debugLayerWithWorld:_boxWorld ptmRatio:GAME_PTM_RATIO] z:10000];
If you would like to enable specific flags, use the alternative constructor…
uint32 flags = b2DebugDraw::e_aabbBit | b2DebugDraw::e_centerOfMassBit | b2DebugDraw::e_jointBit | b2DebugDraw::e_pairBit | b2DebugDraw::e_shapeBit;
[self addChild:[BoxDebugLayer debugLayerWithWorld:_boxWorld ptmRatio:GAME_PTM_RATIO flags:flags] z:10000];
Limitations
There are some limitations of this code as it stands. As each DebugLayer has to register itself with the box2D world, adding two debug layers to the same world will not work as expected. The second world will likely override the first debug layer, making the first defunct.
I also haven’t added any code to allow customisation of how the debug layer is drawn. In my own code, I simply change the GLES-Render.m file if I ever need to tweak how the debug information is drawn. This usually only includes changing a line width, so that the debug-lines are clearly visible over my artwork. However, if you have the time and inclination - I suspect you could add these options to this class, making it easier to customise debug information within the Box2D Debug Layer.
Explanation
If you are interested in how this layer works, then I’ll talk through the main methods below.
initWithWorld:ptmRatio:flags: is the main initialisation method that will ultimately be called to create this object (calling the shorter init method calls this one with default options).
This method maintains a local copy of the box2D world we’re working with and the PTM ratio. We then create an instance of the GLESDebugDraw class provided to us in GLES-Render.h (packaged with Cocos2D). GLESDebugDraw implements a number of methods that Box2D expects to use when drawing debug information to the screen. This class handles the code that does the actual drawing of the debug information even though the Box2D library will be driving that code to draw the information.
There are a number of flags you can provide to inform GLESDebugDraw what you would like to see drawn. The following options exist;
- b2DebugDraw::e_aabbBit - Draw the axis aligned bounding boxes.
- b2DebugDraw::e_centerOfMassBit - Draw the centre of Mass for bodies.
- b2DebugDraw::e_jointBit - Draw any joints between fixtures.
- b2DebugDraw::e_pairBit - Draw broad-phase pairs.
- b2DebugDraw::e_shapeBit - Draw outline shapes for fixtures / bodies.
Naturally, in our dealloc method we clean up after ourselves. As the Box2D and GLESDebugDraw classes aren’t NSObjects / objective-C, we don’t use the normal retain / release mechanics. Instead we delete them the old fashioned C++ way.
The draw method prepares OpenGL for the debug draw operation. Whereas with Cocos2D we’ve been drawing textured sprites, the debug information wants to make use of the primitive drawing methods. As such, before we call GLESDebugDraw we have to disable all of the textured drawing features of OpenGL so that it only expects to receive primitive information.
We then scale the world to account for the retina display (if required) before asking Box2D to draw the information. This is based on the assumption that you will be using one world size for your iPhone app regardless of whether it’s retina or not - so we need to scale the points to pixels in OpenGL. Last but not least, we return OpenGL the state it was in before we did our work.
That’s it. If you find this useful, add anything to your own implementation or think this class needs any additional features - please leave a comment below.