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

Allen Tan

This post is also available in: Chinese (Simplified)

Create a Sprite-Cutting Game with Cocos2D!

Create a Sprite-Cutting Game with Cocos2D!

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.

Welcome to the third part of a tutorial series that shows you how to make a sprite cutting game similar to the game Fruit Ninja by Halfbrick Studios.

In the first part, you covered how to make a Textured Polygon, and made a Watermelon out of it.

In the second part , you showed you how to use Box2D Ray Casting and some math to split the Textured Polygons.

In this final part, you will make this project look like a full-fledged game by adding gameplay, visual effects, and sound effects.

Again, if you are new to Cocos2D or Box2D, please check out the intro to Cocos2D and intro to Box2D on this site first.

This project starts where you left off in the last tutorial, so make sure you have a copy of the project from part 2. Also grab a copy of the resources for this tutorial if you haven’t done so yet. You will be adding some cool stuff from the kit to the project later on.

Tossing Fruits

So far you have only been drawing one static fruit on the scene. Before you can add the tossing mechanic, you must have different types of fruits. If you haven’t prepared all the fruit classes back in the first tutorial, you can use the copies in the Classes folder from the resources.

At this point you should have the following fruit classes in your project: Banana, Grapes, Pineapple, Strawberry, and Watermelon.

Switch to PolygonSprite.h and make the following changes:

// Add to top of file
typedef enum _State
{
kStateIdle = 0,
kStateTossed
} State;
 
typedef enum _Type
{
kTypeWatermelon = 0,
kTypeStrawberry,
kTypePineapple,
kTypeGrapes,
kTypeBanana,
kTypeBomb
} Type;
 
// Add inside @interface
State _state;
Type _type;
 
// Add after @interface
@property(nonatomic,readwrite)State state;
@property(nonatomic,readwrite)Type type;

Next, switch to PolygonSprite.mm and make the following changes:

// Add inside @implementation
@synthesize state = _state;
@synthesize type = _type;
 
// Add inside the if statement of initWithTexture
_state = kStateIdle;
 
// Add inside createBodyForWorld, right after setting the maskBits of the fixture definition
fixtureDef.isSensor = YES;

You added a type definition to PolygonSprite so that the game has a way to distinguish between the subclasses. Next, you create a state for each fruit. An idle state means the fruit can be tossed, while the tossed state means the fruit is still onscreen.

You also made the bodies of the PolygonSprites sensors, which means Box2D will not simulate but only “sense” collisions for these bodies. When you first toss sprites from the bottom, you don’t want them to suddenly collide with falling sprites. The player might lose without even seeing these sprites.

Next, make the following changes:

// Add inside the if statement of Banana.mm
self.type = kTypeBanana;
// Add inside the if statement of Bomb.mm
self.type = kTypeBomb;
// Add inside the if statement of Grapes.mm
self.type = kTypeGrapes;
// Add inside the if statement of Pineapple.mm
self.type = kTypePineapple;
// Add inside the if statement of Strawberry.mm
self.type = kTypeStrawberry;
// Add inside the if statement of Watermelon.mm
self.type = kTypeWatermelon;

Switch back to HelloWorldLayer.mm, and make these changes:

// Add to top of file
#import "Strawberry.h"
#import "Pineapple.h"
#import "Grapes.h"
#import "Banana.h"
#import "Bomb.h"
 
