How To Make A Game Like Fruit Ninja With Box2D and Cocos2D – Part 1

This is a post by iOS Tutorial Team Member Allen Tan, an iOS developer and co-founder at White Widget. You can also find him on Google+ and Twitter. In this tutorial, you’ll learn how to make a sprite cutting game for the iPhone similar to Fruit Ninja by Halfbrick Studios using the powerful Cocos2D and […] By Allen Tan.

Leave a rating/review
Save for later
Share

This is a post by iOS Tutorial Team Member Allen Tan, an iOS developer and co-founder at White Widget. You can also find him on and Twitter.

In this tutorial, you’ll learn how to make a sprite cutting game for the iPhone similar to Fruit Ninja by Halfbrick Studios using the powerful Cocos2D and Box2D libraries along with some pre-made tools.

In most slicing games, when you draw a cut line through a sprite, the game typically converts the sprite image into two pre-drawn sprite images with the slice always down the middle, regardless of where you actually cut.

But this tutorial will demonstrate an even cooler technique. Our fruits will be able to be cut multiple times, and they will be divided dynamically based on the exact cut lines!

As you might imagine, this is an advanced technique, so this tutorial is for advanced Cocos2D and Box2D developers. If you are new to Cocos2D or Box2D, you should go through (at the least) the intro to Cocos2D and intro to Box2D tutorials first before proceeding with this tutorial.

This tutorial series is split into three parts:

  • In this first part of the series, you will lay the foundations for the game, and learn how to create textured polygons.
  • The second part will show you how to slice & split these textured polygons.
  • The third part will show you how to turn this into a complete game by adding gameplay and effects.

I would like to give special thanks to Rick Smorawski for laying the foundations for the project this tutorial is based on. He was responsible for porting this flash-based slicing demo into Cocos2D, and also for porting CCBlade and PRKit to Cocos2D 2.0.

Keep reading to check out the video of what you’ll make and to get started learning some cool new techniques!

Game Demo

Here’s a demo video showing you what you’ll make in this tutorial series:

As I mentioned, you’ll see that the fruit cutting effect is really dynamic. The fruit is cut dynamically based on where you slice, and since you can slice objects multiple times, you can really chop things up!

You can see that you’ll also implement a cool slicing trail effect, some particle systems, gameplay logic, and sounds to spice things up.

There’s a lot to cover – so let’s get started!

Getting Started: Project Setup

You’re going to use Cocos2D 2.X in this project, so go ahead and download it if you don’t have it already. Note that you are free to use Cocos2D 1.X instead of 2.X. With 1.X, you can skip the parts about converting PRKit and CCBlade to Cocos2D 2.X, but be sure to pay attention to the other small changes you make to those classes.

After downloading, double click the tar to unarchive it, then install the templates with the following commands in the Terminal:

cd ~/Downloads/cocos2d-iphone-2.0-beta
./install-templates.sh -f -u

Start up Xcode and create a new project with the iOS\cocos2d v2.x\cocos2d iOS with Box2d template and name it CutCutCut.

Your new project should look something like this:

Project Start

First things first – you should clean up the template a bit to get to a good starting point.

Open HelloWorldLayer.h and remove the following line:

CCTexture2D *spriteTexture_;// weak ref

Switch to HelloWorldLayer.mm and make the following changes

// Remove this line from the top
#import "PhysicsSprite.h"

// Replace the init method with this
-(id) init
{
    if( (self=[super init])) {
        // enable events
        self.isTouchEnabled = YES;
        self.isAccelerometerEnabled = YES;
        CGSize s = [CCDirector sharedDirector].winSize;
        
        // init physics
        [self initPhysics];
        
        [self scheduleUpdate];
    }
    return self;
}

// Remove these two methods
-(void) createMenu {
    //all content
}
-(void) addNewSpriteAtPosition:(CGPoint)p methods {
    //all content
}

// Remove this line from ccTouchesEnded
[self addNewSpriteAtPosition: location];

