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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make A Game Like Fruit Ninja With Box2D and Cocos2D – Part 2
35 mins
A Better Swipe Technique
The slicing feels a little unnatural right now, because the player can move their finger in a curve, but we're treating the cut as a straight line from where the touch started. It's also a bit weird because the cut doesn't take effect until the player lifts their finger.
To fix this, go to HelloWorldLayer.mm and make the following changes:
// Add this method
-(void)clearSlices
{
for (b2Body* b = world->GetBodyList(); b; b = b->GetNext())
{
if (b->GetUserData() != NULL) {
PolygonSprite *sprite = (PolygonSprite*)b->GetUserData();
sprite.sliceEntered = NO;
sprite.sliceExited = NO;
}
}
}
// Add this at the end of ccTouchesMoved
if (ccpLengthSQ(ccpSub(_startPoint, _endPoint)) > 25)
{
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));
_startPoint = _endPoint;
}
// Remove these from ccTouchesEnded
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));
// Add this inside ccTouchesEnded
[self clearSlices];
You transfer the RayCast from ccTouchesEnded to ccTouchesMoved so that polygons could be split while moving the touch. Box2D ray casts can't be made too often, nor too short, so you only perform ray casts at every 5 points distance between the start and end points.
The above way of comparing distance is just an optimized way of saying "if (distance > 5)". Solving for distance requires a square root operation, and this is an expensive operation to be doing too often, so you just square both sides.
Once a ray is cast, you treat the end point of the ray cast as the new start point. Lastly, you clear all intersections whenever the player stops touching the screen.
Compile and run, and now the swipe should feel more natural.
With this method, you are more prone to violating Box2D's rules. Try creating a cut that exits the sprite from the same side it enters and see what happens. Also try cutting the polygon into itty bitty little pieces and see just how small you can go.
To address these issues, switch to RaycastCallback.h, and make these changes:
// Remove the CCLOG commands
// Add to top of file
#define collinear(x1,y1,x2,y2,x3,y3) fabsf((y1-y2) * (x1-x3) - (y1-y3) * (x1-x2))
// Remove this line from the else if statement
ps.sliceExited = YES;
// Add this inside the else if statement, right after setting the exitPoint
b2Vec2 entrySide = ps.entryPoint - ps.centroid;
b2Vec2 exitSide = ps.exitPoint - ps.centroid;
if (entrySide.x * exitSide.x < 0 || entrySide.y * exitSide.y < 0)
{
ps.sliceExited = YES;
}
else {
//if the cut didn't cross the centroid, you check if the entry and exit point lie on the same line
b2Fixture *fixture = ps.body->GetFixtureList();
b2PolygonShape *polygon = (b2PolygonShape*)fixture->GetShape();
int count = polygon->GetVertexCount();
BOOL onSameLine = NO;
for (int i = 0 ; i < count; i++)
{
b2Vec2 pointA = polygon->GetVertex(i);
b2Vec2 pointB;
if (i == count - 1)
{
pointB = polygon->GetVertex(0);
}
else {
pointB = polygon->GetVertex(i+1);
}//endif
float collinear = collinear(pointA.x,pointA.y, ps.entryPoint.x, ps.entryPoint.y, pointB.x,pointB.y);
if (collinear <= 0.00001)
{
float collinear2 = collinear(pointA.x,pointA.y,ps.exitPoint.x,ps.exitPoint.y,pointB.x,pointB.y);
if (collinear2 <= 0.00001)
{
onSameLine = YES;
}
break;
}//endif
}//endfor
if (onSameLine)
{
ps.entryPoint = ps.exitPoint;
ps.sliceEntryTime = CACurrentMediaTime() + 1;
ps.sliceExited = NO;
}
else {
ps.sliceExited = YES;
}//endif
}
Before accepting an exit point, the callback now checks the location of the two points. If the entry and exit points lie on opposite sides from the center of the polygon, then the cut is acceptable.
If not, you check if the entry and exit points lie on the same line by using a collinear checking function on all the lines formed by the vertices. If they are collinear, it means the intersection point is another entry point, otherwise, it is a complete slice.
Switch back to HelloWorldLayer.mm and replace the areVerticesAcceptable method with this:
-(BOOL)areVerticesAcceptable:(b2Vec2*)vertices count:(int)count
{
//check 1: polygons need to at least have 3 vertices
if (count < 3)
{
return NO;
}
//check 2: the number of vertices cannot exceed b2_maxPolygonVertices
if (count > b2_maxPolygonVertices)
{
return NO;
}
//check 3: Box2D needs the distance from each vertex to be greater than b2_epsilon
int32 i;
for (i=0; i<count; ++i)
{
int32 i1 = i;
int32 i2 = i + 1 < count ? i + 1 : 0;
b2Vec2 edge = vertices[i2] - vertices[i1];
if (edge.LengthSquared() <= b2_epsilon * b2_epsilon)
{
return NO;
}
}
//check 4: Box2D needs the area of a polygon to be greater than b2_epsilon
float32 area = 0.0f;
b2Vec2 pRef(0.0f,0.0f);
for (i=0; i<count; ++i)
{
b2Vec2 p1 = pRef;
b2Vec2 p2 = vertices[i];
b2Vec2 p3 = i + 1 < count ? vertices[i+1] : vertices[0];
b2Vec2 e1 = p2 - p1;
b2Vec2 e2 = p3 - p1;
float32 D = b2Cross(e1, e2);
float32 triangleArea = 0.5f * D;
area += triangleArea;
}
if (area <= 0.0001)
{
return NO;
}
//check 5: Box2D requires that the shape be Convex.
float determinant;
float referenceDeterminant;
b2Vec2 v1 = vertices[0] - vertices[count-1];
b2Vec2 v2 = vertices[1] - vertices[0];
referenceDeterminant = calculate_determinant_2x2(v1.x, v1.y, v2.x, v2.y);
for (i=1; i<count-1; i++)
{
v1 = v2;
v2 = vertices[i+1] - vertices[i];
determinant = calculate_determinant_2x2(v1.x, v1.y, v2.x, v2.y);
//you use the determinant to check direction from one point to another. A convex shape's points should only go around in one direction. The sign of the determinant determines that direction. If the sign of the determinant changes mid-way, then you have a concave shape.
if (referenceDeterminant * determinant < 0.0f)
{
//if multiplying two determinants result to a negative value, you know that the sign of both numbers differ, hence it is concave
return NO;
}
}
v1 = v2;
v2 = vertices[0]-vertices[count-1];
determinant = calculate_determinant_2x2(v1.x, v1.y, v2.x, v2.y);
if (referenceDeterminant * determinant < 0.0f)
{
return NO;
}
return YES;
}
You do 5 checks to determine if a polygon is acceptable by Box2D standards:
- Check 1: A polygon needs to have at least 3 vertices.
- Check 2: A polygon cannot exceed the predefined b2_maxPolygonVertices, which is 8 vertices.
- Check 3: The distance from each vertex must be greater than b2_epsilon.
- Check 4: The area of the polygon must be greater than b2_epsilon. This is still too small for us, so you just limit the area to 0.0001.
- Check 5: The shape must be convex.
The implementation of the first two checks are pretty straightforward, while the third and fourth checks are taken straight from the Box2D library. The last check uses determinants once again.
A convex shape's vertices should always turn in the same direction. If the direction suddenly changes, then the shape is automatically concave. You traverse the vertices of the polygon and compare the sign of the determinant. If the sign suddenly changes, then it means the vertex changed directions.
Compile and run, and make yourself some fruit salad!