// Replace the initSprites method
-(void)initSprites
{
    _cache = [[CCArray alloc] initWithCapacity:53];
 
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Watermelon alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Strawberry alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Pineapple alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Grapes alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
    for (int i = 0; i < 10; i++)
    {
        PolygonSprite *sprite = [[Banana alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
 
    for (int i = 0; i < 3; i++)
    {
        PolygonSprite *sprite = [[Bomb alloc] initWithWorld:world];
        sprite.position = ccp(-64*(i+1),-64);
        [self addChild:sprite z:1];
        [_cache addObject:sprite];
    }
}

You added a type value for all the PolygonSprite subclasses, and created 10 of each fruit, and 3 bombs to the game. You don’t want them showing up yet, so you cast them offscreen for now.

Compile and run, and no fruit should be visible yet.

Prepare to Toss

In our game, the fruits are going to be tossed from below the screen. They can be tossed all at the same time (simultaneously), or one by one (consecutively), with some randomness to the interval between tosses, the number of fruits, their position, toss height, and direction.

Having this much randomness in the game will make it more interesting.

Switch back to HelloWorldLayer.h and make the following changes:

// Add to top of file, below the calculate_determinant definition
#define frandom (float)arc4random()/UINT64_C(0x100000000)
#define frandom_range(low,high) ((high-low)*frandom)+low
#define random_range(low,high) (arc4random()%(high-low+1))+low
 
typedef enum _TossType
{
kTossConsecutive = 0,
kTossSimultaneous
}TossType;
 
// Add inside the @interface
double _nextTossTime;
double _tossInterval;
int _queuedForToss;
TossType _currentTossType;

Next, switch to HelloWorldLayer.mm and make the following changes:

// Add inside the init method
_nextTossTime = CACurrentMediaTime() + 1;
_queuedForToss = 0;

You defined functions that output random floats and integers given a range, and also created a type definition for the two kinds of tosses mentioned above.

Next, you defined the following variables for the game logic:

  • nextTossTime: This is the time when a fruit, or a group of fruits will be tossed next. It is always compared against CACurrentMediaTime(), which is the current game time. You initialize it with 1 second more than the current time so that the first toss doesn’t happen immediately after the game starts.
  • tossInterval: This is the random number of seconds in between tosses. You will be adding this value to nextTossTime after every toss.
  • queuedForToss: This is the random number of fruits that still need to be tossed for the current toss type.
  • currentTossType: The toss type of the current toss interval. It is a random choice between simultaneous and consecutive.

Still in HelloWorldLayer.mm, add this method:

-(void)tossSprite:(PolygonSprite*)sprite
{
    CGSize screen = [[CCDirector sharedDirector] winSize];
    CGPoint randomPosition = ccp(frandom_range(100, screen.width-164), -64);
    float randomAngularVelocity = frandom_range(-1, 1);
 
    float xModifier = 50*(randomPosition.x - 100)/(screen.width - 264);
    float min = -25.0 - xModifier;
    float max = 75.0 - xModifier;
 
    float randomXVelocity = frandom_range(min,max);
    float randomYVelocity = frandom_range(250, 300);
 
    sprite.state = kStateTossed;
    sprite.position = randomPosition;
    [sprite activateCollisions];
    sprite.body->SetLinearVelocity(b2Vec2(randomXVelocity/PTM_RATIO,randomYVelocity/PTM_RATIO));
    sprite.body->SetAngularVelocity(randomAngularVelocity);
}

This method assigns a random position to a sprite below the screen, and computes for a random velocity. The min and max values limit the velocity based on the sprites’ position so that sprites don’t get tossed too far left, or too far right.

The values here mostly come out of trial and error. If the sprite is at the leftmost position, an x-velocity of -25 to 75 is enough such that the sprite still lands within the screen bounds. If it’s at the middle, then it’s -50 to 50, and so on.

Planned Trajectories

After computing all the randomness, it informs the system that the sprite has been tossed, activates the sprite’s collision mask, and sets their initial velocity.

I mentioned before that you don’t want sprites that are tossed to collide with falling sprites, so you must be wondering why we call activateCollisions here. This method only sets the category and mask bits of the sprites, but it doesn’t change the fact that these sprites are still sensors.

It’s important to change these bits because when the sprites are split, the new shapes aren’t sensors anymore, and they will inherit these properties as well.

This method already sets the random position and velocity per fruit, so the next logical step is to create the mechanic that tosses fruits at random intervals.

Add this method to HelloWorldLayer.mm:

-(void)spriteLoop
{
    double curTime = CACurrentMediaTime();
 
    //step 1
    if (curTime > _nextTossTime)
    {
        PolygonSprite *sprite;
 
        int random = random_range(0, 4);
        //step 2
        Type type = (Type)random;
        if (_currentTossType == kTossConsecutive && _queuedForToss > 0)
        {
            CCARRAY_FOREACH(_cache, sprite)
            {
                if (sprite.state == kStateIdle && sprite.type == type)
                {
                    [self tossSprite:sprite];
                    _queuedForToss--;
                    break;
                }
            }
        }
        else
        { //step 3
            _queuedForToss = random_range(3, 8);
            int tossType = random_range(0,1);
 
            _currentTossType = (TossType)tossType;
            //step 4
            if (_currentTossType == kTossSimultaneous)
            {
                CCARRAY_FOREACH(_cache, sprite)
                {
                    if (sprite.state == kStateIdle && sprite.type == type)
                    {
                        [self tossSprite:sprite];
                        _queuedForToss--;
                        random = random_range(0, 4);
                        type = (Type)random;
 
                        if (_queuedForToss == 0)
                        {
                            break;
                        }
                    }
                }
            } //step 5
            else if (_currentTossType == kTossConsecutive)
            {
                CCARRAY_FOREACH(_cache, sprite)
                {
                    if (sprite.state == kStateIdle && sprite.type == type)
                    {
                        [self tossSprite:sprite];
                        _queuedForToss--;
                        break;
                    }
                }
            }
        }
        //step 6
        if (_queuedForToss == 0)
        {
            _tossInterval = frandom_range(2,3);
            _nextTossTime = curTime + _tossInterval;
        }
        else 
        {
            _tossInterval = frandom_range(0.3,0.8);
            _nextTossTime = curTime + _tossInterval;
        }
    }
}

Quite a lot of things happening here, so let’s take them step by step:

  • Step 1: Checks if it’s time to toss fruits again by comparing the current time with the nextTossTime variable.
  • Step 2: If there are still fruits queued to be tossed in consecutive mode, it tosses one random fruit and goes straight to step 6.
  • Step 3: Chooses either consecutive or simultaneous tossing modes, and sets the number of fruits to be tossed.
  • Step 4: Tosses random fruits simultaneously. Note that the range of fruit types only goes from 0 to 4 because you don’t want to include the Bomb type.
  • Step 5: Similar to step 2. It tosses the first fruit right after consecutive mode is selected and goes straight to step 6.
  • Step 6: Sets the interval between toss times. Whenever a toss type runs out of fruit, you assign a longer interval, else, you assign short intervals because it means you are tossing fruits consecutively.

To run this method constantly, include it in the scheduled update. In HelloWorldLayer.mm, put this line inside the update method:

[self spriteLoop];

There’s one more thing to do before you launch the game. Since our sprites will be coming from, and eventually falling below the screen, you should remove the walls that were created by default. Still in HelloWorldLayer.mm, make these changes:

// In the initPhysics method, replace gravity.Set(0.0f, -10.0f) with
gravity.Set(0.0f, -4.25f);
 
// Comment out or remove the following code from the initPhysics method
// bottom
groundBox.Set(b2Vec2(0,0), b2Vec2(s.width/PTM_RATIO,0));
groundBody->CreateFixture(&groundBox,0);
 
// top
groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO));
groundBody->CreateFixture(&groundBox,0);
 
// left
groundBox.Set(b2Vec2(0,s.height/PTM_RATIO), b2Vec2(0,0));
groundBody->CreateFixture(&groundBox,0);
 
// right
groundBox.Set(b2Vec2(s.width/PTM_RATIO,s.height/PTM_RATIO), b2Vec2(s.width/PTM_RATIO,0));
groundBody->CreateFixture(&groundBox,0);

Aside from removing all the physical walls, you also make the gravity weaker because you don’t want our sprites to be falling too fast.

Compile and run, and you should now be seeing your fruits rising and falling!

Fruits Galore

While playing the game, you will notice 3 issues.

  • Eventually the fruit tossing will stop because you are not resetting the state of the original fruits and the cache runs out of things to toss.
  • The more you slice, the more performance gets permanently worse. This is because you don’t clean up the cut pieces when they fall offscreen and Box2D simulates all those pieces.
  • When you cut the fruits, the new pieces stick together: This is because you simply divide the fruits into two without forcibly splitting them.

To address these issues, make the following changes to HelloWorldLayer.mm:

// Add inside the splitPolygonSprite method, right before [sprite deactivateCollisions]
sprite.state = kStateIdle; 
 
// Add this method
-(void)cleanSprites
{
    PolygonSprite *sprite;
 
    //we check for all tossed sprites that have dropped offscreen and reset them
    CCARRAY_FOREACH(_cache, sprite)
    {
        if (sprite.state == kStateTossed)
        {
            CGPoint spritePosition = ccp(sprite.body->GetPosition().x*PTM_RATIO,sprite.body->GetPosition().y*PTM_RATIO);
            float yVelocity = sprite.body->GetLinearVelocity().y;
 
            //this means the sprite has dropped offscreen
            if (spritePosition.y < -64 && yVelocity < 0)
            {
                sprite.state = kStateIdle;
                sprite.sliceEntered = NO;
                sprite.sliceExited = NO;
                sprite.entryPoint.SetZero();
                sprite.exitPoint.SetZero();
                sprite.position = ccp(-64,-64);
                sprite.body->SetLinearVelocity(b2Vec2(0.0,0.0));
                sprite.body->SetAngularVelocity(0.0);
                [sprite deactivateCollisions];
            }
        }
    }
 
    //we check for all sliced pieces that have dropped offscreen and remove them
    CGSize screen = [[CCDirector sharedDirector] winSize];
    for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
    {
        if (b->GetUserData() != NULL) {
            PolygonSprite *sprite = (PolygonSprite*)b->GetUserData();
            CGPoint position = ccp(b->GetPosition().x*PTM_RATIO,b->GetPosition().y*PTM_RATIO);
            if (position.x < -64 || position.x > screen.width || position.y < -64)
            {
                if (!sprite.original)
                {
                    world->DestroyBody(sprite.body);
                    [self removeChild:sprite cleanup:YES];
                }
            }
        }
    }
}
 
// Add inside the update method, after [self checkAndSliceObjects]
[self cleanSprites];

Here you introduce state handling. The sprites start with the idle state, and the toss method changes this state. Since the toss method only chooses idle sprites, yo reset the state of original sprites that get sliced back to idle.

In the cleanSprites method, the first part of the code checks for all original sprites that dropped offscreen and resets everything back to their state before being tossed. The second part checks for all sliced pieces that are offscreen, destroys their Box2D body, and removes them from the scene.

Switch to HelloWorldLayer.h, and add this right below #define random_range(low,high):

#define midpoint(a,b) (float)(a+b)/2

Switch back to HelloWorldLayer.mm and make these changes to the splitPolygonSprite method:

// Add to the top part inside of the if (sprite1VerticesAcceptable && sprite2VerticesAcceptable) statement
b2Vec2 worldEntry = sprite.body->GetWorldPoint(sprite.entryPoint);
b2Vec2 worldExit = sprite.body->GetWorldPoint(sprite.exitPoint);
float angle = ccpToAngle(ccpSub(ccp(worldExit.x,worldExit.y), ccp(worldEntry.x,worldEntry.y)));
CGPoint vector1 = ccpForAngle(angle + 1.570796);
CGPoint vector2 = ccpForAngle(angle - 1.570796);
float midX = midpoint(worldEntry.x, worldExit.x);
float midY = midpoint(worldEntry.y, worldExit.y);
 
// Add after [self addChild:newSprite1 z:1]
newSprite1.body->ApplyLinearImpulse(b2Vec2(2*body1->GetMass()*vector1.x,2*body1->GetMass()*vector1.y), b2Vec2(midX,midY));
 
// Add after [self addChild:newSprite2 z:1]
newSprite2.body->ApplyLinearImpulse(b2Vec2(2*body2->GetMass()*vector2.x,2*body2->GetMass()*vector2.y), b2Vec2(midX,midY));

So that the two pieces don’t stick together when the polygon splits, you need to apply some kind of force that changes their direction and velocity.

To get the direction, you compute for the world coordinates and angle of the cutting line, and get two normalized vector angles perpendicular to the center of this line. All Box2D angle values are in radians so the number 1.570796 is just the radian form of 90 degrees.

Next, you get the coordinates of the center of the cutting line so that you know where the pushing force originates.

The diagram below shows our intention:

Push the Pieces Away from Each Other

To push the two pieces away, you apply a linear impulse originating from the center of the cutting line outward in both directions. The impulse is based on each body’s mass so that the push effect is more or less equal for both sprites. A bigger sprite needs a bigger impulse, while a smaller sprite needs a smaller impulse.

Compile and run, and this time the fruits should split properly, and the game can be played endlessly.

Slice with Impulse

Adding a Scoring System

The game won’t be much of a game without an objective and an end, so you need to put some sort of scoring system in place.

You will count score by the number of slices the player makes. You also give the player 3 lives, or chances if you will, which decrease whenever an uncut fruit goes out of the screen bounds. The game ends when the player loses 3 fruits.

Switch to HelloWorldLayer.h and make this change:

// Add inside @interface
int _cuts;
int _lives;
CCLabelTTF *_cutsLabel;

Switch back to HelloWorldLayer.mm and make these changes:

// Add inside the init method, right after [self initSprites]
[self initHUD];
 
// Add these methods
-(void)initHUD
{
    CGSize screen = [[CCDirector sharedDirector] winSize];
 
    _cuts = 0;
    _lives = 3;
 
    for (int i = 0; i < 3; i++)
    {
        CCSprite *cross = [CCSprite spriteWithFile:@"x_unfilled.png"];
        cross.position = ccp(screen.width - cross.contentSize.width/2 - i*cross.contentSize.width, screen.height - cross.contentSize.height/2);
        [self addChild:cross z:4];
    }
 
    CCSprite *cutsIcon = [CCSprite spriteWithFile:@"fruit_cut.png"];
    cutsIcon.position = ccp(cutsIcon.contentSize.width/2, screen.height - cutsIcon.contentSize.height/2);
    [self addChild:cutsIcon];
 
    _cutsLabel = [CCLabelTTF labelWithString:@"0" fontName:@"Helvetica Neue" fontSize:30];
    _cutsLabel.anchorPoint = ccp(0, 0.5);
    _cutsLabel.position = ccp(cutsIcon.position.x + cutsIcon.contentSize.width/2 +                _cutsLabel.contentSize.width/2,cutsIcon.position.y);
    [self addChild:_cutsLabel z:4];
}
 
-(void)restart
{
    [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];
}
 
-(void)endGame
{
    [self unscheduleUpdate];
    CCMenuItemLabel *label = [CCMenuItemLabel itemWithLabel:[CCLabelTTF labelWithString:@"RESTART"fontName:@"Helvetica Neue"fontSize:50] target:self selector:@selector(restart)];
    CCMenu *menu = [CCMenu menuWithItems:label, nil];
    CGSize screen = [[CCDirector sharedDirector] winSize];
    menu.position = ccp(screen.width/2, screen.height/2);
    [self addChild:menu z:4];
}
 
-(void)subtractLife
{
    CGSize screen = [[CCDirector sharedDirector] winSize];
    _lives--;
    CCSprite *lostLife = [CCSprite spriteWithFile:@"x_filled.png"];
    lostLife.position = ccp(screen.width - lostLife.contentSize.width/2 - _lives*lostLife.contentSize.width, screen.height - lostLife.contentSize.height/2);
    [self addChild:lostLife z:4];
 
    if (_lives <= 0)
    {
        [self endGame];
    }
}

In the interface, you set up variables to count the cuts and the lives. You also declare a label that shows the player’s current score.

The initHUD method creates 3 marker images on the upper-right corner of the screen to represent the player’s lives. It also puts an image representing the score, and the score value itself on the upper-left corner.

The subtractLife method replaces each marker image with a new marker image, representing a life lost, whenever it is called. It also checks if the player still has enough lives, if not, then the game should end.

The endGame method simply unschedules the game logic and creates a restart button on the screen. If this button is pressed, then the game is restarted.

The restart method just reloads the scene, going back to the very beginning of the game.

Now that you’ve built all these methods and variables, it’s time to add them to the game logic.

Still in HelloWorldLayer.mm, make the following changes:

// Add to the splitPolygonSprite method, inside the if (sprite1VerticesAcceptable && sprite2VerticesAcceptable) statement
_cuts++;
[_cutsLabel setString:[NSString stringWithFormat:@"%d",_cuts]];
 
// Add to the cleanSprites method, inside the if (spritePosition.y < -64 && yVelocity < 0) statement
if (sprite.type != kTypeBomb)
{
    [self subtractLife];
}

Whenever a polygon is successfully split, the score is incremented, and the label that shows the score is updated. If any original sprites fall to the bottom, then you subtract a life from the player.

Compile and run. The game is almost complete!

Game Over!

Making The Game More Challenging

To make things more interesting, you are going to add bombs to the game. You’ve already initialized 3 Bomb objects earlier, but you haven’t used them in any of the game mechanics.

Bombs are independent, so they should be tossed anytime regardless of the toss type. If a player accidentally slices a bomb, it will explode and take 1 life from the player.

Make these changes to HelloWorldLayer.mm:

// Add to the spriteLoop method, inside if (curTime > _nextTossTime), right after PolygonSprite *sprite;
int chance = arc4random()%8;
if (chance == 0)
{
    CCARRAY_FOREACH(_cache, sprite)
    {
        if (sprite.state == kStateIdle && sprite.type == kTypeBomb)
        {
            [self tossSprite:sprite];
            break;
        }
    }
}
 
// Add to the splitPolygonSprite method, inside the if (sprite.original) statement
if (sprite.type == kTypeBomb)
{
    [self subtractLife];
}
else
{
//placeholder
}

The code is pretty straightforward and similar to what you did before. The first part adds the tossing mechanism for the Bombs in a similar fashion to how the fruits were tossed, but this time without any checks for the toss type, and without counting how many bombs have been tossed.

Bombs are also tossed only by a random chance. By calling the modulo 8 operation on a random number, and requiring the result to be 0, there is only a 1/8 chance that a bomb will get tossed at every interval.

The second part adds a check to the sprite that was split in the splitPolygonSprite method. If the sprite happens to be a bomb, then the player loses a life by calling the subtractLife method you made earlier.

Compile and run. Bombs away!

Bombs Away!

More Life With Particle Effects

With the game mechanic complete, you can focus on polishing the game. You certainly need to add life to the game. For starters, cutting fruits look dull, bombs don’t explode, and the background just seems to be too static.

You can improve the scene using particle systems. Particle Systems allow you to efficiently create a large number of small objects using the same sprite. Cocos2D already comes with a customizable particle system, and it works great in conjunction with Particle Designer for visually setting them up.

Making particle systems in Particle Designer is easy; so easy in fact that you won’t be covering how to do that in this tutorial. Instead, I’ve already created some particle systems that you can use. Particle Designer exports particle systems in PLIST format. All you need to do is load the PLIST files into Cocos2D.

Grab the resources for this tutorial if you haven’t already, and in your Project Navigator panel, right-click on Resources and select “Add Files to CutCutCut”. Add the Particles folder from the resources to the project. While you’re at it, also add the Sounds folder to the project. Make sure that “Copy items into destination group’s folder” is checked and “Create groups for any added folders” is selected.

These are the particle systems that should have been added to your project:

  • banana_splurt.plist
  • blade_sparkle.plist
  • explosion.plist
  • grapes_splurt.plist
  • pineapple_splurt.plist
  • strawberry_splurt.plist
  • sun_pollen.plist
  • watermelon_splurt.plist

You have 5 particles that spray blobs, which you will call “splurt”, for when the various fruits are cut. An explosion particle for when the player slices a bomb. A sparkle effect that follows the blade, and a floating pollen effect for the background.

Switch to HelloWorldLayer.h and add the following inside the @interface:

CCParticleSystemQuad *_bladeSparkle;

Next, switch to HelloWorldLayer.mm again, and make the following changes:

// Add inside the init method
_bladeSparkle = [CCParticleSystemQuad particleWithFile:@"blade_sparkle.plist"];
[_bladeSparkle stopSystem];
[self addChild:_bladeSparkle z:3];
 
// Add inside the initBackground method
CCParticleSystemQuad *sunPollen = [CCParticleSystemQuad particleWithFile:@"sun_pollen.plist"];
[self addChild:sunPollen];
 
//Add inside ccTouchesBegan
_bladeSparkle.position = location;
[_bladeSparkle resetSystem];
 
//Add inside ccTouchesMoved
_bladeSparkle.position = location;
 
// Add inside ccTouchesEnded
[_bladeSparkle stopSystem];

You added the floating pollen particles to the background, and made the sparkle effect follow the user’s touch.

Calling stopSystem simply stops the particle system from emitting sprites, and calling resetSystem makes them emit the sprites again. Both of these particles are endless, meaning that they won’t stop emitting unless you call stopSystem.

For the splurt and explosion effects, make the following changes to PolygonSprite.h

// Add inside the @interface
CCParticleSystemQuad *_splurt;
 
// Add after the @interface
@property(nonatomic,assign)CCParticleSystemQuad *splurt;

Switch to PolygonSprite.mm, and add the following inside the @implementation:

@synthesize splurt = _splurt;

Next, make the following changes to the subclasses of PolygonSprite (the fruits and the bomb):

// Add inside Banana.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"banana_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Bomb.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"explosion.plist"];
[self.splurt stopSystem];
 
// Add inside Grapes.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"grapes_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Pineapple.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"pineapple_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Strawberry.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"strawberry_splurt.plist"];
[self.splurt stopSystem];
 
// Add inside Watermelon.mm init right after setting the type
self.splurt = [CCParticleSystemQuad particleWithFile:@"watermelon_splurt.plist"];
[self.splurt stopSystem];

You just added a particle system to the structure of PolygonSprites, and assigned a particle system for each type.

Switch back to HelloWorldLayer.mm and make these changes:

// Add this line per fruit and bomb in the initSprites method
[self addChild:sprite.splurt z:3];
 
// Add inside the splitPolygonSprite method, inside the if (sprite.original) statement
b2Vec2 convertedWorldEntry = b2Vec2(worldEntry.x*PTM_RATIO,worldEntry.y*PTM_RATIO);
b2Vec2 convertedWorldExit = b2Vec2(worldExit.x*PTM_RATIO,worldExit.y*PTM_RATIO);
float midX = midpoint(convertedWorldEntry.x, convertedWorldExit.x);
float midY = midpoint(convertedWorldEntry.y, convertedWorldExit.y);
sprite.splurt.position = ccp(midX,midY);
[sprite.splurt resetSystem];

You add all the particle systems to our game layer inside the initSprite method, and you make the particle effect appear in the middle of the cutting line when an original fruit or bomb is split.

Compile and run, and see particles fly when you slice objects!

Particles Everywhere

Gratuitous Music and Sound Effects

You know it wouldn’t be a raywenderlich.com game tutorial without fun music and sound effects! :]