At this point, you’ve removed all references to PhysicsSprite from HelloWorldLayer, but don’t remove the files from the project quite yet. Later on, you’ll need to copy a method that PhysicsSprite.mm contains somewhere else, so leave it put for now.

Hit Command+R to compile and run your project, and you should see a blank screen with a green border around it:

Clean Slate

The remaining template code has set up Box2D debug drawing, which draws borders around the Box2D bodies on the screen. See the thin green lines drawn around the screen? Those are the walls generated by the default initPhysics method that came with the template.

Take a look at the remaining template code to make sure you understand what’s going on so far – it initializes a Box2D world, sets up the ground (green borders), sets up debug drawing, etc. This is a pretty good “almost blank” starting point with Box2D we can build on from here.

Resource Kit

Next download the resources for this project and unzip the file.

Don’t add everything into the project just yet; some of the files are actually optional. Keep the folder handy though – as you go through the tutorial, from time to time I will ask you to add some of these files into the project.

Here’s what you will find inside:

  • A background image and a bunch of fruit art made by Vicki, and other miscellaneous images in the Images folder
  • The background sound mix made using gomix.it in the Sounds folder
  • Sound effects made using bfxr or downloaded from freesound in the Sounds folder
  • All the particle systems created with Particle Designer in the Particles folder
  • A PLIST file generated by PhysicsEditor containing vertex information for the Fruits & Bomb classes in the Misc folder
  • Fruits & Bomb classes in the Classes folder
  • The versions of PRKit and CCBlade you will use in the tutorial in the Classes folder
  • An attribution list for resources that are under the Attribution License in the Misc folder

Drawing Textured Polygons with PRKit

Our goal is to cut sprites into multiple pieces. A typical CCSprite contains a texture, and a bounding box no matter what shape the image is. This is not suitable for our game since knowing the actual shapes within the images is a crucial step to creating sprites that can be cut, sliced, and split.

You need to create Textured Polygons, which:

  • Create a correspondence between a polygon/shape and an image (Texture Mapping)
  • Show only the parts of the image that are within the bounds of the polygon (Texture Filling)

Neither Cocos2D nor Box2D comes with a built-in class that handles the custom features you want, and normally, this would take some triangulation coupled with custom OpenGL drawing code.

Sounds hard right?

Fortunately, all the complicated calculations and drawing code needed to achieve this have already been written by the good folks at Precognitive Research. They created an addition to the Cocos2D library named PRKit, which handles Texture Mapping and Filling.

To get started on Textured Polygons, download PRKit, extract it, and drag the PRKit folder into to your project. Make sure that “Copy items into destination group’s folder” is checked and “Create groups for any added folders” is selected.

Note that PRKit is maintained by Precognitive Research, so it may get updated in time. To avoid confusion, our resource kit also contains the exact version of PRKit I used in making this tutorial.

Your project should now include these files:

PRKit Yey!

Compile and run, and you will encounter a few errors:

PRKit Needs to be Converted

The errors pop up because PRKit was made for Cocos2D 1.X, which uses OpenGL ES 1.1, whereas you are using Cocos2D 2.X, which uses OpenGL ES 2.0, and there are significant differences between the two.

To fix this, open PRFilledPolygon.m and make these changes:

// Add inside the initWithPoints: andTexture: usingTriangulator: method
self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];

// Replace the calculateTextureCoordinates method with this
-(void) calculateTextureCoordinates {
    for (int j = 0; j < areaTrianglePointCount; j++) {
        textureCoordinates[j] = ccpMult(areaTrianglePoints[j],1.0f/texture.pixelsWide*CC_CONTENT_SCALE_FACTOR());
        textureCoordinates[j].y = 1 - textureCoordinates[j].y;
    }
}

// Replace the draw method with this
-(void) draw{
    CC_NODE_DRAW_SETUP();
    
    ccGLBindTexture2D( self.texture.name );
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    
    ccGLBlendFunc( blendFunc.src, blendFunc.dst);
    
    ccGLEnableVertexAttribs( kCCVertexAttribFlag_Position | kCCVertexAttribFlag_TexCoords );
    
    glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, areaTrianglePoints);
    glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, textureCoordinates);
    
    glDrawArrays(GL_TRIANGLES, 0, areaTrianglePointCount);
}

