How To Use Box2D For Just Collision Detection with cocos2d-iphone

原文地址:http://www.raywenderlich.com/606/box2d-tutorial-for-ios-how-to-use-box2d-for-just-collision-detection-with-cocos2d-iphone

When making a game with Cocos2D, sometimes you may find yourself wanting to use Cocos2D actions to move your objects instead of using the Box2D physics engine. However that doesn’t mean you can’t still make use of the great collision detection support in Box2D!

This tutorial will show you how to use Box2D for just collision detection – not physics – step by step. We will create a simple demo of a car driving around the screen, laughing merrily as it runs over cats. Yes I know, I can be sadistic sometimes ;]

While we’re doing this, along the way we’ll also coves a couple new and interesting concepts, such as using sprite sheets, the Zwoptex utility, Box2D debug drawing, and the new VertexHelper utility.

This tutorial assumes you have gone through the other Box2D and Cocos2D tutorials, or that you have equivalent knowledge.

And before I forget – special credit goes to Kim from the comments section for suggesting writing this tutorial!

Sprites and Sprite Sheets

Before we begin, we’re going to take a brief detour to talk about sprites and sprite sheets.

So far when we have been using Cocos2D, we have been using the CCSprite class directly and passing in individual sprite images. But according to the Cocos2D best practices page, it’s a lot more efficient to use sprite sheets instead.

The above is an example of a sprite sheet, part of a sprite sheet included in the Cocos2D sample code. Sprite sheets are basically one big image that can be chopped-up into sub images. To specify where each sub-image lies within the sprite sheet, you have to give Cocos2D the coordinates for each image. For example, here’s what it looks like to pull out the first four sprites from that sprite sheet:

CCSpriteSheet *sheet = [CCSpriteSheet 
    spriteSheetWithFile:@"grossini_dance_atlas.jpg" capacity:1];
CCSprite *sprite1 = [CCSprite spriteWithTexture:sheet.texture rect:
    CGRectMake(85*0, 121*1, 85, 121)];
CCSprite *sprite2 = [CCSprite spriteWithTexture:sheet.texture rect:
    CGRectMake(85*1, 121*1, 85, 121)];
CCSprite *sprite3 = [CCSprite spriteWithTexture:sheet.texture rect:
    CGRectMake(85*2, 121*1, 85, 121)];
CCSprite *sprite4 = [CCSprite spriteWithTexture:sheet.texture rect:
    CGRectMake(85*3, 121*1, 85, 121)];

As you can imagine, writing a lot of code like that with hard-coded coordinates can be quite tedious. Luckily, Robert Payne has written an incredibly handly web tool called Zwoptex that makes both creating sprite sheets and importing the coordinates into Cocos2D extremely easy.

Creating Our Sprite Sheet

Before we begin, you’ll need some images to work with. You can either use the car and cat images created by my lovely wife, or use your own.

Next, load up a web browser to Zwoptex. You will see a big blank screen that looks something like this:

Once it’s loaded up, inside Zwoptex click File, and then Import Images. Select the car and cat images that you downloaded and click select, and your images should appear on top of each other. Drag your images apart a bit so you can see them more clearly.

Note that the images have been automatically cropped to remove extra white space. This isn’t what we want (you’ll see why later), so drag a box around both of the images to select them and click Modify\Untrim Selected Images.

Now let’s get the images nice and straightened. Click Arrange\Complex by Height (no spacing) and they will line up nicely for you.

Finally, let’s get the canvas size down to a reasonable size. Click Modify\Canvas Width and set the width to 128px. Do the same with Modify\Canvas Height and set the height to 64px. Your screen should now look something like this:

Finally, time to export our sprite sheet and coordinates! Click on File\Export Texture and save the sprite sheet as “sprites.jpg”. Then click on File\Export Coordinates and save the coordinates as “sprites.plist”. Note that it is important that both the sprite sheet and coordinates have the same filename (before the extension), as the sprite sheet code assumes that they do.

Just for fun, open up sprites.plist. You can see that what Zwoptex did is automatically fill up a property list with the original names of each sprite, and the coordinates for each sprite. We can provide this to Cocos2D instead of typing in the coordinates manually!

Adding Our Sprites From the Sprite Sheet

Ok time to write some code!

Create a new project in XCode, choose the cocos2d-0.99.1 Box2d Application template, and name the project Box2DCollision. Then clear out the sample code to get an empty starting point like the way we did in the bouncing balls tutorial.

Also, make sure you add the following to the top of your HelloWorldScene.mm:

#define PTM_RATIO 32.0