Our sound effects will not only help set the mood, they will also help the player distinguish the different game events.

So go ahead and add the Sounds folder from the resources folder into your Xcode project. This contains sounds for the following events:

  • A bomb explodes
  • A bomb is tossed
  • Fruits are tossed consecutively
  • Fruits are tossed simultaneously
  • The player loses a life
  • The player cuts a fruit and blobs jump out
  • The player cuts a fruit repeatedly
  • The player swipes
  • Nature sounds in the background

Switch to HelloWorldLayer.h and make these changes:

// Add to top of file
#import "SimpleAudioEngine.h"
 
// Add inside the @interface
float _timeCurrent;
float _timePrevious;
CDSoundSource *_swoosh;
 
// Add after the @interface
@property(nonatomic,retain)CDSoundSource *swoosh;

Switch again to HelloWorldLayer.mm, and make the following changes:

// Add inside @implementation
@synthesize swoosh = _swoosh;
 
// Add inside the dealloc method, before [super dealloc]
[_swoosh release];
 
// Add inside the init method
[[SimpleAudioEngine sharedEngine] preloadEffect:@"swoosh.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"squash.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_consecutive.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_simultaneous.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"toss_bomb.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"lose_life.caf"];
_swoosh = [[[SimpleAudioEngine sharedEngine] soundSourceForFile:@"swoosh.caf"] retain];
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"nature_bgm.aifc"];
_timeCurrent = 0;
_timePrevious = 0;
 
