This is the second and final part of a tutorial on how to create a simple breakout game using the Box2D physics library that comes with Cocos2D. If you haven’t already, make sure you go through part 1 first!
We left off with a box that bounces around the screen and a paddle we could move with our fingers. Let’s start adding in some game logic by making the player lose if the ball hits the bottom of the screen!
Box2D and Collisions
To find out when a fixture collides with another fixture in Box2D, we need to register a contact listener. A contact listener is a C++ object that we give Box2D, and it will call methods on that object to let us know when two objects begin to touch and stop touching.
The trick to a contact listener, however, is according to the Box2D User Manual, you cannot perform any operation that alters game physics within the callback. Since this is something we will probably want to do (such as destroy an object when two objects collide), instead we will just keep references to the collisions so we can deal with them later.
Another tricky bit is we can’t just store references to the contact points that are sent to the listener, because they are reused by Box2D. So we have to store copies of them instead.
Ok enough talk, let’s try this out for ourselves!
When We’ve Hit Rock Bottom
Note that in this section we’re going to be using some C++ and the standard template library (STL) a bit. If you are unfamiliar with C++ or the STL, don’t worry about it too much – you can just copy and paste the code, it is general purpose and should work in your projects as well.
Ok. Click on your Classes folder and add a new file (File/New File), click “Cocoa Touch Class” on the left, and choose “Objective-C class”, verifying that “Subclass of NSObject” is selected, then click Next. Name your object MyContactListener, and click finish.
Right click on MyContactListener.m and rename the file to MyContactListener.mm. This is because we are actually creating a C++ class in this file, and the convention when you are using C++ in a file is to have the file end with mm.
Then replace the contents of MyContactListener.h with the following file:
#import "Box2D.h" #import <vector> #import <algorithm> struct MyContact { b2Fixture *fixtureA; b2Fixture *fixtureB; bool operator==(const MyContact& other) const { return (fixtureA == other.fixtureA) && (fixtureB == other.fixtureB); } }; class MyContactListener : public b2ContactListener { public: std::vector<MyContact>_contacts; MyContactListener(); ~MyContactListener(); virtual void BeginContact(b2Contact* contact); virtual void EndContact(b2Contact* contact); virtual void PreSolve(b2Contact* contact, const b2Manifold* oldManifold); virtual void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse); }; |
Here we define the structure that we will use to keep track of the data we’re interested in from the contact notifications. Again, we need to store a copy because the contact points passed in are reused. note we have to declare an equality operator here, because we’re going to use a the find() method to look for matching objects in the vector, which requires this method.
After that we declare our contact listener class, which derives from b2ContactListener. We just declare the methods we need to implement, as well as a STL vector that we will use to buffer our contact points.
Now replace the contents of MyContactListener.mm with the following:
#import "MyContactListener.h" MyContactListener::MyContactListener() : _contacts() { } MyContactListener::~MyContactListener() { } void MyContactListener::BeginContact(b2Contact* contact) { // We need to copy out the data because the b2Contact passed in // is reused. MyContact myContact = { contact->GetFixtureA(), contact->GetFixtureB() }; _contacts.push_back(myContact); } void MyContactListener::EndContact(b2Contact* contact) { MyContact myContact = { contact->GetFixtureA(), contact->GetFixtureB() }; std::vector<MyContact>::iterator pos; pos = std::find(_contacts.begin(), _contacts.end(), myContact); if (pos != _contacts.end()) { _contacts.erase(pos); } } void MyContactListener::PreSolve(b2Contact* contact, const b2Manifold* oldManifold) { } void MyContactListener::PostSolve(b2Contact* contact, const b2ContactImpulse* impulse) { } |
We initialize our vector in the constructor. Then the only two methods we actually implement are BeginContact and EndContact. In BeginContact we make a copy of the fixtures that just collided, and store them in our vector. In EndContact, we look to see if the contact point is in our vector and remove it if so.
Ok, now let’s put this to use. Switch over to HelloWorldScene.m and add the following member variable to the HelloWorld class:
MyContactListener *_contactListener; |
Then add the following code to your init method:
// Create contact listener _contactListener = new MyContactListener(); _world->SetContactListener(_contactListener); |
Here we create our contact listener object, and call a method on the world object to set the contact listener.
Next add the cleanup code to dealloc before we forget:
delete _contactListener; |
And finally add the following code to the bottom of your tick method:
std::vector<MyContact>::iterator pos; for(pos = _contactListener->_contacts.begin(); pos != _contactListener->_contacts.end(); ++pos) { MyContact contact = *pos; if ((contact.fixtureA == _bottomFixture && contact.fixtureB == _ballFixture) || (contact.fixtureA == _ballFixture && contact.fixtureB == _bottomFixture)) { NSLog(@"Ball hit bottom!"); } } |
This iterates through all of the buffered contact points, and checks to see if any of them are a match between the ball and the bottom of the screen. For now, we just log this out with a NSLog message because it’s time to check if it’s working!
So compile and run in debug mode, and switch over to your console by clicking Run/Console, and whenever the ball intersects the bottom you should see a message in your log that reads “Ball hit bottom!”
Adding a game over scene
Add the GameOverScene.h and GameOverScene.mm files that we developed in the how to make a simple game with Cocos2D tutorial. Note that you’ll have to rename GameOverScene.m to GameOverScene.mm since we’re dealing with C++ code now or you will get compilation errors.
Then add the import to the top of your HelloWorldScene.mm file:
#import "GameOverScene.h" |
Then replace the NSLog statement with the following code:
GameOverScene *gameOverScene = [GameOverScene node]; [gameOverScene.layer.label setString:@"You Lose :["]; [[CCDirector sharedDirector] replaceScene:gameOverScene]; |
Allright, we’re getting somewhere! But what fun is a game where you can’t win?
Adding some blocks
Download a copy of a block image I made and drag it to the Resources folder of your project, making sure “Copy items into destination group’s folder (if needed)” is checked.
Then add the following code to your init method:
for(int i = 0; i < 4; i++) { static int padding=20; // Create block and add it to the layer CCSprite *block = [CCSprite spriteWithFile:@"Block.png"]; int xOffset = padding+block.contentSize.width/2+ ((block.contentSize.width+padding)*i); block.position = ccp(xOffset, 250); block.tag = 2; [self addChild:block]; // Create block body b2BodyDef blockBodyDef; blockBodyDef.type = b2_dynamicBody; blockBodyDef.position.Set(xOffset/PTM_RATIO, 250/PTM_RATIO); blockBodyDef.userData = block; b2Body *blockBody = _world->CreateBody(&blockBodyDef); // Create block shape b2PolygonShape blockShape; blockShape.SetAsBox(block.contentSize.width/PTM_RATIO/2, block.contentSize.height/PTM_RATIO/2); // Create shape definition and add to body b2FixtureDef blockShapeDef; blockShapeDef.shape = &blockShape; blockShapeDef.density = 10.0; blockShapeDef.friction = 0.0; blockShapeDef.restitution = 0.1f; blockBody->CreateFixture(&blockShapeDef); } |
You should understand this code pretty well by now. We create a body just the same way we did for the paddle, except this time we do it in a loop so we can easily create four blocks along the top. Also notice that we set the tag on the block sprite to 2, for future reference.
Compile and run this code, and you should now have blocks you can mess around with your ball!
Destroying the Blocks
To be a true breakout game, we need to destroy the blocks when the ball intersects them. Well we’ve already added the code to keep track of collisions, so all we need to do is modify the tick method!
Modify the code you added in the tick method to be the following:
std::vector<b2Body *>toDestroy; std::vector<MyContact>::iterator pos; for(pos = _contactListener->_contacts.begin(); pos != _contactListener->_contacts.end(); ++pos) { MyContact contact = *pos; if ((contact.fixtureA == _bottomFixture && contact.fixtureB == _ballFixture) || (contact.fixtureA == _ballFixture && contact.fixtureB == _bottomFixture)) { GameOverScene *gameOverScene = [GameOverScene node]; [gameOverScene.layer.label setString:@"You Lose :["]; [[CCDirector sharedDirector] replaceScene:gameOverScene]; } 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(); // Sprite A = ball, Sprite B = Block if (spriteA.tag == 1 && spriteB.tag == 2) { if (std::find(toDestroy.begin(), toDestroy.end(), bodyB) == toDestroy.end()) { toDestroy.push_back(bodyB); } } // Sprite B = block, Sprite A = ball else if (spriteA.tag == 2 && spriteB.tag == 1) { if (std::find(toDestroy.begin(), toDestroy.end(), bodyA) == toDestroy.end()) { toDestroy.push_back(bodyA); } } } } 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(); [self removeChild:sprite cleanup:YES]; } _world->DestroyBody(body); } |
Ok, let’s explain this. We go through the contact points again, but this time after we check for collisions between the ball and the bottom of the screen, we take a look at the bodies that are colliding. We can get to the bodies by calling the GetBody() method on the fixtures.
Once we have the bodies, we check to see if they have user data. If they do, we cast them to sprites – because we know that’s what we’ve set the user data to.
Then we look to see what sprites are colliding based on their tags. If a sprite is intersecting with a block, we add the block to a list of objects to destroy.
Note that we add it to a list to destroy rather than destroying the body right away. This is because if we destroy the body right away, the world will clean up a lot of pointers leaving us with garbage data in our contact listener. Also note that we only should add it to the list if it isn’t there already!
Finally, we go through the list of bodies we want to delete. Note that we not only have to destroy the body from Box2D’s world, we also have to remove the sprite object from our Cocos2D scene.
Give it a compile and run, and you should now be able to destroy bricks! Yay!
Winning the Game
Next we need to add some logic in to let the user actually win the game. Modify the beginning of your tick method to read as follows:
- (void)tick:(ccTime) dt { bool blockFound = false; _world->Step(dt, 10, 10); for(b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) { if (b->GetUserData() != NULL) { CCSprite *sprite = (CCSprite *)b->GetUserData(); if (sprite.tag == 2) { blockFound = true; } //... |
All we’re doing here is looking to see if we ever come across a block while we’re iterating through the objects in the scene – if we do find one we set the blockFound variable to true – otherwise it is false.
Then add the following code at the end of the function:
if (!blockFound) { GameOverScene *gameOverScene = [GameOverScene node]; [gameOverScene.layer.label setString:@"You Win!"]; [[CCDirector sharedDirector] replaceScene:gameOverScene]; } |
Here we just display a game over scene if no blocks were found. Give it a compile and run, and see if you can win the game!
Finishing Touches
The game is quite cool, but we need some sound of course! You can download the awesome background music I made and a cool blip sound I made to use. As usual, drag them to your resources folder once you’ve downloaded them.
By the way – I made the sound effect with an awesome program called cfxr that one of our commenters – Indy – pointed out. Thanks Indy this program pwns!
Anyway – once you’ve added the files to your project, add the following to the top of HelloWorldScene.mm:
#import "SimpleAudioEngine.h" |
And the following to your init method:
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"background-music-aac.caf"]; |
And finally the following at the end of your tick method:
if (toDestroy.size() > 0) { [[SimpleAudioEngine sharedEngine] playEffect:@"blip.caf"]; } |
And there you have it – your own simple breakout game with Box2D physics!
Gimme The Code!
Here’s the full code for the Cocos2D and Box2D Breakout Game that we’ve made in this tutorial.
Where To Go From Here?
Obviously this is a quite simple implementation of breakout, but now that you have this working there’s a lot more you can do. You could extend this code to give the blocks hit points and make the ball have to hit them a number of times before they are destroyed. You could add new blocks, let the paddle shoot lasers toward the blocks, whatever you dream up!
Let me know if you have any tips or suggestions for better ways to do things, and hope this comes in handy!
Category: iPhone