Box2D Tutorial for iOS: How to Use Box2D For Just Collision Detection With Cocos2D iPhone

A Box2D tutorial for iOS on how to use Box2D for just collision detection, in order to make detecting collisions for polygon shapes easy and accurate. By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Creating Box2D Bodies for the Sprites

The next step is to create Box2D bodies for each sprite so Box2D knows where they are – and hence can tell us when they collide! We will do this similar to how we did in the previous Box2D tutorials.

However the difference is instead of updating our sprites to be where Box2D tells us they should be, we are going to update the Box2D bodies to where the sprites are and let Cocos2D control their movement.

So let’s begin by creating our world. Open up HelloWorldScene.h and add the Box2D header to the top of the file:

#import "Box2D.h"

Then add the following member variable to HelloWorldScene.h:

b2World *_world;

Then add the following code to your init method in HelloWorldScene.mm:

b2Vec2 gravity = b2Vec2(0.0f, 0.0f);
bool doSleep = false;
_world = new b2World(gravity, doSleep);

Note two important things here. First, we set the gravity vector to 0 since we don’t want these objects moving around artificially. Second we tell Box2D not to let the objects go to sleep. This is important because since we’re artificially moving the objects, they will tend to fall asleep unless we set this.

Then add the following method above spawnCat:

- (void)addBoxBodyForSprite:(CCSprite *)sprite {
 
    b2BodyDef spriteBodyDef;
    spriteBodyDef.type = b2_dynamicBody;
    spriteBodyDef.position.Set(sprite.position.x/PTM_RATIO, 
        sprite.position.y/PTM_RATIO);
    spriteBodyDef.userData = sprite;
    b2Body *spriteBody = _world->CreateBody(&spriteBodyDef);
    
    b2PolygonShape spriteShape;
    spriteShape.SetAsBox(sprite.contentSize.width/PTM_RATIO/2,
                         sprite.contentSize.height/PTM_RATIO/2);
    b2FixtureDef spriteShapeDef;
    spriteShapeDef.shape = &spriteShape;
    spriteShapeDef.density = 10.0;
    spriteShapeDef.isSensor = true;
    spriteBody->CreateFixture(&spriteShapeDef);
    
}

This code should look familiar to you – it’s the same as we did in the breakout tutorials. However, there is one difference – we set isSensor to true on the shape definition.

According to the Box2D Manual, you should set isSensor to true when you want to know when objects will collide without triggering a collision response. This is exactly what we want!

Next call that method from spawnCat, right before you call addChild:

[self addBoxBodyForSprite:cat];

And do the same thing for spawnCar:

[self addBoxBodyForSprite:car];

We need to remember to destroy the Box2D bodies when the sprites get destroyed. So replace your spriteDone method with the following:

- (void)spriteDone:(id)sender {
    
    CCSprite *sprite = (CCSprite *)sender;
    
    b2Body *spriteBody = NULL;
    for(b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) {
        if (b->GetUserData() != NULL) {
            CCSprite *curSprite = (CCSprite *)b->GetUserData();
            if (sprite == curSprite) {
                spriteBody = b;
                break;
            }
        }
    }
    if (spriteBody != NULL) {
        _world->DestroyBody(spriteBody);
    }
    
    [_spriteSheet removeChild:sprite cleanup:YES];
    
}

Now, the most important part of all of this. We need to update the positions of the Box2D bodies periodically as the sprites move. So add the following to your init method:

[self schedule:@selector(tick:)];

And then write your tick method as the following:

- (void)tick:(ccTime)dt {
    
    _world->Step(dt, 10, 10);
    for(b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) {
        if (b->GetUserData() != NULL) {
            CCSprite *sprite = (CCSprite *)b->GetUserData();
            
            b2Vec2 b2Position = b2Vec2(sprite.position.x/PTM_RATIO,
                                       sprite.position.y/PTM_RATIO);
            float32 b2Angle = -1 * CC_DEGREES_TO_RADIANS(sprite.rotation);
            
            b->SetTransform(b2Position, b2Angle);
        }
    }
    
}

This is similar to the tick method we wrote in our breakout project, except this time we are updating the position of the Box2D object based on the position of the Cocos2D sprite rather than the other way around.

Compile and run the project… and it should look exactly the same as before. So similar, in fact, that you might wonder if this is even working. Well let’s wonder no more – time to enable debug drawing!

Enabling Box2D Debug Drawing

Since you started with the Box2D template, your project should already have the files GLES-Render.h and GLES-Render.mm included in the project, which has all of the code necessary for Box2D debug drawing. All we have to do is turn this on in our project.

