Cocos2d: Uses of Box2D Physics Engine

0
112
7 min read

 

(For more resources on Cocos2d, see here.)

 

Box2D setup and debug drawing

In our first physics recipe, we will explore the basics of creating a Box2D project and setting up a Box2D world. The example creates a scene that allows the user to create realistic 2D blocks.

Cocos2d for iPhone 1 Game Development Cookbook

Getting ready

Please refer to the project RecipeCollection02 for full working code of this recipe.

How to do it…

The first thing we need to do is create a Box2D project using the built-in Box2D project template:

  1. Go to File | New Project.
  2. Under User Templates click on Cocos2d.
  3. Now, right click on Cocos2d Box2d Application.

    Cocos2d for iPhone 1 Game Development Cookbook

  4. Click Choose, name your project, and hit Save.

Now, execute the following code:

#import "Box2D.h"
#import "GLES-Render.h"

//32 pixels = 1 meter
#define PTM_RATIO 32

@implementation Ch4_BasicSetup

-(CCLayer*) runRecipe {
[super runRecipe];

/* Box2D Initialization */

//Set gravity
b2Vec2 gravity;
gravity.Set(0.0f, -10.0f);

//Initialize world
bool doSleep = YES;
world = new b2World(gravity, doSleep);
world->SetContinuousPhysics(YES);

//Initialize debug drawing
m_debugDraw = new GLESDebugDraw( PTM_RATIO );
world->SetDebugDraw(m_debugDraw);
uint32 flags = 0;
flags += b2DebugDraw::e_shapeBit;
m_debugDraw->SetFlags(flags);

//Create level boundaries
[self addLevelBoundaries];

//Add batch node for block creation
CCSpriteBatchNode *batch = [CCSpriteBatchNode
batchNodeWithFile:@"blocks.png" capacity:150];
[self addChild:batch z:0 tag:0];

//Add a new block
CGSize screenSize = [CCDirector sharedDirector].winSize;
[self addNewSpriteWithCoords:ccp(screenSize.width/2, screenSize.
height/2)];

//Schedule step method
[self schedule:@selector(step:)];

return self;
}

/* Adds a polygonal box around the screen */
-(void) addLevelBoundaries {
CGSize screenSize = [CCDirector sharedDirector].winSize;

//Create the body
b2BodyDef groundBodyDef;
groundBodyDef.position.Set(0, 0);
b2Body *body = world->CreateBody(&groundBodyDef);

//Create a polygon shape
b2PolygonShape groundBox;

//Add four fixtures each with a single edge
groundBox.SetAsEdge(b2Vec2(0,0), b2Vec2(screenSize.width/PTM_
RATIO,0));
body->CreateFixture(&groundBox,0);

groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO),
b2Vec2(screenSize.width/PTM_RATIO,screenSize.height/PTM_RATIO));
body->CreateFixture(&groundBox,0);

groundBox.SetAsEdge(b2Vec2(0,screenSize.height/PTM_RATIO),
b2Vec2(0,0));
body->CreateFixture(&groundBox,0);

groundBox.SetAsEdge(b2Vec2(screenSize.width/PTM_RATIO,screenSize.
height/PTM_RATIO), b2Vec2(screenSize.width/PTM_RATIO,0));
body->CreateFixture(&groundBox,0);
}

/* Adds a textured block */
-(void) addNewSpriteWithCoords:(CGPoint)p {
CCSpriteBatchNode *batch = (CCSpriteBatchNode*) [self
getChildByTag:0];

//Add randomly textured block
int idx = (CCRANDOM_0_1() > .5 ? 0:1);
int idy = (CCRANDOM_0_1() > .5 ? 0:1);
CCSprite *sprite = [CCSprite spriteWithBatchNode:batch
rect:CGRectMake(32 * idx,32 * idy,32,32)];
[batch addChild:sprite];
sprite.position = ccp( p.x, p.y);

//Define body definition and create body
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO);
bodyDef.userData = sprite;
b2Body *body = world->CreateBody(&bodyDef);

//Define another box shape for our dynamic body.
b2PolygonShape dynamicBox;
dynamicBox.SetAsBox(.5f, .5f);//These are mid points for our 1m box

//Define the dynamic body fixture.
b2FixtureDef fixtureDef;
fixtureDef.shape = &dynamicBox;
fixtureDef.density = 1.0f;
fixtureDef.friction = 0.3f;
body->CreateFixture(&fixtureDef);
}

/* Draw debug data */
-(void) draw {
//Disable textures
glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);

//Draw debug data
world->DrawDebugData();

