Physics engines are what drive popular iOS games like Angry Birds, Stick Golf, Jelly Car, and Stair Dismount. They allow you to create a world that feels more dynamic and lifelike.
Cocos2d is distributed with two physics engines: Box2D and Chipmunk. Both are designed to work only in two dimensions, so they're a perfect fit for cocos2d.
In this chapter, you'll learn the basics of both physics engines, and along the way you'll probably come to appreciate one more than the other. I'll briefly explain the differences between the two physics engines, but for the most part it's a choice based on personal preference.
If you've never worked with a physics engine before, I'll also give you a quick introduction to their basic concepts and key elements.
You can think of a physics engine as an animation system for game objects. Of course, it's up to the game developer to connect and synchronize game objects like sprites with the physics objects, called rigid bodies. They are called that because physics engines animate them as if they were stiff, nondeformable objects. This simplification allows physics engines to calculate a large number of bodies.
There are generally two types of bodies: dynamic (moving) and static (immovable) objects. The differentiation is important because static bodies never move—and should never be moved—and the physics engine can rely on certain optimizations based on the fact that static bodies never collide with each other.
Dynamic bodies, on the other hand, collide with each other and with static bodies. They also have at least three defining parameters in addition to their position and rotation. One is density, or mass—in other words, a measure of how heavy an object is. Then there is friction—how resistant or slippery the dynamic body is with respect to moving over surfaces. Finally, there is restitution, which determines the bounciness of the object. While impossible in the real world, physics engines can create dynamic bodies that will never lose momentum as they bounce, or even gain speed every time they bounce off of some other body.
Both dynamic and static bodies have one or more shapes that determine the area the body encompasses. Most often the shape is a circle or a rectangle, but it can also be a polygon, a number of vertices forming any complex shape, or merely a straight line. The shapes of a body determine where other bodies and their shapes collide. And in turn, each collision generates contact points—the points where the two bodies' shapes intersect. These contact points can be used to play particle effects or add scratch marks at exactly the places where the bodies have collided.
Dynamic bodies are animated by the physics engine through applying forces, impulses, and torque instead of setting their position and rotation directly. Modifying position and rotation directly is advised against, because physics engines make certain predictions that no longer hold true if you manually reposition bodies.
Finally, bodies can be connected together by using a selection of joints, which limit the movement of connected bodies in various ways. Some joints may have motors, which for example can act as the drive wheel of a car or as friction for the joint so that the joint tries to snap back to its original position.
Physics engines have their limits. They have to take shortcuts because the real world is prohibitively complex to simulate. The use of rigid bodies is such an example. In some extreme cases, physics engines may not be able to catch every collision—for example, when bodies are moving very fast, in which case they can tunnel through each other. While this has been proven to happen in quantum physics, real-world objects that we can actually see with our own eyes have yet to show that effect.
Rigid bodies can sometimes penetrate each other and get stuck, especially if they are constrained by joints that hold two or more bodies together and limit their range of motion. This can lead to undesirable trembling motions of the bodies as they struggle to move apart while keeping their joint constraints satisfied. You may have seen this even in modern games; for example, the ragdolls of dead actors in a first-person shooter game can sometimes be seen in very unnatural body positions or limbs not coming to rest at all. Such events continue to amaze and amuse players.
And of course there can be game-play issues. With rigid bodies, you never know what will happen given enough players interacting with them. Eventually some players may manage to block themselves and trap themselves in a dead-end situation, or they may figure out how to exploit the physics simulation to be able to move to areas they shouldn't be able to reach.
Cocos2d is distributed with two physics engines: Box2D and Chipmunk. How should you choose between them?
In a lot of cases, this boils down to a matter of taste. Most developers argue along the lines of the programming language in which the physics engines are implemented: Box2D is written entirely in C++, while Chipmunk is written in C.
You may favor Box2D over Chipmunk simply because of its C++ interface. Being written in C++ has the added advantage that it integrates better with the likewise object-oriented Objective-C language. You may also appreciate that Box2D uses fully written-out words throughout, as opposed to the many one-letter abbreviations common in Chipmunk. In addition, Box2D makes use of operator overloading so that you can, for example, add two vectors simply by writing the following:
b2Vec2 newVec = vec1 + vec2;
Box2D has a few features that Chipmunk doesn't offer. For example, it has a solution for fast-moving objects (bullets) that solves the tunneling problem I mentioned earlier.
If you're not very familiar with C++, you may find the steep learning curve of the C++ language daunting. To that end, the Chipmunk physics engine may be more welcoming to you if you're more familiar with C language syntax or prefer a lightweight implementation of a physics engine that is easier to pick up and learn. Its being part of the cocos2d distribution for many months longer than Box2D has also spawned more tutorials and forum posts about Chipmunk, although Box2D tutorials are catching on.
One warning ahead of time: Chipmunk uses C structures, which expose internal fields. If you're experimenting and don't know what certain fields are used for and they're not documented, that means you should not change them—because they are used only internally.
There is also the popular Chipmunk SpaceManager, which adds an easy-to-use Objective-C interface to Chipmunk. SpaceManager also makes it easy to attach cocos2d sprites to bodies and adds debug drawing, among other things. You can download Chipmunk SpaceManager here: http://code.google.com/p/chipmunk-spacemanager
.
In terms of functionality, you can safely choose either engine. Unless your game relies on one particular feature that one physics engine has and the other doesn't, you can use either one to great effect. Especially if you have no familiarity with either physics engine, feel free to choose the one that appeals to you more based on the language and coding style.
I will now introduce you to the basics of both physics engines for the rest of this chapter so that you can decide for yourself which one appeals to you more. In the next chapter, you'll learn how to build a playable pinball game with bumpers, flippers, and lanes built with Box2D and the VertexHelper tool.
The Box2D physics engine is written in C++. It was developed by Erin Catto, who has given presentations about physics simulations at every Game Developers Conference (GDC) since 2005. It was his GDC 2006 presentation that eventually led to the public release of Box2D in September 2007. It has been in active development ever since.
Because of its popularity, the Box2D physics engine is distributed with cocos2d. You can create a new project using Box2D by choosing the cocos2d Box2D application template from Xcode's File
New Project
dialog. This project template adds the necessary Box2D source files to the project and provides a test project in which you can add boxes that bounce off each other, as shown in Figure 12–1. They also fall according to gravity, depending on how you're holding your device.
CAUTION: Because the Box2D physics engine is written in C++, you have to use the file extension .mm instead of .m for all your project's implementation files. This tells Xcode to treat the implementation file's source code as either Objective-C++ or C++ code. With the .m file extension, Xcode will compile the code as Objective-C and C code and won't understand Box2D's C++ code, which will result in numerous compile errors for every line of code that uses or makes a reference to Box2D. So if you're getting a lot of errors, check that your implementation files all end in .mm, and if not, rename them.
Documentation for Box2D is available in two places. First, you can read the Box2D manual online at www.box2d.org/manual.html
, which introduces you to common concepts and shows example code. The Box2D API reference is distributed with Box2D itself, which you can download at http://code.google.com/p/box2d
. You can also find the Box2D API reference in the Box2D version distributed with the book's source code. The API reference is in the folder /Physics Engine Libraries/Box2D_v2.1.2/Box2D/Documentation/API — to view it, locate and open the file index.html in that folder.
If you like Box2D, you should also consider donating to the project; you can do so via the Donate
button on its home page: www.box2d.org
.
Because the example project provided by cocos2d is quite complex, I decided to break it down into smaller pieces and re-create the example project step by step but not without adding some extras and variations.
Listing 12–1 shows the HelloWorldScene header file from the PhysicsBox2D01 project.
Listing 12–1. The Box2D Hello World Interface
#import "cocos2d.h"
#import "Box2D.h"
#import "GLES-Render.h"
enum
{
kTagBatchNode,
};
@interface HelloWorld : CCLayer
{
b2World* world;
}
+(id) scene;
@end
It's fairly standard, except that it includes the Box2D.h header file and adds a member variable of type b2World
. This is the physics world—think of it as the container class that will store and update all physics bodies.
The Box2D world is initialized by creating a new b2World
object in the HelloWorldScene
's init
method, as shown in Listing 12–2.
Listing 12–2. Initializing the Box2D World
b2Vec2 gravity = b2Vec2(0.0f, -10.0f);
bool allowBodiesToSleep = true;
world = new b2World(gravity, allowBodiesToSleep);
Remember that Box2D is written in C++. To instantiate one of Box2D's classes, you have to add the new
keyword in front of the class's name. If Box2D were written in Objective-C, the equivalent line might look like this:
world = [[b2World alloc] initWithGravity:gravity allowSleep:allowBodiesToSleep];
In other words, the new
keyword in C++ is equivalent to sending the alloc
message to an Objective-C class followed by an init
message. That of course means you also have to deallocate the Box2D world. In C++ this is done using the delete
keyword:
The Box2D world in Listing 12–2 is initialized with an initial gravity vector and a flag that determines whether the dynamic bodies are allowed to fall asleep.
Sleeping bodies? It's a trick that allows the physics simulation to quickly skip over objects that do not need processing. A dynamic body goes to sleep when the forces applied to it have been below a threshold for a certain amount of time. In other words, if the dynamic body is moving and rotating very slowly or not at all, the physics engine will flag it as sleeping and won't apply forces to it anymore—that is, unless an impulse or force applied to the body is strong enough to make the body move or rotate again. This trick allows the physics engine to save time by not processing the bodies that are at rest. Unless all of your game's dynamic bodies are in constant motion, you should enable this feature by setting the allowBodiesToSleep
variable to true
, as in Listing 12–2.
The gravity passed to Box2D is a b2Vec2struct
type. It's essentially the same as a CGPoint
, because it stores x and y float values. In this case, and fortunately for us in the real world too, gravity is a constant force. The 0, –10 vector is constantly applied to all dynamic bodies, making them fall down, which in this case means toward the bottom of the screen.
World setup, check. What next? Well, we should limit the movement of the Box2D bodies to within the visible screen area. For that, we'll need a static body. The simplest way to create a static body is by using the world's CreateBody
method and an empty body definition:
// Define the static container body, which will provide the collisions at screen borders
b2BodyDef containerBodyDef;
b2Body* containerBody = world->CreateBody(&containerBodyDef);
Bodies are always created through the world's CreateBody
method. This ensures that the body's memory is correctly allocated and freed. The b2BodyDef
is a struct that holds all the data needed to create a body, such as position and the body type. By default, an empty body definition creates a static body at position 0, 0.
NOTE: The &containerBodyDef
variable is passed with a leading &
(ampersand) character to the CreateBody
method. That's C++ for “Give me the memory address of containerBodyDef
.” If you look at the definition of the CreateBody
method, it requires a pointer passed to it: b2World::CreateBody(const b2BodyDef *def);
. Since pointers store a memory address, you can get that address of a nonpointer variable by prefixing it with the ampersand character.
The body itself won't do anything. To make it enclose the screen area, you'll have to create a shape with four sides:
// Create the screen box sides by using a polygon assigning each side individually
b2PolygonShape screenBoxShape;
int density = 0;
// Bottom
screenBoxShape.SetAsEdge(lowerLeftCorner, lowerRightCorner);
containerBody->CreateFixture(&screenBoxShape, density);
// Top
screenBoxShape.SetAsEdge(upperLeftCorner, upperRightCorner);
containerBody->CreateFixture(&screenBoxShape, density);
// Left side
screenBoxShape.SetAsEdge(upperLeftCorner, lowerLeftCorner);
containerBody->CreateFixture(&screenBoxShape, density);
// Right side
screenBoxShape.SetAsEdge(upperRightCorner, lowerRightCorner);
containerBody->CreateFixture(&screenBoxShape, density);
You may notice the missing declarations for the corner variable names. I'll get to them in a moment. First I'd like you to focus on how the b2PolygonShape screenBoxShape
variable is reused. Each SetAsEdge
method call is followed by a call to containerBody->CreateFixture()
, which uses the ->
operator to denote that containerBody
is a C-style pointer and passes &screenBoxShape
. Since Box2D makes a copy of screenBoxShape
, you can safely reuse the same shape to create all four sides enclosing the screen area without modifying or overriding the previous lines. Since the body is a static body (the default setting for Box2D bodies), density
doesn't matter and is set to 0
.
NOTE: The b2PolygonShape
class has a SetAsBox
method that looks like it might make the definition of the screen area easier by simply providing the screen's width and height. However, that would make the inside of the body a solid object, and any dynamic body added to the screen would actually be contained inside the solid shape. This would make the dynamic bodies try to move away from the collision, possibly at rapid speeds. The sides need to be created separately in order to make only the sides of the screen solid.
Now we'll move on to the missing variable declarations. Notice that the screen width and height is divided by a PTM_RATIO
constant to convert them from pixels to meters:
// For the ground body we'll need these values
CGSize screenSize = [CCDirector sharedDirector].winSize;
float widthInMeters = screenSize.width / PTM_RATIO;
float heightInMeters = screenSize.height / PTM_RATIO;
b2Vec2 lowerLeftCorner = b2Vec2(0, 0);
b2Vec2 lowerRightCorner = b2Vec2(widthInMeters, 0);
b2Vec2 upperLeftCorner = b2Vec2(0, heightInMeters);
b2Vec2 upperRightCorner = b2Vec2(widthInMeters, heightInMeters);
Why meters, and what is PTM_RATIO
? Box2D is optimized to work best with dimensions in the range of 0.1 to 10 meters. It is tuned for the metric system, so all distances are considered to be meters, all masses are in kilograms, and time is measured in—quite oddly—seconds. If you're not familiar with the meters, kilograms, and seconds (MKS) system, don't worry—you don't have to meticulously convert yards into meters and pounds into kilograms. The conversion to meters is just a way to keep the distance values for Box2D in the desirable range of 0.1 to 10, and the masses used by bodies do not resemble real-world masses anyway. The masses of bodies will often need to be tweaked by feel rather than by using realistic weights.
You should try to keep the dimensions of objects in your world as close to 1 meter as much as possible. That is not to say that you can't have objects that are smaller than 0.1 meters or larger than 10 meters, but you may run into glitches and strange behavior if you create relatively small or large bodies.
The PTM_RATIO
is defined like this:
#define PTM_RATIO 32
It is used to define that 32 pixels on the screen equal 1 meter in Box2D. A box-shaped body that's 32 pixels wide and high will be 1 meter wide and high. A body that's 4×4 pixels in size will be 0.125×0.125 meters in Box2D, while a relatively huge object of 256×256 pixels will be 8×8 meters in Box2D. The PTM_RATIO
allows you to scale the size of Box2D objects down to the dimensions within which Box2D works best, and a PTM_RATIO
of 32 is a good compromise for a screen area that may be as large as 1024×768 pixels on the iPad.
Note that the b2Vec2struct
is different from CGPoint
, which means you cannot use a CGPoint
where a b2Vec2
is required, and vice versa. In addition, Box2D points need to be converted to meters and back to pixels. To avoid making any mistakes, such as forgetting to convert from or to meters or simply making a typo and using the x coordinate twice, it's highly recommended to wrap this repetitive code into convenience methods like these:
-(b2Vec2) toMeters:(CGPoint)point
{
return b2Vec2(point.x / PTM_RATIO, point.y / PTM_RATIO);
}
-(CGPoint) toPixels:(b2Vec2)vec
{
return ccpMult(CGPointMake(vec.x, vec.y), PTM_RATIO);
}
This allows you to write the following code to easily convert between CGPoint
and pixels to b2Vec2
and meters:
// Box2D coordinates to Cocos2D point coordinates
b2Vec2 vec = b2Vec2(200, 200);
CGPoint pointFromVec = [self toPixels:vec];
// Cocos2D point coordinates to Box2D coordinates
CGPoint point = CGPointMake(100, 100);
b2Vec2 vecFromPoint = [self toMeters:point];
With a static body containing the objects within screen boundaries, all that's missing is something to be kept within the screen boundaries. How about little boxes, then?
I've added David Gervais' orthogonal tileset image dg_grounds32.png to the Resources folder of the PhysicsBox2D01 project. The tiles are 32×32 pixels, so they'll make perfect 1×1-meter boxes. Listing 12–3 is the code in the init
method that adds the texture and creates a couple boxes. It also schedules the update
method, which is needed to update the box sprite positions, and it enables touch so that the user can tap the screen to create a new box.
Listing 12–3. Adding an Initial Set of Boxes
// Use the orthogonal tileset for the little boxes
CCSpriteBatchNode* batch = [CCSpriteBatchNode
batchNodeWithFile:@"dg_grounds32.png"
capacity:150];
[self addChild:batch z:0 tag:kTagBatchNode];
// Add a few objects initially
for (int i = 0; i < 11; i++)
{
[self addNewSpriteAt:CGPointMake(screenSize.width / 2, screenSize.height / 2)];
}
[self scheduleUpdate];
self.isTouchEnabled = YES;
The addNewSpriteAt
method shown in Listing 12–4 is part of the cocos2d Box2D application template project but slightly modified to make use of all the tiles in the tileset.
Listing 12–4. Adding a New Dynamic Body with a Sprite
-(void) addNewSpriteAt:(CGPoint)pos
{
CCSpriteBatchNode* batch =
(CCSpriteBatchNode*) [self getChildByTag:kTagBatchNode];
int idx = CCRANDOM_0_1() * TILESET_COLUMNS;
int idy = CCRANDOM_0_1() * TILESET_ROWS;
CGRect tileRect = CGRectMake(TILESIZE * idx, TILESIZE * idy, TILESIZE, TILESIZE);
CCSprite* sprite = [CCSprite spriteWithBatchNode:batch rect:tileRect];
sprite.position = pos;
[batch addChild:sprite];
// Create a body definition and set it to be a dynamic body
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position = [self toMeters:pos];
bodyDef.userData = sprite;
b2Body* body = world->CreateBody(&bodyDef);
// Define a box shape and assign it to the body fixture
b2PolygonShape dynamicBox;
float tileInMeters = TILESIZE / PTM_RATIO;
dynamicBox.SetAsBox(tileInMeters * 0.5f, tileInMeters * 0.5f);
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 0.3f;
fixtureDef.friction = 0.5f;
fixtureDef.restitution = 0.6f;
body->CreateFixture(&fixtureDef);
}
First, a sprite is created from the CCSpriteBatchNode
by using CCSprite
's spriteWithBatchNode
initializer and supplying a CGRect
that is 32×32 pixels in size, to randomly pick one of the tileset's tiles as the sprite's image.
Then a body is created, but this time the b2BodyDef
type property is set to b2_dynamicBody
, which makes it a dynamic body that can move around and collide with other dynamic bodies. The previously created sprite is assigned to the userData
property. Later, when you are iterating over the bodies in the world, this allows you to quickly access the body's sprite.
The body's shape is a b2PolygonShape
set to a box shape that is half a meter in size. The SetAsBox
method creates a box shape that is twice the given width and height, so the coordinates need to be divided by 2—or, as in this case, multiplied by 0.5f to create a box shape whose sides are 1 meter wide and high.
The dynamic body also needs a fixture that contains the body's essential parameters—first and foremost the shape but also the density, friction, and restitution—which influence how the body moves and bounces around in the world. Consider the fixture to be a set of data used by bodies.
The box sprites won't follow their physics bodies automatically, and the bodies won't do anything unless you regularly call the Step
method of the Box2D world. You then have to update the sprite positions by taking the body position and angle and assigning it to the sprite. This is done in the update
method shown in Listing 12–5.
Listing 12–5. Updating Each Body's Sprite Position and Rotation
-(void) update:(ccTime)delta
{
// Advance the physics world by one step, using fixed time steps
float timeStep = 0.03f;
int32 velocityIterations = 8;
int32 positionIterations = 1;
world->Step(timeStep, velocityIterations, positionIterations);
for (b2Body* body = world->GetBodyList(); body != nil; body = body->GetNext())
{
CCSprite* sprite = (CCSprite*)body->GetUserData();
if (sprite!= NULL)
{
sprite.position = [self toPixels:body->GetPosition()];
float angle = body->GetAngle();
sprite.rotation = CC_RADIANS_TO_DEGREES(angle) * -1;
}
}
}
The Box2D world is animated by regularly calling the Step
method. It takes three parameters. The first is timeStep
, which tells Box2D how much time has passed since the last step. It directly affects the distance that objects will move in this step. For games, it is not recommended to pass the delta time as timeStep
, because the delta time fluctuates, and so the speed of the physics bodies will not be constant. This effect rears its ugly head when the device may be taking a tenth of a second to do background processing, such as sending or receiving an e-mail in the background. This can make all physics objects move large distances in the next frame. In a game, you'd rather have the game stop for a tenth of a second and then carry on where you left off. Without a fixed time step, the physics engine would try to cope with a short interruption by moving all objects based on the time difference. If the time difference is large, the objects will move a lot more in a single frame, and that can lead to them suddenly moving a large distance.
The second and third parameters to the Step
method are the number of iterations. They determine the accuracy of the physics simulation and also the time it takes to calculate the movement of the bodies. It's a trade-off between speed and accuracy. In the case of position iterations, you can safely err on the side of speed and require only a single iteration—more position accuracy is not normally needed in games unless you experience objects not coming to rest, colliding in unnatural ways, or missing collision entirely. Velocity is more important, however; a good starting point for velocity iterations is eight. More than ten velocity iterations have no discernable effect in games, but just one to four iterations won't be enough to get a stable simulation. The fewer the velocity iterations, the more bumpily and restlessly the objects will behave. I encourage you to experiment with these values.
After the world has advanced one step, the for
loop iterates over all of the world's bodies using the world->GetBodyList
and body->GetNext
methods. For each body, its user data is returned and cast to a CCSprite
pointer. If it exists, the body's position is converted to pixels and assigned to the sprite's position so that the sprite moves along with the body. Likewise, the body's angle is obtained; because that measurement is in radians, it's converted to degrees using cocos2d's CC_RADIANS_TO_DEGREES
method and multiplied by –1 to rotate the sprites in the same direction as the body.
Box2D has a b2ContactListener
class, which you are supposed to subclass if you want to receive collision callbacks. The following code refers to the PhysicsBox2D02 project.
Create a new class in Xcode and name it ContactListener
. Then rename the implementation file to ContactListener.mm so it has the file extension .mm. Then you will be able to use both C++ and Objective-C code in the same file. Listing 12–6 shows the ContactListener
header file.
Listing 12–6. The ContactListener Class's Interface
#import "Box2D.h"
class ContactListener : public b2ContactListener
{
private:
void BeginContact(b2Contact* contact);
void EndContact(b2Contact* contact);
};
It's a C++ class, so the class definition is a little different. Note the trailing semicolon after the last bracket. It's a common error to forget that semicolon. The BeginContact
and EndContact
methods are defined by Box2D and get called whenever there is a collision between two bodies.
In the implementation, I merely change the sprite's colors to magenta while the two bodies are in contact and set it back to white when they are no longer in contact, as shown in Listing 12–7.
Listing 12–7. Checking for the Beginning and Ending of Collisions
#import "ContactListener.h"
#import "cocos2d.h"
void ContactListener::BeginContact(b2Contact* contact)
{
b2Body* bodyA = contact->GetFixtureA()->GetBody();
b2Body* bodyB = contact->GetFixtureB()->GetBody();
CCSprite* spriteA = (CCSprite*)bodyA->GetUserData();
CCSprite* spriteB = (CCSprite*)bodyB->GetUserData();
if (spriteA != NULL && spriteB != NULL)
{
spriteA.color = ccMAGENTA;
spriteB.color = ccMAGENTA;
}
}
void ContactListener::EndContact(b2Contact* contact)
{
b2Body* bodyA = contact->GetFixtureA()->GetBody();
b2Body* bodyB = contact->GetFixtureB()->GetBody();
CCSprite* spriteA = (CCSprite*)bodyA->GetUserData();
CCSprite* spriteB = (CCSprite*)bodyB->GetUserData();
if (spriteA != NULL && spriteB != NULL)
{
spriteA.color = ccWHITE;
spriteB.color = ccWHITE;
}
}
b2Contact
contains all the contact information, including two sets of everything suffixed with A
and B
. These are the two contacting bodies; no differentiation is made as to which is colliding with the other—they are both simply colliding with each other. If, for example, you have an enemy colliding with a player's bullet, you would want to damage the enemy, not the bullet. It is up to you to determine which is which. Also keep in mind that the contact methods may be called multiple times per frame, once for each contact pair.
It's a bit convoluted to get to the sprite from the contact through the fixture to the body and then get the user data from that. The Box2D API reference certainly helps you find your way through the hierarchy, and with a little experience this will become second nature.
To actually get the ContactListener
connected with Box2D, you have to add it to the world. In HelloWorldScene
, import the ContactListener.h header file and add a ContactListener* contactListener
to the class as a member variable:
#import "cocos2d.h"
#import "Box2D.h"
#import "GLES-Render.h"
#import "ContactListener.h"
...
@interface HelloWorld : CCLayer
{
b2World* world;
ContactListener* contactListener;
}
...
@end
In the init
method of HelloWorldScene
, you can then create a new ContactListener
instance and set it as the contact listener for the world:
contactListener = new ContactListener();
world->SetContactListener(contactListener);
What remains is to delete the contactListener
in the dealloc
method to free its memory:
-(void) dealloc
{
delete contactListener;
delete world;
[super dealloc];
}
Now the boxes in the PhysicsBox2D02 project will be tinted purple whenever they touch other boxes.
With joints, you can connect bodies together. The type of joint determines which way the connected bodies are connected. In this example method, I create four bodies total. Three are dynamic bodies connected to each other using a revolute joint, which keeps the bodies at the same distance but allows them to rotate 360 degrees around each other. If you find it hard to imagine how these objects might behave, you should try them in the PhysicsBox2D02 project. In this case, working code can explain it better than words or a static image could. The fourth body is a static body to which one of the dynamic bodies is also attached using a revolute joint.
-(void) addSomeJoinedBodies:(CGPoint)pos
{
// Create a body definition and set it to be a dynamic body
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
// Position must be converted to meters
bodyDef.position = [self toMeters:pos];
bodyDef.position = bodyDef.position + b2Vec2(-1, -1);
bodyDef.userData = [self addRandomSpriteAt:pos];
b2Body* bodyA = world->CreateBody(&bodyDef);
[self bodyCreateFixture:bodyA];
bodyDef.position = [self toMeters:pos];
bodyDef.userData = [self addRandomSpriteAt:pos];
b2Body* bodyB = world->CreateBody(&bodyDef);
[self bodyCreateFixture:bodyB];
bodyDef.position = [self toMeters:pos];
bodyDef.position = bodyDef.position + b2Vec2(1, 1);
bodyDef.userData = [self addRandomSpriteAt:pos];
b2Body* bodyC = world->CreateBody(&bodyDef);
[self bodyCreateFixture:bodyC];
// Create the revolute joints
b2RevoluteJointDef jointDef;
jointDef.Initialize(bodyA, bodyB, bodyB->GetWorldCenter());
bodyA->GetWorld()->CreateJoint(&jointDef);
jointDef.Initialize(bodyB, bodyC, bodyC->GetWorldCenter());
bodyA->GetWorld()->CreateJoint(&jointDef);
// Create an invisible static body and attach body A to it
bodyDef.type = b2_staticBody;
bodyDef.position = [self toMeters:pos];
b2Body* staticBody = world->CreateBody(&bodyDef);
jointDef.Initialize(staticBody, bodyA, bodyA->GetWorldCenter());
bodyA->GetWorld()->CreateJoint(&jointDef);
}
b2BodyDef
is reused for all bodies; only the position is modified for each body, and a random CCSprite
is created and assigned as userData
. The addRandomSpriteAt
method contains the code that creates a sprite from the CCSpriteBatchNode
, as discussed earlier. Since in the addSomeJoinedBodies
method there are now several sprites needed, it made sense to refactor the creation of a sprite into the method addRandomSpriteAt
.
b2RevoluteJointDef
is filled with data by using the Initialize
method providing two bodies to connect to each other and a coordinate where the joint is located. By using one body's GetWorldCenter
coordinate, that body will be centered on the joint and allowed only to rotate around itself.
The joint is created by the CreateJoint
method of the b2World
class. Even though the HelloWorldScene
class in the PhysicsBox2D02 project has a b2World
member variable, I wanted to illustrate that you can also get the world through any body—it doesn't matter which one—by using the body's GetWorld
method. This is good to know, because in the ContactListener
discussed earlier, you do not have a b2World
member variable, so you'll have to get the b2World
pointer through one of the contact bodies.
The Chipmunk physics engine was developed by Scott Lembcke of Howling Moon Software. Chipmunk was actually inspired by an early version of Box2D, before it was a full-fledged physics engine. You can download Chipmunk from its Google Code site. If you like Chipmunk, you should also consider donating to the project, which you can do via the Donate
button from the same site: http://code.google.com/p/chipmunk-physics
. The Chipmunk documentation is located on Scott's home page, at http://files.slembcke.net/chipmunk/release/ChipmunkLatest-Docs
. And if you need help, you can find that in the Chipmunk forums: www.slembcke.net/forums
.
There are actually two Objective-C wrappers available for Chipmunk, and more are being worked on. I don't discuss them in this book, but you should be aware of them and try them.
Scott's Objective-C wrapper is part of the Chipmunk distribution but works only in the iPhone Simulator. To be able to deploy it to the iPhone, you have to buy his Chipmunk Objective-C wrapper on the Howling Moon Software web site, at http://howlingmoonsoftware.com/objectiveChipmunk.php
.
It's always best to try before you buy, so you can follow this tutorial on how to use the Objective-C wrapper for the iPhone Simulator: http://files.slembcke.net/chipmunk/tutorials/SimpleObjectiveChipmunk
.
The alternative is Chipmunk SpaceManager, written by Robert Blackwood, which comes with a free Objective-C wrapper for Chipmunk. However, its main purpose is to make integration of Chipmunk with cocos2d easier. If you would like to try Chipmunk SpaceManager, you can download it from the following link, where you can also donate to the project should you like it: http://code.google.com/p/chipmunk-spacemanager
.
The SpaceManager API reference can be found on Robert's home page at www.mobile-bros.com/spacemanager/docs
.
Both the Chipmunk and SpaceManager distributions, including their respective documentation files, are also in this chapter's source code folder, in the Physics Engine Librariessubfolder.
For the Chipmunk tutorial, I'll be building the same project as before. I'll start with the PhysicsChipmunk01 project and the initial setup of the Chipmunk physics engine. Listing 12–8 shows the HelloWorldScene
header file.
Listing 12–8. The Chipmunk HelloWorld Interface
#import "cocos2d.h"
#import "chipmunk.h"
enum
{
kTagBatchNode = 1,
};
@interface HelloWorld : CCLayer
{
cpSpace* space;
}
+(id) scene;
@end
There's nothing unusual here, except for the cpSpace
member variable. Instead of world, Chipmunk calls its world a space. It's a different term for the same thing. The Chipmunk space contains all the rigid bodies.
Chipmunk is initialized in HelloWorldScene
's init
method as follows:
cpInitChipmunk();
space = cpSpaceNew();
space->iterations = 8;
space->gravity = CGPointMake(0, -100);
The very first thing you must do before using any Chipmunk methods is to call cpInitChipmunk
. After that, you can create the space with cpSpaceNew
and set the number of iterations—in this case eight. This is the same iteration count we used for velocity iterations in the Box2D example's update
method. Chipmunk knows only one type of iteration—the elasticIterations
field is deprecated and should no longer be used. I mention this in case you are already familiar with Chipmunk. You may get away with fewer than eight iterations if your game does not allow objects to stack; otherwise, you may find that stacked objects never get to rest and keep jittering and sliding for a long period of time.
Notice how Chipmunk can use the same CGPoint
structure used in the iPhone SDK. Chipmunk internally uses a structure called cpVect
, but in cocos2d you can use both interchangeably. I use a CGPoint
to set the gravity to –100, which means downward acceleration that is roughly the same as that used in the Box2D project.
Of course, the space also needs to be released in the dealloc
method; this is done by calling cpSpaceFree
and passing the space as a parameter:
-(void) dealloc
{
cpSpaceFree(space);
[super dealloc];
}
To keep all the boxes within the boundaries of the screen, you need to create a static body whose shape defines the screen area. First, the variables for the screen's corners are defined:
// For the ground body we'll need these values
CGSize screenSize = [CCDirector sharedDirector].winSize;
CGPoint lowerLeftCorner = CGPointMake(0, 0);
CGPoint lowerRightCorner = CGPointMake(screenSize.width, 0);
CGPoint upperLeftCorner = CGPointMake(0, screenSize.height);
CGPoint upperRightCorner = CGPointMake(screenSize.width, screenSize.height);
Contrary to Box2D, you do not have to take any pixel-to-meter ratio into account. You can use the screen size in pixels as it is to define the corner points and to work with Chipmunk bodies in general.
Next you'll create the static body by using the cpBodyNew
and passing INFINITY
for both parameters, which makes the body a static body. Those parameters are mass and inertia, and with them being set to the INFINITY
value, this body isn't going to go anywhere.
// Create the static body that keeps objects within the screen area
float mass = INFINITY;
float inertia = INFINITY;
cpBody* staticBody = cpBodyNew(mass, inertia);
NOTE: Mass and inertia in Chipmunk are comparable to density and friction in Box2D. The difference between inertia and friction is that the former determines the resistance of a body to start moving, while the latter determines how much motion a body loses when it is in contact with other bodies.
Next, you'll define the shape that goes with the body and makes up the screen borders, as shown in Listing 12–9.
Listing 12–9. Creating the Screen Border Collisions
cpShape* shape;
float elasticity = 1.0f;
float friction = 1.0f;
float radius = 0.0f;
// Bottom
shape = cpSegmentShapeNew(staticBody, lowerLeftCorner, lowerRightCorner, radius);
shape->e = elasticity;
shape->u = friction;
cpSpaceAddStaticShape(space, shape);
// Top
shape = cpSegmentShapeNew(staticBody, upperLeftCorner, upperRightCorner, radius);
shape->e = elasticity;
shape->u = friction;
cpSpaceAddStaticShape(space, shape);
// Left side
shape = cpSegmentShapeNew(staticBody, lowerLeftCorner, upperLeftCorner, radius);
shape->e = elasticity;
shape->u = friction;
cpSpaceAddStaticShape(space, shape);
// Right side
shape = cpSegmentShapeNew(staticBody, lowerRightCorner, upperRightCorner, radius);
shape->e = elasticity;
shape->u = friction;
cpSpaceAddStaticShape(space, shape);
The cpSegmentShapeNew
method is used to create four new line segments to define the sides of the screen area. The shape
variable is reused for convenience, but it requires you to set elasticity (which is the same as restitution) and friction after each call to cpSegmentShapeNew
. Then each shape is added to the space as a static shape via the cpSpaceAddStaticShape
method.
NOTE: In Chipmunk you will have to work with one-letter fields like e
and u
regularly. Personally, I find that this makes it hard to pick up Chipmunk because you don't immediately grasp the meaning of these fields, and you have to refer to the Chipmunk documentation more often than necessary.
To add boxes to the world, I used the same code in the init
method of HelloWorldScene
as in the Box2D example. Refer to Listing 12–3 if you'd like to refresh your memory.
I'll go straight to creating the dynamic body for new boxes, which is what the addNewSpriteAt
method does (Listing 12–10).
Listing 12–10. Adding a Body with a Sprite, Chipmunk Style
-(void) addNewSpriteAt:(CGPoint)pos
{
float mass = 0.5f;
float moment = cpMomentForBox(mass, TILESIZE, TILESIZE);
cpBody* body = cpBodyNew(mass, moment);
body->p = pos;
cpSpaceAddBody(space, body);
float halfTileSize = TILESIZE * 0.5f;
int numVertices = 4;
CGPoint vertices[] =
{
CGPointMake(-halfTileSize, -halfTileSize),
CGPointMake(-halfTileSize, halfTileSize),
CGPointMake(halfTileSize, halfTileSize),
CGPointMake(halfTileSize, -halfTileSize),
};
CGPoint offset = CGPointZero;
float elasticity = 0.3f;
float friction = 0.7f;
cpShape* shape = cpPolyShapeNew(body, numVertices, vertices, offset);
shape->e = elasticity;
shape->u = friction;
shape->data = [self addRandomSpriteAt:pos];
cpSpaceAddShape(space, shape);
}
The dynamic body for the box is created with the cpBodyNew
method with the given mass and a moment of inertia. The moment of inertia determines the resistance of a body to move, and it's calculated by the helper method cpMomentForBox
, which takes the body's mass and the size of the box—in this case TILESIZE
—which makes it a 32×32-pixel box.
The body's position, p
, is then updated, and the body is added to the space via the cpSpaceAddBody
method. Note that contrary to Box2D, you do not have to convert pixels to meters; you can work with pixel coordinates directly.
Then a list of vertices are created, which will become the corners of the box shape. Because the corner positions are positioned relative to the center of the box we're creating, they are all half a tile's size away from the center. Otherwise, the box shape would be twice as big as the tile.
The cpPolyShapeNew
method then takes the body as input, the vertices
array, and the number of vertices in the array, as well as an optional offset, which is set to CGPointZero
in this case. Out comes a new cpShape
pointer for the box shape. The shape's elasticity and friction are set to values that give a similar behavior to the Box2D boxes, and after the sprite is set as user data to the data field, the shape is added to the space via cpSpaceAddShape
.
The addRandomSpriteAt
method in Listing 12–11 simply creates the CCSprite
object that goes along with the new dynamic body.
Listing 12–11. Creating New Box Objects with Random Images
-(CCSprite*) addRandomSpriteAt:(CGPoint)pos
{
CCSpriteBatchNode* batch = (CCSpriteBatchNode*)[self getChildByTag:kTagBatchNode];
int idx = CCRANDOM_0_1() * TILESET_COLUMNS;
int idy = CCRANDOM_0_1() * TILESET_ROWS;
CGRect tileRect = CGRectMake(TILESIZE * idx, TILESIZE * idy, TILESIZE, TILESIZE);
CCSprite* sprite = [CCSprite spriteWithBatchNode:batch rect:tileRect];
sprite.position = pos;
[batch addChild:sprite];
return sprite;
}
Just like with Box2D, you have to update the sprite's position and rotation to be in line with their dynamic body's position and rotation every frame. Again, this is done in the update
method:
-(void) update:(ccTime)delta
{
float timeStep = 0.03f;
cpSpaceStep(space, timeStep);
// Call forEachShape C method to update sprite positions
cpSpaceHashEach(space->activeShapes, &forEachShape, nil);
cpSpaceHashEach(space->staticShapes, &forEachShape, nil);
}
Just as with Box2D, you have to advance the physics simulation using a step method. In this case, it's cpSpaceStep
, which takes the space and a timeStep
as input. A fixed time step works best, and just like in Box2D, it's highly recommended you use a fixed time step as opposed to passing the delta time. As long as the framerate doesn't fluctuate heavily (it really shouldn't anyway), using a fixed-time-step approach works very well.
The cpSpaceHashEach
method calls the C method forEachShape
for, well, each of the shapes. Or, more accurately, the cpSpaceHashEach
method calls forEachShape
for each active shape (dynamic body) and then for each static shape (static body). With the third parameter, you can pass an arbitrary pointer as a parameter to the forEachShape
method, but because it's not needed in this case, it is set to nil
. And even though this example project doesn't have static shapes with sprites assigned to them, it nevertheless calls the method for static shapes, just in case you want to be adding some static shapes with a sprite.
The forEachShape
method is a callback method that's written in C. In the example project, you can find it at the top of the HelloWorldScene.mfile, outside the @implementation
. Although it's not strictly necessary to place the method outside the @implementation
, it signals that this method isn't part of the HelloWorldScene
class. The method is defined as static
, which effectively makes it a global method, as Listing 12–12 shows.
Listing 12–12. Updating a Body Sprite's Position and Rotation
// C method that updates sprite position and rotation:
static void forEachShape(void* shapePointer, void* data)
{
cpShape* shape = (cpShape*)shapePointer;
CCSprite* sprite = (CCSprite*)shape->data;
if (sprite != nil)
{
cpBody* body = shape->body;
sprite.position = body->p;
sprite.rotation = CC_RADIANS_TO_DEGREES(body->a) * -1;
}
}
The signature for methods passed to cpSpaceHashEach
is strictly defined; the method must take two parameters, and both are void
pointers. The second parameter would be the data pointer passed as third parameter to cpSpaceHashEach
. For both shapePointer
and data pointer, you have to know what kind of object they're pointing to; otherwise, disaster will strike in the form of EXC_BAD_ACCESS
.
In this case, I know that shapePointer
is going to point to a cpShapestruct
, so I can safely cast it and then access the shape's data field to get its CCSprite
pointer. If the sprite is a valid pointer, I can get the body from the shape and use that to set the position and rotation of the sprite to that of the body. As with Box2D before, the rotation has to be converted from radians to degrees first and then multiplied by –1 to correct the direction of the rotation.
Collisions in Chipmunk are also handled by C callback methods. In the PhysicsChipmunk02 project, I've added the contactBegin
and contactEnd
static methods (in Listing 12–13), which do exactly the same as their Box2D counterparts: change the color of boxes that are in contact to magenta.
Listing 12–13. Collision Callbacks a la Chipmunk
static int contactBegin(cpArbiter* arbiter, struct cpSpace* space, void* data)
{
bool processCollision = YES;
cpShape* shapeA;
cpShape* shapeB;
cpArbiterGetShapes(arbiter, &shapeA, &shapeB);
CCSprite* spriteA = (CCSprite*)shapeA->data;
CCSprite* spriteB = (CCSprite*)shapeB->data;
if (spriteA != nil && spriteB != nil)
{
spriteA.color = ccMAGENTA;
spriteB.color = ccMAGENTA;
}
return processCollision;
}
static void contactEnd(cpArbiter* arbiter, cpSpace* space, void* data)
{
cpShape* shapeA;
cpShape* shapeB;
cpArbiterGetShapes(arbiter, &shapeA, &shapeB);
CCSprite* spriteA = (CCSprite*)shapeA->data;
CCSprite* spriteB = (CCSprite*)shapeB->data;
if (spriteA != nil && spriteB != nil)
{
spriteA.color = ccWHITE;
spriteB.color = ccWHITE;
}
}
The contactBegin
method should return YES
if the collision should be processed normally. By returning NO
or 0
from this method, you can also ignore collisions. To get to the sprites, you first have to get the shapes from the cpArbiter
, which just like b2Contact
holds the contact information. Via the cpArbiterGetShapes
method and passing two shapes as out
parameters, you get the colliding shapes from which you can then retrieve the individual CCSprite
pointers. If they are both valid, their color can be changed.
As with Box2D, these callbacks don't get called by themselves. In the HelloWorldSceneinit
method, right after the space is created, you must add the collision handlers using the cpSpaceAddCollisionHandler
method:
unsigned int defaultCollisionType = 0;
cpSpaceAddCollisionHandler(space, defaultCollisionType, defaultCollisionType,
&contactBegin, NULL, NULL, &contactEnd, NULL);
The default collision type for shapes is 0, and because I don't care about filtering collisions, both collision type parameters are set to 0
. You can assign each body's shape an integer value to its collision_type
property and then add collision handlers that are called only if bodies of matching collision types collide. This is called filtering collisions and is described in the Chipmunk manual, at http://files.slembcke.net/chipmunk/release/ChipmunkLatest-Docs/#cpShape
.
The next four parameters are pointers to C callback methods for the four collision stages: begin, pre-solve, post-solve, and separation (the same as the EndContact
event in Box2D). These serve the same purpose as the corresponding callbacks in Box2D. Most of the time you'll be interested only in the begin
and separation
events.
I pass NULL
for pre-solve and post-solve, because I'm not interested in handling these. You can use these methods to influence the collision or to retrieve the collision force in the post-solve step. The final parameter is an arbitrary data pointer you can pass on to the callback methods if you need it. I don't, so I set it to NULL
as well.
With that, you have a working collision callback mechanism.
The Chipmunk example project also needs its own implementation of addSomeJoinedBodies
. The setup is more verbose than for Box2D, as shown in Listing 12–14. You'll recognize most of the code as setting up static and dynamic bodies—if you find that code familiar, feel free to skip to the end where the joints are created.
Listing 12–14. Creating Three Bodies Connected with Joints
-(void) addSomeJoinedBodies:(CGPoint)pos
{
float mass = 1.0f;
float moment = cpMomentForBox(mass, TILESIZE, TILESIZE);
float halfTileSize = TILESIZE * 0.5f;
int numVertices = 4;
CGPoint vertices[] =
{
CGPointMake(-halfTileSize, -halfTileSize),
CGPointMake(-halfTileSize, halfTileSize),
CGPointMake(halfTileSize, halfTileSize),
CGPointMake(halfTileSize, -halfTileSize),
};
// Create a static body
cpBody* staticBody = cpBodyNew(INFINITY, INFINITY);
staticBody->p = pos;
CGPoint offset = CGPointZero;
cpShape* shape = cpPolyShapeNew(staticBody, numVertices, vertices, offset);
cpSpaceAddStaticShape(space, shape);
// Create three new dynamic bodies
float posOffset = 1.4f;
pos.x += TILESIZE * posOffset;
cpBody* bodyA = cpBodyNew(mass, moment);
bodyA->p = pos;
cpSpaceAddBody(space, bodyA);
shape = cpPolyShapeNew(bodyA, numVertices, vertices, offset);
shape->data = [self addRandomSpriteAt:pos];
cpSpaceAddShape(space, shape);
pos.x += TILESIZE * posOffset;
cpBody* bodyB = cpBodyNew(mass, moment);
bodyB->p = pos;
cpSpaceAddBody(space, bodyB);
shape = cpPolyShapeNew(bodyB, numVertices, vertices, offset);
shape->data = [self addRandomSpriteAt:pos];
cpSpaceAddShape(space, shape);
pos.x += TILESIZE * posOffset;
cpBody* bodyC = cpBodyNew(mass, moment);
bodyC->p = pos;
cpSpaceAddBody(space, bodyC);
shape = cpPolyShapeNew(bodyC, numVertices, vertices, offset);
shape->data = [self addRandomSpriteAt:pos];
cpSpaceAddShape(space, shape);
// Create the joints and add the constraints to the space
cpConstraint* constraint1 = cpPivotJointNew(staticBody, bodyA, staticBody->p);
cpConstraint* constraint2 = cpPivotJointNew(bodyA, bodyB, bodyA->p);
cpConstraint* constraint3 = cpPivotJointNew(bodyB, bodyC, bodyB->p);
cpSpaceAddConstraint(space, constraint1);
cpSpaceAddConstraint(space, constraint2);
cpSpaceAddConstraint(space, constraint3);
}
In this example, I'm creating a pivot joint with cpPivotJointNew
, which is the same as the b2RevoluteJoint
used in the Box2D example. Each joint is created with the two bodies that should be connected to each other and one of the bodies' center position as the anchor point. The cpPivotJointNew
method returns a cpConstraint
pointer, which you'll have to add to the space using the cpSpaceAddConstraint
method.
In this chapter, you learned the basics of the two physics engines distributed with cocos2d: Box2D and Chipmunk. You now have two working examples of these physics engines at your disposal, which should help you decide which one you'd like to use.
You learned how to set up a screen area that contains all the little boxes created from a tilemap as dynamic bodies. You now also know the basics of detecting collisions and how to create joints to connect bodies together in both physics engines.
In the next chapter, you'll be making a game that uses the Box2D physics engine.