Let’s start by adding the following include at the top of HelloWorldScene.h:

#import "GLES-Render.h"

Then add the following member variable to the HelloWorld object:

GLESDebugDraw *_debugDraw;

Next, add the following to your init method:

// Enable debug draw
_debugDraw = new GLESDebugDraw( PTM_RATIO );
_world->SetDebugDraw(_debugDraw);

uint32 flags = 0;
flags += b2DebugDraw::e_shapeBit;
_debugDraw->SetFlags(flags);

This is the code that creates an instance of the GLESDebugDrawClass and registers it with the world object. We pass it a flag specifying what we want it to draw – here we specify that we want it to draw the Box2D shapes. For a list of the other flags you can set, take a look at b2WorldCallbacks.h.

Next, we need to add a draw method. Add the following underneath the init method:

-(void) draw
{
	glDisable(GL_TEXTURE_2D);
	glDisableClientState(GL_COLOR_ARRAY);
	glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	
	_world->DrawDebugData();
	
	glEnable(GL_TEXTURE_2D);
	glEnableClientState(GL_COLOR_ARRAY);
	glEnableClientState(GL_TEXTURE_COORD_ARRAY);	
}

To be honest I am still new to OpenGL so I’m not quite sure what these OpenGL calls are doing. However, this is the standard boilerplate code from the template to get debug drawing set up and it works :]

One final note, let’s add our dealloc method while we’re thinking of it:

- (void)dealloc {
    
    delete _world;
    delete _debugDraw;
    [super dealloc];
}

Now when you compile and run the project, you will see some pink shapes around the sprites that show where the Box2D shapes are. If all goes well, they should be moving along with the sprites, proving out that the box2D shapes are where we want them to be.

Screenshot of our project with Box2D debug drawing

Detecting the Collision

Now it’s time to run over some cats!

We’re going to set up a contact listener on the world just like we did in the breakout game. Download the generic contact listener code we wrote in that project, and add MyContactListener.h and MyContactListener.mm to your project.

While you’re at it, also download this amusing sound effect I made and add it to the project too. I bet you know what’s coming! :]

Back to code. Add the following imports to the top of HelloWorldScene.h:

#import "MyContactListener.h"
#import "SimpleAudioEngine.h"

And the following member variable to HelloWorld:

MyContactListener *_contactListener;

Then add the following code to your init method:

// Create contact listener
_contactListener = new MyContactListener();
_world->SetContactListener(_contactListener);

// Preload effect
[[SimpleAudioEngine sharedEngine] preloadEffect:@"hahaha.caf"];

And finally the following to the bottom of your tick method:

std::vector<b2Body *>toDestroy; 
std::vector<MyContact>::iterator pos;
for(pos = _contactListener->_contacts.begin(); 
    pos != _contactListener->_contacts.end(); ++pos) {
    MyContact contact = *pos;
    
    b2Body *bodyA = contact.fixtureA->GetBody();
    b2Body *bodyB = contact.fixtureB->GetBody();
    if (bodyA->GetUserData() != NULL && bodyB->GetUserData() != NULL) {
        CCSprite *spriteA = (CCSprite *) bodyA->GetUserData();
        CCSprite *spriteB = (CCSprite *) bodyB->GetUserData();
        
        if (spriteA.tag == 1 && spriteB.tag == 2) {
            toDestroy.push_back(bodyA);
        } else if (spriteA.tag == 2 && spriteB.tag == 1) {
            toDestroy.push_back(bodyB);
        } 
    }        
}

std::vector<b2Body *>::iterator pos2;
for(pos2 = toDestroy.begin(); pos2 != toDestroy.end(); ++pos2) {
    b2Body *body = *pos2;     
    if (body->GetUserData() != NULL) {
        CCSprite *sprite = (CCSprite *) body->GetUserData();
        [_spriteSheet removeChild:sprite cleanup:YES];
    }
    _world->DestroyBody(body);
}

if (toDestroy.size() > 0) {
    [[SimpleAudioEngine sharedEngine] playEffect:@"hahaha.caf"];   
}

This code is all the same stuff we already learned how to do in the breakout tutorial, so you should be familiar with all of the above.

One more thing: let’s add some more cleanup code that we forgot to add so far (thanks to Indy for reminding me of this!) Add the following inside your dealloc method:

delete _contactListener;
[_spriteSheet release];

So just give the code a run, and now there should be some squished cats!

Contributors

Over 300 content creators. Join our team.