Wrapping Box2D Debug into a Cocos2D Layer

John WordsworthiOS Development18 Comments

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.

John WordsworthWrapping Box2D Debug into a Cocos2D Layer

18 Comments on “Wrapping Box2D Debug into a Cocos2D Layer”

  1. F Rueger

    John: Thank you. Thank you. Thank you. I have been wrecking my brain the last 24 hours on how to do this. I have a PhD too, just not in Applied Math — this is excellent, and if I ever get to meet you in a pub, and you come round to Maine, I owe you a beer.

    1. John Wordsworth

      Haha, very kind! I’m glad that someone found this post useful – I thought it might have been a little too specialised to be found. I hope you enjoyed your PhD as much as I did – it was a wonderful experience (apart from the last 4 months or so!).

      Thanks for leaving a comment. Best of luck with your Cocos2D project(s).

      1. Jady

        Hi John Wordsworth, would you help tell me the tool’s name which generate the animation file? (Basic-knight-def.plist) I think it is very useful, and where can i download it? Thanks!

  2. Pingback: Kobold2D v1.0.1 now available! | Learn & Master Cocos2D Game Development

  3. Marcio Valenzuela

    I just tried this but it doesn’t work. I actually get an error now saying include cassert from box2d b2Settings class file. Actually now even if i get rid of the code I added my project won’t run…

  4. Marcio Valenzuela

    I had to remove the complete BoxDebugLayer Class files and then my project ran fine. I re-added them and re-added the code in the init method and again i get the lexical preprocessor cassert file not found. What could be causing this?

    1. Steven

      your file its usually

      BoxDebugLayer.h
      BoxDebugLayer.m

      but change it to:

      BoxDebugLayer.h
      BoxDebugLayer.mm

      Double M at the end. that fixes your problem.

  5. Marcio Valenzuela

    I didn’t rename the file .mm. Got that fixed, now when loading the box2d layer it crashes in b2World.cpp saying EXC BAD ACCESS at this line:

    void b2World::SetDebugDraw(b2DebugDraw* debugDraw)
    {
    m_debugDraw = debugDraw; //<—here
    }

  6. brad

    For cocos2d 2.0, make the follow changes:

    replace the draw method with:

    -(void)draw
    {
    ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position | kCCVertexAttribFlag_Color);

    _boxWorld->DrawDebugData();

    }

    and initWithWorld with:

    -(id)initWithWorld:(b2World*)world ptmRatio:(int)ptmRatio
    {
    return [self initWithWorld:world ptmRatio:ptmRatio flags:b2Draw::e_shapeBit];
    }

    Thanks it! Works perfect! Thanks!

  7. MaK

    Thanks for this one! It’s so much cleaner when the debug drawing is done on another layer and in a separate class. Good thinking!

  8. Scott

    Oh my actual days…

    Grabbed those files – made sure that the implementation file was .mm and not .m to stop the cassert issue, and imported “BoxDebugLayer.h” in to my level.h file – then just changed a the world name, and the PTM_RATIO lines, to what is defined in my project:

    [self addChild:[BoxDebugLayer debugLayerWithWorld: myWorldName ptmRatio:PTM_RATIO] z:10000];

    And it just worked!

    Thank you so much for posting this code! Literally took 2 minutes top, and it works perfectly for me. Literally just wanted to see my level design as it appears in the box2d world, to make sure my b2vecs were correct!

    Thanks again!!

  9. contract surety bond

    Please let me know if you’re looking for a article author for
    your weblog. You have some really great articles and I feel I would be a good asset.
    If you ever want to take some of the load off, I’d love to write some material for your blog in exchange for a link back to mine.
    Please shoot me an e-mail if interested. Kudos!

  10. bid bond

    This is really interesting, You are a very skilled blogger.
    I have joined your feed and look forward to seeking more of your excellent post.
    Also, I have shared your site in my social networks!

Leave a Reply

Your email address will not be published. Required fields are marked *