// Add inside the update method
_timeCurrent += dt;
 
// Add inside the spriteLoop method, after tossing the bomb
[[SimpleAudioEngine sharedEngine] playEffect:@"toss_bomb.caf"];
 
// Add inside the spriteLoop method, for both the consecutive tosses
[[SimpleAudioEngine sharedEngine] playEffect:@"toss_consecutive.caf"];
 
// Add inside the spriteLoop method, for the simultaneous toss
[[SimpleAudioEngine sharedEngine] playEffect:@"toss_simultaneous.caf"];
 
// Add inside splitPolygon if sprite is a bomb
[[SimpleAudioEngine sharedEngine] playEffect:@"explosion.caf"];
 
// Add inside splitPolygon if sprite is not a bomb
[[SimpleAudioEngine sharedEngine] playEffect:@"squash.caf"];
 
// Add before destroying the body in the splitPolygonSprite method
[[SimpleAudioEngine sharedEngine] playEffect:@"smallcut.caf"];
 
// Add inside the subtractLife method
[[SimpleAudioEngine sharedEngine] playEffect:@"lose_life.caf"];
 
// Add inside ccTouchesMoved before setting _bladeSparkle.position = location
ccTime deltaTime = _timeCurrent - _timePrevious;
_timePrevious = _timeCurrent;
CGPoint oldPosition = _bladeSparkle.position;
 