If you’re confused what that means or why you need it, check out the bouncing balls tutorial for more information.

Next, let’s add the sprite sheet and coordinates property list into our project. Drag sprites.jpg and sprites.plist into the Resources folder of your project, verifying that “Copy items into destination group’s folder (if needed)” is checked.

Then, add the following member variable to the HelloWorld class in HelloWorldScene.h:

 CCSpriteSheet *_spriteSheet;

Now let’s modify the init method in HelloWorldScene.mm to load up our sprite sheet and property list. Modify your init method to look like the following:

- (id)init {
 
    if ((self=[super init])) {
 
        // Create our sprite sheet and frame cache
        _spriteSheet = [[CCSpriteSheet spriteSheetWithFile:@"sprites.jpg" 
            capacity:2] retain];
        [[CCSpriteFrameCache sharedSpriteFrameCache] 
            addSpriteFramesWithFile:@"sprites.plist"];
        [self addChild:_spriteSheet];
 
        [self spawnCar];
        [self schedule:@selector(secondUpdate:) interval:1.0];
 
    }
    return self;
 
}

The first thing we do is to create a sprite sheet, which is an object that efficiently draws all of its CCSprite children. For this to work, they obviously must all share the same texture. When we add our car and cat image to the scene, we will add them as children of the CCSpriteSheet. Special thanks to Victor from the comments section for pointing this out!

Then we use CCSpriteFrameCache to load up our sprite sheet and give it the property list. This function actually looks for a image with the same name, which is why it was important to name the sprite sheet “sprites.jpg” since the property list was named “sprites.plist.”

After that we call a function to spawn a car into the scene, and a periodic 1 second update function, which we are about to write.

We’ll start with spawning the car. Let’s have him move in a triangle pattern across the middle of the screen, repeating forever. Add the following function to HelloWorldScene.mm above the init method:

- (void)spawnCar {
 
    CCSprite *car = [CCSprite spriteWithSpriteFrameName:@"car.jpg"];
    car.position = ccp(100, 100);
    car.tag = 2;
 
    [car runAction:[CCRepeatForever actionWithAction:
                    [CCSequence actions:
                     [CCMoveTo actionWithDuration:1.0 position:ccp(300,100)],
                     [CCMoveTo actionWithDuration:1.0 position:ccp(200,200)],
                     [CCMoveTo actionWithDuration:1.0 position:ccp(100,100)],
                     nil]]];
 
    [_spriteSheet addChild:car];
 
}

Note that to create our Sprite, we use the method spriteWithSpriteFrameName rather than spriteWithFile. This tells it to use the part of the sprite sheet texture representing the Car image.

Also note that instead of adding the car as a child of the HelloWorld layer, we add it as a child of the sprite sheet for more efficient drawing.

The rest of this function should be familiar to you at this point. So let’s spawn some cats! Add the following methods above the spawnCar method:

- (void)spawnCat {
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
 
    CCSprite *cat = [CCSprite spriteWithSpriteFrameName:@"cat.jpg"];
 
    int minY = cat.contentSize.height/2;
    int maxY = winSize.height - (cat.contentSize.height/2);
    int rangeY = maxY - minY;
    int actualY = arc4random() % rangeY;
 
    int startX = winSize.width + (cat.contentSize.width/2);
    int endX = -(cat.contentSize.width/2);
 
    CGPoint startPos = ccp(startX, actualY);
    CGPoint endPos = ccp(endX, actualY);
 
    cat.position = startPos;
    cat.tag = 1;
 
    [cat runAction:[CCSequence actions:
                    [CCMoveTo actionWithDuration:1.0 position:endPos],
                    [CCCallFuncN actionWithTarget:self 
                        selector:@selector(spriteDone:)],
                    nil]];
 
    [_spriteSheet addChild:cat];
 
}
 
- (void)spriteDone:(id)sender {
 
    CCSprite *sprite = (CCSprite *)sender;
    [_spriteSheet removeChild:sprite cleanup:YES];
 
}
 
- (void)secondUpdate:(ccTime)dt {
 
    [self spawnCat];
 
}

Again you should be familiar with all of the above code by now. Compile and run the code, and if all goes well you should see a car moving around the screen, with cats moving right to left. But the cats are getting away scott free! We’ll have to do something about that.

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.

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 genericcontact 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*>toDestroy; 
std::vector::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*>::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!

Fine Tuning the Bounding Boxes

As you may have noticed, the problem with the current setup is the shapes for the Box2D objects do not accurately represent the shapes of the sprites. For some games this is good enough, but there’s an extra degree of realism if we can define a shape that matches up somewhat to the actual shape of the sprite object.

