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

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. This is the second part of a tutorial series that shows you how to make a sprite cutting game similar to the game Fruit Ninja by Halfbrick […] 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.

This is the second 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 learned how to create a Textured Polygon, and made a Watermelon out of it.

All the preparation you did in the first part will pay off in this second part of the series, where you’ll finally be able to cut our sprites.

As with the first part, this tutorial assumes that you are no stranger to Cocos2D and Box2D. If you are new to Cocos2D or Box2D, please check out the intro to Cocos2D and intro to Box2D tutorials on this site first.

Getting Started

If you don’t have it already, download a copy of the sample project where you left it off in the previous tutorial.

Next, you need to make modifications to PolygonSprite’s structure so that it can handle being cut.

Open PolygonSprite.h and make the following changes:

// Add inside the @interface
BOOL _sliceEntered;
BOOL _sliceExited;
b2Vec2 _entryPoint;
b2Vec2 _exitPoint;
double _sliceEntryTime;

// Add after the @interface
@property(nonatomic,readwrite)BOOL sliceEntered;
@property(nonatomic,readwrite)BOOL sliceExited;
@property(nonatomic,readwrite)b2Vec2 entryPoint;
@property(nonatomic,readwrite)b2Vec2 exitPoint;
@property(nonatomic,readwrite)double sliceEntryTime;

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

// Add inside the @implementation
@synthesize entryPoint = _entryPoint;
@synthesize exitPoint = _exitPoint;
@synthesize sliceEntered = _sliceEntered;
@synthesize sliceExited = _sliceExited;
@synthesize sliceEntryTime = _sliceEntryTime;

// Add inside the initWithTexture method, inside the if statement
_sliceExited = NO;
_sliceEntered = NO;
_entryPoint.SetZero();
_exitPoint.SetZero();
_sliceExited = 0;

Compile and check for any syntax errors.

The above code retrofits the PolygonSprite class and its subclasses with variables that store cut/slice information:

  • entryPoint: The point where the slice first intersects the polygon.
  • exitPoint: The point where the slice intersects the polygon for the second time.
  • sliceEntered: Determines if the polygon has been intersected.
  • sliceExited: Determines if there has been 1 complete slice for this polygon.
  • sliceEntryTime: The exact time the slice entered the polygon. Used to eliminate swipes that are too slow to be considered as cuts.

Intersecting Sprites Using Ray Casts

In order to cut sprites, you must be able to determine where it was cut. This is where Box2D ray casting comes in.

In ray casting, you specify a start point and an end point, and Box2D will trace along the line from start to end and tell you all the Box2D fixtures it collides with. Not only that, it can also perform a callback method telling each fixture what to do.

You’re going to use ray casts based on the player’s touch input to determine all the fixtures that the player’s touch passes by, and use the callback method to record the intersection points.

Open HelloWorldLayer.h and add the following inside the @interface:

CGPoint _startPoint;
CGPoint _endPoint;

Switch to HelloWorldLayer.mm and make the following changes:

// Add inside the draw method after kmGLPushMatrix()
ccDrawLine(_startPoint, _endPoint);

// Add this method
-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch *touch in touches){
        CGPoint location = [touch locationInView:[touch view]];
        location = [[CCDirector sharedDirector] convertToGL:location];
        _startPoint = location;
        _endPoint = location;
    }
}

// Add this method
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch *touch in touches){
        CGPoint location = [touch locationInView:[touch view]];
        location = [[CCDirector sharedDirector] convertToGL:location];
        _endPoint = location;
    }
}

The above code specifies a starting and ending point for the touch.

The start point is stored when the player touches the screen in ccTouchesBegan, and the end point moves while the player moves the touch in ccTouchesMoved.

The ccDrawLine method draws a line from the start point to the end point.

Compile and run, and draw a line on the screen:

Draw the Line

This line will represent the ray cast that you will create next.

To use Box2D ray casting, you call a simple function on the world called RayCast, and provide it with the start and end points, along with a callback function that runs for each fixture the ray intersects.

The ray cast function needs to be stored in a b2RayCastCallback class.

In Xcode, go to File\New\New File, choose iOS\C and C++\Header File, and click Next. Name the new header RayCastCallback.h, and click Save.

Replace the file with the following:

#ifndef CutCutCut_RaycastCallback_h
#define CutCutCut_RaycastCallback_h

#import "Box2D.h"
#import "PolygonSprite.h"

class RaycastCallback : public b2RayCastCallback
{
public:
RaycastCallback(){
}

float32 ReportFixture(b2Fixture *fixture,const b2Vec2 &point,const b2Vec2 &normal,float32 fraction)
{
    PolygonSprite *ps = (PolygonSprite*)fixture->GetBody()->GetUserData();
    if (!ps.sliceEntered)
    {
        ps.sliceEntered = YES;
        
        //you need to get the point coordinates within the shape
        ps.entryPoint  = ps.body->GetLocalPoint(point);
        
        ps.sliceEntryTime = CACurrentMediaTime() + 1;
        CCLOG(@"Slice Entered at world coordinates:(%f,%f), polygon coordinates:(%f,%f)", point.x*PTM_RATIO, point.y*PTM_RATIO, ps.entryPoint.x*PTM_RATIO, ps.entryPoint.y*PTM_RATIO);
    }
    else if (!ps.sliceExited)
    {
        ps.exitPoint = ps.body->GetLocalPoint(point);
        ps.sliceExited = YES;
        
        CCLOG(@"Slice Exited at world coordinates:(%f,%f), polygon coordinates:(%f,%f)", point.x*PTM_RATIO, point.y*PTM_RATIO, ps.exitPoint.x*PTM_RATIO, ps.exitPoint.y*PTM_RATIO);
    }
    return 1;
}
};

#endif

ReportFixture is the method that gets called whenever Box2D detects an intersection. You set the intersection point as the entry point if the polygon has not been intersected yet, and set it to the exit point if the polygon has already been intersected.

You convert the points using GetLocalPoint because you need to know the coordinate within the polygon’s vertices, and not the coordinate within the world. World Coordinates start from the lower left corner of the screen, while Local Coordinates start from the lower left corner of the shape.

Lastly, you return 1 to tell Box2D that this ray cast should continue to check for fixtures even after the first fixture. Returning other numbers will make the ray cast behave differently, but this is beyond the scope of this tutorial.

Switch to HelloWorldLayer.h and make the following changes:

// Add to top of file
#import "RaycastCallback.h"

// Add inside the @interface
RaycastCallback *_raycastCallback;

Next, switch to HelloWorldLayer.mm and make these changes:

// Add inside the init method, right after [self initSprites]
_raycastCallback = new RaycastCallback();

// Add at the end of the ccTouchesEnded method
world->RayCast(_raycastCallback, 
               b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO),
               b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO));

world->RayCast(_raycastCallback, 
               b2Vec2(_endPoint.x / PTM_RATIO, _endPoint.y / PTM_RATIO),
               b2Vec2(_startPoint.x / PTM_RATIO, _startPoint.y / PTM_RATIO));

You declare a RayCastCallback class and pass it as a parameter to the RayCast method. For now, you only call the RayCast when the player’s touch ends.

You do the ray cast twice because Box2D ray casting only collects fixtures in one direction and will only intersect each fixture once. To get around this, you cast one ray from the start point to the end point and one from the end point to the start point.

Compile and run. Draw a line and check the logs.

Check the Logs

Allen Tan

Contributors

Allen Tan

Author

Over 300 content creators. Join our team.