// Add inside ccTouchesMoved after setting _bladeSparkle.position = location
if (ccpDistance(_bladeSparkle.position, oldPosition) / deltaTime > 1000)
{
    if (!_swoosh.isPlaying)
    {
        [_swoosh play];
    }
}

Aside from the usual sound playing code, you added a time factor to solve for the current velocity of the swipe based on a distance/time formula so that the swoosh sound effect only plays when the player swipes fast enough. You also maintain a reference to 1 swoosh sound effect, and only play it if it is not already playing.

You’re done! Congratulations, you’ve just made a complete fruit cutting game for the iPhone!

Where To Go From Here?

Here is the sample project with the completed project from the tutorial series.

From here on out, you can work on improving the game. Here are some ideas for some fun things you could do to make the game even better:

  • Support concave polygons by using triangulation (splitting a concave polygon into several convex polygons).
  • Support polygons with more than 8 vertices by following the same method.
  • Update PolygonSprite to support batch nodes.
  • Support multiple touches and swipes.
  • Support the iPad.
  • Cache even the split polygons so that everything can be reused. This is sure to boost performance.
  • Add slice streaks and give bonus points to cutting sprites in sequence.
  • Add special events when certain fruits are cut.
  • More randomness and variety of tosses, like tossing some fruits from the sides.