In Box2D, you can define the vertices for shapes yourself by specifying the point for each vertex. However, hard coding all of these points is error-prone and time intensive. Luckily, Johannes Fahrenkrug has just released a neat tool called VertexHelper that makes defining these vertices and importing them into Box2D a snap.

Go ahead and visit the above site and download VertexHelper. Is is a Mac application with source code, so you’ll have to open VertexHelper.xcodeproj and compile and run the app. When you run the app you will see a screen like the following:

Go ahead and drag sprites.jpg into VertexHelper on top of the “Drop Sprite Image here” label, and in the Rows/Cols section set the number of rows to 1 and the number of columns to 2. VertexHelper will automatically draw lines on the image showing how the image is divided up.

Note that of course the image must be equally spaced for this to work – this is why we told Zwopple not to auto-chop the images earlier in the tutorial.

Next, click the check box for “Edit Mode” up top and start to define your vertices by clicking in counter-clockwiseorder around the sprites for each image. Note that Box2D will automatically close up the last point you click with the first point, so no need to connect them yourself.

Another very important thing to note pointed out by toadkick in the comments section that I wasn’t aware of at first. When you are defining the vertices, you need to make sure that the polygon you create is convex. This means that none of the internal angles can be greater than 180 degrees, or in more simple terms, there can be no “indentations.” If you are interested in more info on this, check out this great page that shows you a Javascript demo of convex vs. concave polygons.

Finally, note that Box2D defines a variable b2_maxPolygonVertices that limits how many vertices you can add per shape, which defaults to 8. You can change this in b2Settings.h if you need to, but for now just make sure that you draw a border around each sprite with at most 8 vertices.

This is best shown through a video, so here’s a quick clip that shows how to define the vertices for each sprite:

Once you have that complete, in the type dropdown choose Box2D, and for the Style pick “Initialization”. In the box to the right it will put a bunch of code which we’ll be able to copy and paste into our project.

Ok so let’s fix our shapes up in our project! Open up HelloWorldScene.mm and modify the addBoxBodyForSprite method according to this template. Start by commenting out the spriteShape.SetAsBox call and then follow the instructions in the comments below:

/*spriteShape.SetAsBox(sprite.contentSize.width/PTM_RATIO/2,
                     sprite.contentSize.height/PTM_RATIO/2);*/
if (sprite.tag == 1) {
    // Uncomment this and replace the number with the number of vertices
    // for the cat that you defined in VertexHelper
    //int num = 6;
    //b2Vec2 verts[] = {b2Vec2(4.5f / PTM_RATIO, -17.7f / PTM_RATIO),
    //b2Vec2(20.5f / PTM_RATIO, 7.2f / PTM_RATIO),
    //b2Vec2(22.8f / PTM_RATIO, 29.5f / PTM_RATIO),
    //b2Vec2(-24.7f / PTM_RATIO, 31.0f / PTM_RATIO),
    //b2Vec2(-20.2f / PTM_RATIO, 4.7f / PTM_RATIO),
    //b2Vec2(-11.7f / PTM_RATIO, -17.5f / PTM_RATIO)};
    // Then add this
    //spriteShape.Set(verts, num);
} else {
    // Do the same thing as the above, but use the car data this time
}

A side note: Again, toadkick pointed out that it’s better to use the “Initialization” style and the b2PolygonShape::Set method rather than the “Assignment” style so that Box2D can automatically compute the centroid and the normals for the shape for us.

Once you have that done, compile and run your project. You should notice that the debug draw shapes around the sprites now match up much better, and the collisions are much more realistic!

Now you can see the true power of using Box2D for collision detection – defining bounding shapes like that would be a bit more tricky to do yourself.

Of course, in our case tailoring the bounding shape has the unfortunate side effect of letting a few cats scurry away that would have been squashed with our simple box method! ;]

And That’s A Wrap!

Here’s a sample project with all of the code we’ve developed in the above tutorial.

Note that this is only one way of doing collision detection with Box2D. Lam from the Box2D forums has pointed outanother way where you can use functions such as b2CollidePolygons instead of using this method. If you are looking for just a quick collision check rather than going through all of the above setup, you might want to check out Lam’s method.

I’m really interested in hearing how other developers have used Box2D and Cocos2D in their projects – or are planning to use it – do you use Box2D for physics, or just for collision detection, or not at all? And if you use Box2D for collision detection, which method do you use?


你可能感兴趣的:(cocos2d-x,cocos2d,box2d,collision,detection)