//Re-enable textures
glEnable(GL_TEXTURE_2D);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}

/* Update graphical positions using physical positions */
-(void) step: (ccTime) dt {
//Set velocity and position iterations
int32 velocityIterations = 8;
int32 positionIterations = 3;

//Steo the Box2D world
world->Step(dt, velocityIterations, positionIterations);

//Update sprite position and rotation to fit physical bodies
for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) {
if (b->GetUserData() != NULL) {
CCSprite *obj = (CCSprite*)b->GetUserData();
obj.position = CGPointMake( b->GetPosition().x * PTM_RATIO,
b->GetPosition().y * PTM_RATIO);
obj.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());
}
}
}

/* Tap to add a block */
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for( UITouch *touch in touches ) {
CGPoint location = [touch locationInView: [touch view]];
location = [[CCDirector sharedDirector] convertToGL: location];
[self addNewSpriteWithCoords: location];
}
}

@end

How it works…

The Box2D sample project is a simple way to understand what a physics system looks like.

  • Initialization:
    Upon initialization of the b2World object, we set a few things including gravity, object sleeping, and continuous physics. Sleeping allows bodies that are at rest to take up less system resources. Gravity is typically set to a negative number in the Y direction but can be reset at any time using the following method on b2World:
    void SetGravity(const b2Vec2& gravity);

    In addition to storing a pointer to the main b2World instance, we also usually store a pointer to an instance of GLESDebugDraw.

  • Debug drawing:
    Debug drawing is handled by the GLESDebugDraw class as defined in GLESRender.h. Debug drawing encompasses drawing five different elements onscreen. These include shapes, joint connections, AABBs (axis-aligned bounding boxes), broad-phase pairs, and a center of mass bit.
  • Visual to physical drawing ratio:
    We define the constant PTM_RATIO at 32, to allow consistent conversion between the physical world and the visual world. PTM stands for pixel to meter. Box2D measures bodies in meters and is built and optimized to work with bodies between the sizes of 0.1 to 10.0 meters. Setting this ratio to 32 is a common convention for optimal shapes to appear between 3.2 to 320 pixels on screen. Optimization aside, there is no upper or lower limit to Box2D body size.
  • Level boundaries:
    In this and many future examples, we add a level boundary roughly encompassing the entire screen. This is handled with the creation of a b2Body object with four fixtures. Each fixture has a b2Polygon shape that defines a single edge. Creating an edge typically involves the following:
    b2BodyDef bodyDef;
    bodyDef.position.Set(0, 0);
    b2Body *body = world->CreateBody(&bodyDef);
    b2PolygonShape poly;
    poly.SetAsEdge(b2Vec2(0,0), b2Vec2(480/PTM_RATIO,0));
    body->CreateFixture(&poly,0);

    Because these edges have no corresponding visual components (they are invisible), we do not need to set the bodyDef.userData pointer.

  • Creating the blocks:
    Blocks are created much in the same way that the level boundaries are created. Instead of calling SetAsEdge, we call SetAsBox to create a box-shaped polygon. We then set the density and friction attributes of the fixture. We also set bodyDef.userData to point to the CCSprite we created. This links the visual and the physical, and allows our step: method to reposition sprites as necessary.
  • Scheduling the world step:
    Finally, we schedule our step method. In this method, we run one discrete b2World step using the following code:
    int32 velocityIterations = 8;
    int32 positionIterations = 3;
    world->Step(dt, velocityIterations, positionIterations);

    The Box2D world Step method moves the physics engine forward one step. The Box2D constraint solver runs in two phases: the velocity phase and position phase. These determine how fast the bodies move and where they are in the game world. Setting these variables higher results in a more accurate simulation at the cost of speed. Setting velocityIterations to 8 and positionIterations to 3 is the suggested baseline in the Box2D manual. Using the dt variable syncs the logical timing of the application with the physical timing. If a game step takes an inordinate amount of time, the physics system will move forward quickly to compensate. This is referred to as a variable time step. An alternative to this would be a fixed time step set to 1/60th of a second. In addition to the physical step, we also reposition and re-orientate all CCSprites according to their respective b2Body positions and rotations:

    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) {
    if (b->GetUserData() != NULL) {
    CCSprite *obj = (CCSprite*)b->GetUserData();
    obj.position = CGPointMake( b->GetPosition().x * PTM_RATIO,
    b->GetPosition().y * PTM_RATIO);
    obj.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle());
    }
    }

    Taken together, these pieces of code sync the physical world with the visual.

 

LEAVE A REPLY

Please enter your comment!
Please enter your name here