If you have any ideas on how to make the game cooler, or if you have any concerns, questions, and comments on the tutorial, please join the forum discussion below!


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.

Allen Tan

Allen Tan is an iOS Developer and Founder at White Widget, a Philippines-based start-up mobile apps & games development studio that does both contractual and indie work. Right now, Allen’s pretty much devoting all of his time getting the studio off the ground. To find out more, you can follow White Widget’s Facebook page & Twitter for announcements.

You can also check out Allen’s personal LinkedIn profile and Twitter, or reach him for work via email.

User Comments

20 Comments

[ 1 , 2 ]
  • the game has helped me a lot in cutting the sprites..
    nithyaeswaran
  • can i add more levels?
    rollstone
  • Thank you for the Tutorial that you made.

    I have a question. How can i 1 power up. Like Freeze power up like fruit ninja.

    Thanks! .
    kimkaye17
  • I need help please.
    I Don't know what is the problem.
    the tossed sprite rotates fast.
    how to reduce the rotation of the sprite?
    kimkaye17
  • I think it is a good idea to remake this tutorial using sprite kit. don't you think?
    JorgeB
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: Best iOS Animations in 2014. [Read Now]!

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in October: Xcode 6 Tips and Tricks!

Sign Up - October

Our Books

Our Team

Tutorial Team

  • Kyle Richter

... 49 total!

Update Team

  • Zouhair Mahieddine

... 15 total!

Editorial Team

  • Matt Galloway
  • Alexis Gallagher

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • David Hidalgo

... 32 total!

Subject Matter Experts

  • Richard Casey

... 4 total!