Let's go over these changes bit by bit.

First, in Cocos2D every CCNode has an OpenGL ES 2.0 shader program attached to it. To draw the PRFilledPolygon, you need to sue the built-in "Position/Texture" shader, which you assign in the init method.

Next, you need to set up the correct texture coordinates for each point in the polygon. To do this, you have to make two changes the calculateTextureCoordinates method:

  • Scale: Since this class does its own calculations on the coordinates of its texture, it does not automatically handle the retina display. To fix this, you just multiply texture.pixelsWide with CC_CONTENT_SCALE_FACTOR - a convenient multiplier value provided by Cocos2D for converting values between regular and retina equivalents.
  • Flip Y: For some reason, PRFIlledPolygon draws textures upside down, so you simply flip the y value here.

Last, the drawing code is updated to OpenGL ES 2.0, in a similar fashion to how CCSprite drawing changed from Cocos2D 1.X to Cocos2D 2.X:

  • Start by calling CC_NODE_DRAW_SETUP() to prepare the node for drawing.
  • Calls to glDisableClientState() and glEnableClientState() are obsolete and are discarded.
  • The glVertexPointer() and glTexCoordPointer() commands are both replaced by glVertexAttribPointer(), which now accepts Vertex Position or Texture Coordinate as its first option.
  • The setup of glTexEnvf(), which was responsible for repeating the sprite in case the polygon was bigger than the texture, is replaced by calls to glTexParameteri().

If you're confused by any of this, you might want to check out our OpenGL ES 2.0 for iPhone and Custom Cocos2D 2.X Shaders tutorials for more background information. But you don't need to worry about it too much, because at this point all we're doing is porting the class to work in Cocos2D 2.X :]

Compile and run, and all PRKit errors should disappear!

It's time to put PRKit in action. You will be sub-classing PRKit's PRFilledPolygon class to make a base PolygonSprite class that will draw our fruits.

The PolygonSprite builds on PRFilledPolygon by attaching a Box2D body to the sprite, and it will also contain other custom variables and methods for the fruits in our game implementation.

Let's get to it. Hit Command+N and create a new file with the iOS\cocos2d v2.x\CCNode Class template. Make it a subclass of PRFilledPolygon and name it PolygonSprite.m.

Switch over to PolygonSprite.h and make the following changes:

// Add to top of file
#import "Box2D.h"
#import "PRFilledPolygon.h"
#define PTM_RATIO 32

// Add inside @interface
b2Body *_body;
BOOL _original;
b2Vec2 _centroid;

// Add after the @interface
@property(nonatomic,assign)b2Body *body;
@property(nonatomic,readwrite)BOOL original;
@property(nonatomic,readwrite)b2Vec2 centroid;

// Add before the @end
-(id)initWithTexture:(CCTexture2D*)texture body:(b2Body*)body original:(BOOL)original;
-(id)initWithFile:(NSString*)filename body:(b2Body*)body original:(BOOL)original;
+(id)spriteWithFile:(NSString*)filename body:(b2Body*)body original:(BOOL)original;
+(id)spriteWithTexture:(CCTexture2D*)texture body:(b2Body*)body original:(BOOL)original;
-(id)initWithWorld:(b2World*)world;
+(id)spriteWithWorld:(b2World*)world;
-(b2Body*)createBodyForWorld:(b2World*)world position:(b2Vec2)position rotation:(float)rotation vertices:(b2Vec2*)vertices vertexCount:(int32)count density:(float)density friction:(float)friction restitution:(float)restitution;
-(void)activateCollisions;
-(void)deactivateCollisions;

