Box2D is a powerful physics library that comes with the Cocos2D game programming library for the iPhone. There’s a lot you can do with it, and a great way to start learning about how it works is to create a simple game with it!
In this tutorial, we are going to create a simple Breakout game step by step, complete with collision detection, a ball bouncing with physics effects, dragging the paddle via touches, and win/lose screens.
If you are new to Cocos2D or Box2D, it may help to go through the intro to Cocos2D tutorialand/or intro to Box2D tutorial before proceeding with this tutorial.
Allright, time for some Breakout!
An Ever-Bouncing Ball
Start by creating a new project with the cocos2d-0.99.1 Box2d Application template, and name your project “Box2DBreakout”. Clear out the template code and so you have an empty project to start with – see the intro to Box2D tutorial for instructions on how to do that.
Once you have a nice clean project, add the following import to the top of HelloWorldScene.h:
#import "Box2D.h" |
And add the following member variables to the HelloWorld class:
b2World *_world; b2Body *_groundBody; b2Fixture *_bottomFixture; b2Fixture *_ballFixture; |
Then add the following to the top of HelloWorldScene.mm:
#define PTM_RATIO 32 |
This is the same ratio to convert from pixels to “meters” that we discussed in our previous Box2D tutorial.
Then add the following code to your init method:
CGSize winSize = [CCDirector sharedDirector].winSize; // Create a world b2Vec2 gravity = b2Vec2(0.0f, 0.0f); bool doSleep = true; _world = new b2World(gravity, doSleep); // Create edges around the entire screen b2BodyDef groundBodyDef; groundBodyDef.position.Set(0,0); _groundBody = _world->CreateBody(&groundBodyDef); b2PolygonShape groundBox; b2FixtureDef groundBoxDef; groundBoxDef.shape = &groundBox; groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO, 0)); _bottomFixture = _groundBody->CreateFixture(&groundBoxDef); groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(0, winSize.height/PTM_RATIO)); _groundBody->CreateFixture(&groundBoxDef); groundBox.SetAsEdge(b2Vec2(0, winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO, winSize.height/PTM_RATIO)); _groundBody->CreateFixture(&groundBoxDef); groundBox.SetAsEdge(b2Vec2(winSize.width/PTM_RATIO, winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO, 0)); _groundBody->CreateFixture(&groundBoxDef); |
Again, this is the same code that we had in our previous Box2D tutorial to create a bounding box around the screen. However, this time we set the gravity to zero, because in our breakout game there will not be any gravity! Also note we store a pointer to the bottom fixture for future reference (we’ll need it to keep track of when the ball hits the bottom of the screen).
Now download a copy of the image of a bouncy ball I created and drag it into the Resources folder of your project, making sure “Copy items into destination group’s folder (if needed)” is checked.
Let’s add a sprite for the ball into the scene. Add the following right after the last bit of code you added:
// Create sprite and add it to the layer CCSprite *ball = [CCSprite spriteWithFile:@"Ball.png" rect:CGRectMake(0, 0, 52, 52)]; ball.position = ccp(100, 100); ball.tag = 1; [self addChild:ball]; |
There should be no surprises here, we’ve been doing this for a while now. Note that we set a tag on the ball for identification purposes (you’ll see why later in the tutorial).
Next let’s create a body for the shape:
// Create ball body b2BodyDef ballBodyDef; ballBodyDef.type = b2_dynamicBody; ballBodyDef.position.Set(100/PTM_RATIO, 100/PTM_RATIO); ballBodyDef.userData = ball; b2Body * ballBody = _world->CreateBody(&ballBodyDef); // Create circle shape b2CircleShape circle; circle.m_radius = 26.0/PTM_RATIO; // Create shape definition and add to body b2FixtureDef ballShapeDef; ballShapeDef.shape = &circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.f; ballShapeDef.restitution = 1.0f; _ballFixture = ballBody->CreateFixture(&ballShapeDef); |
This should look familiar as well from our last tutorial. As a refresher, to create a body we need to create a body definition, then a body object, then a shape, then a fixture definition, and finally a fixture object.
Note that we set the parameters a bit differently this time: we’ve set the restitution to 1.0, meaning the when the ball collides with an object the collision will be perfectly elastic. In plain English, this means that the ball will bounce back with equal force to the impact.
Also note that we store the ball fixture for future reference (same reason as why we stored the bottom fixture).
Update: Also note that the ball is set to have no friction. Thanks to Steve Oldmeadow for pointing out that this is important in this case so that the ball bounces nicely off the walls, preventing the ball from frequently getting stuck bouncing back and forth in a straight up-down or left-right angle.
Ok, now for something completely different. Add the following after the above:
b2Vec2 force = b2Vec2(10, 10); ballBody->ApplyLinearImpulse(force, ballBodyDef.position); |
This applies an impulse (you can think of it like a propulsion from a jet pack thruster) to the ball to get it to start moving in a particular direction (in this case, diagonally up to the right). We need this to get the ball moving in the first place!
One last thing for the init method: add the tick scheduling:
[self schedule:@selector(tick:)]; |
And then the tick method itself!
- (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(); sprite.position = ccp(b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO); sprite.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()); } } } |
Again no surprises with these two since we did the same thing in the last tutorial.
One last thing and then we’re ready to try it out: the cleanup method!
- (void)dealloc { delete _world; _groundBody = NULL; [super dealloc]; } |
Ok, let’s try it out! When you compile and run the project, you should see a ball continuously bouncing around the screen – cool!
Adding the Paddle
It wouldn’t be a breakout game if we didn’t have a paddle. Download a copy of a graphic of a paddle 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 member variable to HelloWorld in HelloWorldScene.h:
b2Body *_paddleBody; b2Fixture *_paddleFixture; |
And then construct the paddle body in your init method:
// Create paddle and add it to the layer CCSprite *paddle = [CCSprite spriteWithFile:@"Paddle.png"]; paddle.position = ccp(winSize.width/2, 50); [self addChild:paddle]; // Create paddle body b2BodyDef paddleBodyDef; paddleBodyDef.type = b2_dynamicBody; paddleBodyDef.position.Set(winSize.width/2/PTM_RATIO, 50/PTM_RATIO); paddleBodyDef.userData = paddle; _paddleBody = _world->CreateBody(&paddleBodyDef); // Create paddle shape b2PolygonShape paddleShape; paddleShape.SetAsBox(paddle.contentSize.width/PTM_RATIO/2, paddle.contentSize.height/PTM_RATIO/2); // Create shape definition and add to body b2FixtureDef paddleShapeDef; paddleShapeDef.shape = &paddleShape; paddleShapeDef.density = 10.0f; paddleShapeDef.friction = 0.4f; paddleShapeDef.restitution = 0.1f; _paddleFixture = _paddleBody->CreateFixture(&paddleShapeDef); |
I’m not going to explain this much because you should be a pro at creating bodies by this point. However, note a few differences this time:
- When you create a CCSprite, you don’t need to specify the size of the sprite if you don’t want to. If you give it the filename, it can automatically determine the size.
- Note that instead of using a circle shape, we use a polygon shape this time. We use a helper method to create the shape in the form of a box.
- Note that there is an alternate SetAsBox method that allows you to specify the position of the shape relative to the body, which comes in handy when constructing complex shapes. However we don’t need to use that here, since we just want the shape centered on the body.
- We make the paddle more dense than the ball, and tweak the other parameters as well.
- We are storing paddleBody and paddleFixture for future reference.
If you compile and run this you’ll see our paddle in the scene, and the ball will bounce off it:
However this isn’t much fun, because we can’t move paddle yet!
Moving The Paddle
So let’s get moving! Moving the paddle is going to require touches, so enable touches in your init method:
self.isTouchEnabled = YES; |
Then add the following member variable to your HelloWorld class in HelloWorldScene.h:
b2MouseJoint *_mouseJoint; |
Now let’s implement the touch methods! Let’s start with ccTouchesBegan:
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (_mouseJoint != NULL) return; UITouch *myTouch = [touches anyObject]; CGPoint location = [myTouch locationInView:[myTouch view]]; location = [[CCDirector sharedDirector] convertToGL:location]; b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO); if (_paddleFixture->TestPoint(locationWorld)) { b2MouseJointDef md; md.bodyA = _groundBody; md.bodyB = _paddleBody; md.target = locationWorld; md.collideConnected = true; md.maxForce = 1000.0f * _paddleBody->GetMass(); _mouseJoint = (b2MouseJoint *)_world->CreateJoint(&md); _paddleBody->SetAwake(true); } } |
Wow, a lot of new stuff in here. Let’s discuss it bit by bit.
First, we convert the touch location to our Cocos2D coordinates (convertToGL) and then to our Box2D coordinates (locationWorld).
Then we use a method on our paddle fixture object that we’ve stored away to see if the touch point is within the fixture.
If it is, we create something called a “mouse joint.” In Box2D, a mouse joint is used to make a body move toward a specified point – in this case where the user is tapping.
When you set up a mouse joint, you have to give it two bodies. The first isn’t actually used, but the convention is to use the ground body. The second is the body you want to move – in our case the paddle.
Then you specify where you want the target to move – in our case where the user is tapping.
Then you tell Box2D that when bodyA and bodyB collide, treat it as a collision, rather than ignoring it. This is very important. When I was trying to get this working, I didn’t have this set, so when I was moving the paddle with my mouse it wouldn’t collide with the edges of the screen, and my paddle would fly off screen sometimes! This was very confusing and frustrating until I discovered this simple way to fix it :]
You then specify the max force with which to move the body. If you reduce this amount, the body will react more slowly to your mouse movements (which may be what you want sometimes!). But here we want the paddle to respond rather quickly to movements.
Finally we add the joint to the world, and store away the pointer for future reference. We also set the body to awake. We need to do this because if the body is asleep and we don’t awaken it, it won’t respond to the movements!
Ok, next let’s add the ccTouchesMoved method:
-(void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (_mouseJoint == NULL) return; UITouch *myTouch = [touches anyObject]; CGPoint location = [myTouch locationInView:[myTouch view]]; location = [[CCDirector sharedDirector] convertToGL:location]; b2Vec2 locationWorld = b2Vec2(location.x/PTM_RATIO, location.y/PTM_RATIO); _mouseJoint->SetTarget(locationWorld); } |
The beginning of this method is the same as ccTouchesBegan – we get the location of the touch in Box2D coordinates. The only thing we do here is update the target of the mouse joint (i.e. where we want the body to move) to be the current location of the touch.
Let’s wrap up by adding ccTouchesEnded and ccTouchesCancelled:
-(void)ccTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { if (_mouseJoint) { _world->DestroyJoint(_mouseJoint); _mouseJoint = NULL; } } - (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (_mouseJoint) { _world->DestroyJoint(_mouseJoint); _mouseJoint = NULL; } } |
All we do in these methods is destroy the mouse joint because when the touches end, we’re done moving the object.
Give it a compile and run, and you should be able to move the paddle all around the screen to bounce the ball!
Pretty cool… but wait a minute, this isn’t breakout, we shouldn’t be able to move the paddle anywhere we want, we should just be able to move it back and forth!
Restricting Movement of the Paddle
We can easily restrict movement of the paddle by adding another joint into the world called a prismatic joint. This lets us restrict the movement of one body to another along a specified axis.
So we can use this to restrict the movement of the paddle relative to the ground to only be able to move along the x-axis.
Let’s give this a shot in code. Add this to your init method:
// Restrict paddle along the x axis b2PrismaticJointDef jointDef; b2Vec2 worldAxis(1.0f, 0.0f); jointDef.collideConnected = true; jointDef.Initialize(_paddleBody, _groundBody, _paddleBody->GetWorldCenter(), worldAxis); _world->CreateJoint(&jointDef); |
The first thing we do is specify the axis to be a vector along the x axis, but not at all along the y axis. We then specify the ever important collideConnected value so our paddle will correctly bounce against the edge of the screen rather than flying into never-never land.
We then initialize the joint specifying the paddle and the ground body and create the joint!
Give it a compile and run and now you should only be able to move the paddle back and forth instead of anywhere you want:
Finishing Touches
Now, as you’ve been playing around with this so far you may have noticed that sometimes the ball can get super-fast or super slow, depending on how you hit it with the paddle.
Update: The first time I tried to fix this, I tried to adjust the velocity of the ball directly by calling SetLinearVelocity. However, as Steve Oldmeadow also pointed out (thanks Steve!), this is a bad idea as it messes up the collision simulations, and it’s better to indirectly affect the velocity by increasing the linear damping. So that’s what we’ll do!
Add the following code to the tick method, after getting the user data:
if (sprite.tag == 1) { static int maxSpeed = 10; b2Vec2 velocity = b->GetLinearVelocity(); float32 speed = velocity.Length(); if (speed > maxSpeed) { b->SetLinearDamping(0.5); } else if (speed < maxSpeed) { b->SetLinearDamping(0.0); } } |
Here I check the tag of the sprite to see if it’s the tag for the ball object. If it is, I check the velocity and if it’s too too large, I increase the linear damping so it will eventually slow down.
If you compile and run you should see the ball goes back to a normal rate when the speed increases too much.
Gimme The Code!
Here’s the full code for the Cocos2D and Box2D Breakout Game that we’ve developed up to this point. More is coming in the next portion of the series!
What’s Next?
So far, we have a ball that bounces around the screen and a paddle we can move around via touch. In the next tutorial in the series, we pick it up from here and add some bricks that get destroyed when the ball collides into them, and some win/lose logic!
Category: iPhone