The above code declares the base variables and methods you need to create a PolygonSprite. These are:

  • body: This is the Box2D body that is attached to our sprite. It is needed for physics simulation.
  • original: Complete and sliced sprites will use the same PolygonSprite class, as such, differentiating between the two will be important. If this is YES, it means that it is the uncut, or original object that you created, otherwise, it is just a piece of the whole.
  • centroid: The center of the polygon within the image won't always be the same as the center of the image, so it is useful to store this value.
  • properties: Expose all the variables using properties so that other classes can access them freely.
  • init/spriteWith*: Our main init methods following the same naming convention as Cocos2D.
  • other methods: These are methods creating & dealing with the attached Box2D body and its properties.
  • PTM_RATIO: Pixels to Meters ratio. Box2D needs this conversion value because it deals with Meters instead of Pixels.

Quickly switch to PolygonSprite.m and rename it PolygonSprite.mm. All classes that mix Objective-C (Cocos2D) and C++ (Box2D) code need to have a ".mm" extension to notify the compiler of the mixed syntax.

Next, make the following changes to PolygonSprite.mm:

// Add inside the @implementation
@synthesize body = _body;
@synthesize original = _original;
@synthesize centroid = _centroid;

+(id)spriteWithFile:(NSString *)filename body:(b2Body *)body  original:(BOOL)original
{
    return [[[self alloc]initWithFile:filename body:body original:original] autorelease];
}

+(id)spriteWithTexture:(CCTexture2D *)texture body:(b2Body *)body  original:(BOOL)original
{
    return [[[self alloc]initWithTexture:texture body:body original:original] autorelease];
}

+(id)spriteWithWorld:(b2World *)world
{
    return [[[self alloc]initWithWorld:world] autorelease];
}

-(id)initWithFile:(NSString*)filename body:(b2Body*)body  original:(BOOL)original
{
    NSAssert(filename != nil, @"Invalid filename for sprite");
    CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage: filename];
    return [self initWithTexture:texture body:body original:original];
}

-(id)initWithTexture:(CCTexture2D*)texture body:(b2Body*)body original:(BOOL)original
{
    // gather all the vertices from our Box2D shape
    b2Fixture *originalFixture = body->GetFixtureList();
    b2PolygonShape *shape = (b2PolygonShape*)originalFixture->GetShape();
    int vertexCount = shape->GetVertexCount();
    NSMutableArray *points = [NSMutableArray arrayWithCapacity:vertexCount];
    for(int i = 0; i < vertexCount; i++) {
        CGPoint p = ccp(shape->GetVertex(i).x * PTM_RATIO, shape->GetVertex(i).y * PTM_RATIO);
        [points addObject:[NSValue valueWithCGPoint:p]];
    }
    
    if ((self = [super initWithPoints:points andTexture:texture]))
    {
        _body = body;
        _body->SetUserData(self);
        _original = original;
        // gets the center of the polygon
        _centroid = self.body->GetLocalCenter();
        // assign an anchor point based on the center
        self.anchorPoint = ccp(_centroid.x * PTM_RATIO / texture.contentSize.width, 
                               _centroid.y * PTM_RATIO / texture.contentSize.height);
        // more init stuff here later when you expand PolygonSprite
    }
    return self;
}

-(id)initWithWorld:(b2World *)world
{
    //nothing to do here
    return nil;
}

Similar to Cocos2D, all the spriteWith* methods are just autorelease counterparts of the initWith* methods, while initWithWorld has no actual use for this class yet, but instead it will be used by PolygonSprite's subclasses later.

The bulk of the changes can be found in the initWithFile and initWithTexture methods. To get the flow of things, creating a fruit will be called in this sequence:

Init Sequence

  • initWithWorld: This is intended for subclasses of PolygonSprite so you do nothing except return nil, and deal with it later.
  • initWithFile: This adds the texture from our file and passes everything to initWithTexture.
  • initWithTexture: Our main initialization. PRFilledPolygon needs a texture and all the vertices of the polygon it fills. Since the previous step already handled the texture part, this step handles the vertices by collecting them from the sprite's Box2D body. After passing them to PRFilledPolygon, it proceeds to initialize the variables that you have previously declared.
  • initWithPoints: Everything this does is contained within PRKit and the good thing is that you don't really need to touch PRKit anymore now that you have updated its code.

Still inside PolygonSprite.mm, add the following methods:

-(void)setPosition:(CGPoint)position
{
    [super setPosition:position];
    _body->SetTransform(b2Vec2(position.x/PTM_RATIO,position.y/PTM_RATIO), _body->GetAngle());
}

-(b2Body*)createBodyForWorld:(b2World *)world position:(b2Vec2)position rotation:(float)rotation vertices:(b2Vec2*)vertices vertexCount:(int32)count density:(float)density friction:(float)friction restitution:(float)restitution
{
    b2BodyDef bodyDef;
    bodyDef.type = b2_dynamicBody;
    bodyDef.position = position;
    bodyDef.angle = rotation;
    b2Body *body = world->CreateBody(&bodyDef);
    
    b2FixtureDef fixtureDef;
    fixtureDef.density = density;
    fixtureDef.friction = friction;
    fixtureDef.restitution = restitution;
    fixtureDef.filter.categoryBits = 0;
    fixtureDef.filter.maskBits = 0;
    
    b2PolygonShape shape;
    shape.Set(vertices, count);
    fixtureDef.shape = &shape;
    body->CreateFixture(&fixtureDef);
    
    return body;
}

-(void)activateCollisions
{
    b2Fixture *fixture = _body->GetFixtureList();
    b2Filter filter = fixture->GetFilterData();
    filter.categoryBits = 0x0001;
    filter.maskBits = 0x0001;
    fixture->SetFilterData(filter);
}

-(void)deactivateCollisions
{
    b2Fixture *fixture = _body->GetFixtureList();
    b2Filter filter = fixture->GetFilterData();
    filter.categoryBits = 0;
    filter.maskBits = 0;
    fixture->SetFilterData(filter);
}

In the above code, you first overload the setPosition method of CCNode so that when you update the sprite's position, the associated Box2D body's position gets updated as well.

You then make a convenience method for creating and defining a Box2D body. To create a body, you need to define a body definition, a body object, a shape, and a fixture definition. No real hard values are being assigned here yet since this method will be used by subclasses of PolygonSprite later on.

The only thing to note is the categoryBits and maskBits. These two are used for filtering collisions between objects such that if a category bit of an object matches a mask bit of another object and vice versa, there will be a collision between those two objects. You set these to 0 first because you don't want any collisions happening when the objects are first initialized.

Lastly, you define two methods that simply replace the categoryBits and maskBits so that you can activate and deactivate the collisions of our PolygonSprites.

There is one more thing to add to PolygonSprite.mm:

-(CGAffineTransform) nodeToParentTransform
{
    b2Vec2 pos  = _body->GetPosition();
    
    float x = pos.x * PTM_RATIO;
    float y = pos.y * PTM_RATIO;
    
    if ( !isRelativeAnchorPoint_ ) {
        x += anchorPointInPoints_.x;
        y += anchorPointInPoints_.y;
    }
    
    // Make matrix
    float radians = _body->GetAngle();
    float c = cosf(radians);
    float s = sinf(radians);
    
    if( ! CGPointEqualToPoint(anchorPointInPoints_, CGPointZero) ){
        x += c*-anchorPointInPoints_.x+ -s*-anchorPointInPoints_.y;
        y += s*-anchorPointInPoints_.x+ c*-anchorPointInPoints_.y;
    }
    
    // Rot, Translate Matrix
    transform_ = CGAffineTransformMake( c,  s,
                                       -s,c,
                                       x,y );
    
    return transform_;
}

Remember when I mentioned that you needed something from PhysicsSprite? Well, this is it. All this does is to make sure that our Box2D shape and our sprite are in the same position when moving. It's prepared for us by Cocos2D and that makes it all the more awesome.

After copying the above code, you can now delete both PhysicsSprite.h and PhysicsSprite.mm from the project as you have completely eliminated their usefulness.

Compile and run, and everything should be fine. You are done with PolygonSprite for now.

Allen Tan

Contributors

Allen Tan

Author

Over 300 content creators. Join our team.