如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第2部分

2024-03-17 02:38

本文主要是介绍如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第2部分,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这是教你如何制作一款像Halfbrick Studios公司的Fruit Ninja一样的切割精灵游戏系列教程的第2篇。

在第1篇中,你学会了如何创建纹理多边形,并基于它制作了一个西瓜。

在第1部分中所做的努力将在第2部分中收到回报,在此部分中,你将能够切割sprite。

和第1部分一样,本篇教程需要你熟悉Cocos2D和Box2D。如果你是刚刚接触它们的话,请先学习本网站的Cocos2D入门和Box2D入门。

准备工作

如果你还没有第1部分结束时的工程,请下载sample project来继续本篇教程。

接下来,对PolygonSprite的结构体进行一些修改以让它能处理切割。

打开PolygonSprite.h并作如下修改:

// 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;

然后,打开PolygonSprite.mm并作如下修改:

// 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;

编译并检查语法错误。

以上的代码对PolygonSprite类及其子类进行了改进,储存了切割需要的变量信息:

  • entryPoint: 切割线首次和多边形接触的点。
  • exitPoint: 切割线第二次和多边形接触的点。
  • sliceEntered: 判断多边形是否已经有切割线进入了。
  • sliceExited: 判断多边形是否被完整切割过一次。
  • sliceEntryTime: 切割线进入多边形时的准确时间。用来决定过慢的轻扫动作不被视为切割动作。

使用Ray Casts与Sprites相交

为了切割sprite,你必须能够判断点在哪儿。这就需要用到Box2D的ray casting。

在ray casting中,你需要指定一个起始点和一个结束点,Box2D会根据它们组成的线段判断哪些Box2D的fixtures和它有相交。不只如此,它还会触发一个回调函数来告诉你具体每一个与其碰撞的fixture。

你将要使用ray casts,基于玩家触摸屏幕的点,来判断出所有触摸经过的fixtures,并使用回调函数来记录每个具体的相交的点。

打开HelloWorldLayer.h并在@interface中加入如下内容:

CGPoint _startPoint;
CGPoint _endPoint;

切换到HelloWorldLayer.mm并做如下修改:

// 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;}
}

以上代码为触摸事件指定了起始点和结束点。

当玩家触摸屏幕时,起始点在ccTouhesBegan方法中被记录下来,结束点跟随玩家手指的滑动,相应的在ccTouhesMoved方法中被记录。

ccDrawLine方法从起始点到结束点画一条线。

编译并运行,试着在屏幕中画一条线:

Draw the Line

这条线将会代表你接下来要创建的ray cast。

为了使用Box2D的ray casting,你只需简单的调用world对象中的RayCast,并提供给它起始和结束点即可,每和任意一个fixture有交集的时候,就会触发一个回调函数。

ray cast的方法需要存储在一个b2RayCastCallback类当中。

在Xcode中,进入FileNewNew File菜单,选择 iOSC and C++Header File,并点击Next。为新的头文件命名为RayCastCallback.h,点击Save。

把该文件替换为以下内容:

#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 shapeps.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

每当Box2D检测到一次接触,就会调用ReportFixture方法。如果多边形还没有切割线进入,那么就把相交点设置为entry point,如果已经有切割线进入了,就把相交点设置为exit point。

你使用GetLocalPoint转换了坐标点是因为你需要知道在多边形内部的坐标,而不是世界坐标。世界坐标是起始于屏幕左下角,而本地坐标起始于形状的左下角。

最后,你返回 1 来告诉Box2D,ray cast在检测到第一个fixture之后,还应该继续检测其他fixtures。返回其他的值会另次方法有其他表现,但是这已经超出了本篇教学的范畴。

切换到HelloWorldLayer.h并作如下修改:

// Add to top of file
#import "RaycastCallback.h"// Add inside the @interface
RaycastCallback *_raycastCallback;

接下来,切换到HelloWorldLayer.mm并做如下修改:

// 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));

你声明了一个RayCastCallback类并将其作为RayCast方法的参数。目前你只在玩家触摸结束的时刻调用RayCast。

你调用两次ray cast是因为Box2D ray casting只在一个方向上检测相交。解决的方法是在反方向上再次调用RayCast。

编译并运行。试着画一条线并检查logs。

Check the Logs

分隔多边形

分隔多边形也许是本教程中最难的一部分,主要是因为此操作需要很多的计算,同时有很多的Box2D的规则需要遵守。

不要着急,这同时也是最cool的一部分,我会一点一点的带你学会它!

切换到HelloWorldLayer.h并作如下修改:

// Add to top of file
#define calculate_determinant_2x2(x1,y1,x2,y2) x1*y2-y1*x2
#define calculate_determinant_2x3(x1,y1,x2,y2,x3,y3) x1*y2+x2*y3+x3*y1-y1*x2-y2*x3-y3*x1// Add after the properties
-(b2Vec2*)arrangeVertices:(b2Vec2*)vertices count:(int)count;
-(void)splitPolygonSprite:(PolygonSprite*)sprite;
-(BOOL)areVerticesAcceptable:(b2Vec2*)vertices count:(int)count;
-(b2Body*)createBodyWithPosition:(b2Vec2)position rotation:(float)rotation vertices:(b2Vec2*)vertices vertexCount:(int32)count density:(float)density friction:(float)friction restitution:(float)restitution;

切换到HelloWorldLayer.mm并添加如下方法:

-(void)splitPolygonSprite:(PolygonSprite*)sprite
{//declare & initialize variables to be used for laterPolygonSprite *newSprite1, *newSprite2;//our original shape's attributesb2Fixture *originalFixture = sprite.body->GetFixtureList();b2PolygonShape *originalPolygon = (b2PolygonShape*)originalFixture->GetShape();int vertexCount = originalPolygon->GetVertexCount();//our determinant(to be described later) and iteratorfloat determinant;int i;//you store the vertices of our two new sprites hereb2Vec2 *sprite1Vertices = (b2Vec2*)calloc(24, sizeof(b2Vec2));b2Vec2 *sprite2Vertices = (b2Vec2*)calloc(24, sizeof(b2Vec2));b2Vec2 *sprite1VerticesSorted, *sprite2VerticesSorted;//you store how many vertices there are for each of the two new sprites hereint sprite1VertexCount = 0;int sprite2VertexCount = 0;//step 1://the entry and exit point of our cut are considered vertices of our two new shapes, so you add these before anything elsesprite1Vertices[sprite1VertexCount++] = sprite.entryPoint;sprite1Vertices[sprite1VertexCount++] = sprite.exitPoint;sprite2Vertices[sprite2VertexCount++] = sprite.entryPoint;sprite2Vertices[sprite2VertexCount++] = sprite.exitPoint;//step 2://iterate through all the vertices and add them to each sprite's shapefor (i=0; i<vertexCount; i++){//get our vertex from the polygonb2Vec2 point = originalPolygon->GetVertex(i);//you check if our point is not the same as our entry or exit point firstb2Vec2 diffFromEntryPoint = point - sprite.entryPoint;b2Vec2 diffFromExitPoint = point - sprite.exitPoint;if ((diffFromEntryPoint.x == 0 && diffFromEntryPoint.y == 0) || (diffFromExitPoint.x == 0 && diffFromExitPoint.y == 0)){}else {determinant = calculate_determinant_2x3(sprite.entryPoint.x, sprite.entryPoint.y, sprite.exitPoint.x, sprite.exitPoint.y, point.x, point.y);if (determinant > 0){//if the determinant is positive, then the three points are in clockwise ordersprite1Vertices[sprite1VertexCount++] = point;}else{//if the determinant is 0, the points are on the same line. if the determinant is negative, then they are in counter-clockwise ordersprite2Vertices[sprite2VertexCount++] = point;}//endif}//endif}//endfor//step 3://Box2D needs vertices to be arranged in counter-clockwise order so you reorder our points using a custom functionsprite1VerticesSorted = [self arrangeVertices:sprite1Vertices count:sprite1VertexCount];sprite2VerticesSorted = [self arrangeVertices:sprite2Vertices count:sprite2VertexCount];//step 4://Box2D has some restrictions with defining shapes, so you have to consider these. You only cut the shape if both shapes pass certain requirements from our functionBOOL sprite1VerticesAcceptable = [self areVerticesAcceptable:sprite1VerticesSorted count:sprite1VertexCount];BOOL sprite2VerticesAcceptable = [self areVerticesAcceptable:sprite2VerticesSorted count:sprite2VertexCount];//step 5://you destroy the old shape and create the new shapes and spritesif (sprite1VerticesAcceptable && sprite2VerticesAcceptable){//create the first sprite's body        b2Body *body1 = [self createBodyWithPosition:sprite.body->GetPosition() rotation:sprite.body->GetAngle() vertices:sprite1VerticesSorted vertexCount:sprite1VertexCount density:originalFixture->GetDensity() friction:originalFixture->GetFriction() restitution:originalFixture->GetRestitution()];//create the first spritenewSprite1 = [PolygonSprite spriteWithTexture:sprite.texture body:body1 original:NO];[self addChild:newSprite1 z:1];//create the second sprite's bodyb2Body *body2 = [self createBodyWithPosition:sprite.body->GetPosition() rotation:sprite.body->GetAngle() vertices:sprite2VerticesSorted vertexCount:sprite2VertexCount density:originalFixture->GetDensity() friction:originalFixture->GetFriction() restitution:originalFixture->GetRestitution()];//create the second spritenewSprite2 = [PolygonSprite spriteWithTexture:sprite.texture body:body2 original:NO];[self addChild:newSprite2 z:1];//you don't need the old shape & sprite anymore so you either destroy it or squirrel it awayif (sprite.original){   [sprite deactivateCollisions];sprite.position = ccp(-256,-256);   //cast them farawaysprite.sliceEntered = NO;sprite.sliceExited = NO;sprite.entryPoint.SetZero();sprite.exitPoint.SetZero();}else {world->DestroyBody(sprite.body);[self removeChild:sprite cleanup:YES];}}else{sprite.sliceEntered = NO;sprite.sliceExited = NO;}//free up our allocated vectorsfree(sprite1VerticesSorted);free(sprite2VerticesSorted);free(sprite1Vertices);free(sprite2Vertices);
}

Wow,好多的代码啊。先编译一下确保没有错误,然后让我们循序渐进的过一遍这个方法:

准备阶段
声明变量。此部分最重要的是你声明了两个PolygonSprites对象,并使用两个数组保存了他们多边形的顶点。

阶段 1
第一步,分别向代表每个形状中顶点的数组中加入分割点。
下边的图例说明了这个步骤的意义:

Intersection Points Belong to Both Shapes

两个相交点同时属于两个形状的顶点。

阶段 2
你分派原形状中剩余的顶点。你知道这个形状永远都会被切成两部分,新的两个形状分别会在切割线的两端。

你仅仅需要一个新的规则来决定原形状上的顶点该属于哪个新的形状。

想象一下你有一个方法可以判断任意给定的三个点是顺时针的,还是逆时针的。如果你有了这个方法,那么你就可以根据起始点,结束点和原图形上的一点来做如下判断:

“如果这三个点是顺时针的,那么把这个点加到形状2中,否则,加入到形状1!”

Clockwise & Counter-Clockwise

好消息是,有一个方法可以用来决定这种顺序,通过使用一个叫做determinants的数学概念来实现它!

在几何学中,determinants是一种数学方法,它可以判断一个点和一条线的关系,根据返回值结果的不同(正,负,0)来决定点在线的位置。

determinant方程定义在HelloWorldLayer.h中,接收的参数为entry point,exit point,还有原图形上其中一个顶点。

如果结果是正的,那么3个点就是顺时针的,如果结果是负的,它们就是逆时针的。如果结果是0,那么它们就在一条线上。

你把所有的顺时针的点都加入到第1个sprite中,其他的加入到第2个sprite中。

阶段 3
Box2D需要所有的顶点都以逆时针顺序组织,所以你使用arrangeVertices方法为两个新sprite需要重新排列顶点。

阶段 4
这一步确保了这些经过重新排列的顶点满足Box2D的定义多边形的规则。如果areVerticesAcceptable方法认为这些顶点是不满足条件的,那么就把本次切割的信息从原sprite中移除。

阶段 5
这一步初始化了两个新的PolygonSprite对象并使用createBody方法创建了它们的Box2D body。新的sprite的属性会继承原sprite。

如果是一个原sprite被切割了,它的状态会被重置。如果是一片被切割了,那么它将会被销毁并从场景中移除。

呼…还跟着我呢吗?好,在你运行程序之前,还有额外的一些内容要添加:

仍然在HelloWorldLayer.mm中,作如下修改:

// Add before the @implementation
int comparator(const void *a, const void *b) {const b2Vec2 *va = (const b2Vec2 *)a;const b2Vec2 *vb = (const b2Vec2 *)b;if (va->x > vb->x) {return 1;} else if (va->x < vb->x) {return -1;}return 0;    
}// Add these methods
-(b2Body*)createBodyWithPosition:(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;b2PolygonShape shape;shape.Set(vertices, count);fixtureDef.shape = &shape;body->CreateFixture(&fixtureDef);return body;
}-(b2Vec2*)arrangeVertices:(b2Vec2*)vertices count:(int)count
{float determinant;int iCounterClockWise = 1;int iClockWise = count - 1;int i;b2Vec2 referencePointA,referencePointB;b2Vec2 *sortedVertices = (b2Vec2*)calloc(count, sizeof(b2Vec2));//sort all vertices in ascending order according to their x-coordinate so you can get two points of a lineqsort(vertices, count, sizeof(b2Vec2), comparator);sortedVertices[0] = vertices[0];referencePointA = vertices[0];          //leftmost pointreferencePointB = vertices[count-1];    //rightmost point//you arrange the points by filling our vertices in both clockwise and counter-clockwise directions using the determinant functionfor (i=1;i<count-1;i++){determinant = calculate_determinant_2x3(referencePointA.x, referencePointA.y, referencePointB.x, referencePointB.y, vertices[i].x, vertices[i].y);if (determinant<0){sortedVertices[iCounterClockWise++] = vertices[i];}else {sortedVertices[iClockWise--] = vertices[i];}//endif}//endifsortedVertices[iCounterClockWise] = vertices[count-1];return sortedVertices;
}-(BOOL)areVerticesAcceptable:(b2Vec2*)vertices count:(int)count
{return YES;
}

这是以上方法的分类说明:

  • createBody: 此方法创建了活跃的可以和其他body产生碰撞的Box2D body。
  • arrangeVertices: 此方法按照逆时针的顺序重排顶点。它使用qsort方法按x坐标升序排列,然后使用determinants来完成最终的重排。
  • comparator: 此方法被qsort使用,它完成顶点比较并返回结果给qsort。
  • areVerticesAcceptable: 目前,此方法假设所有的顶点都是合理的。

就是它了!理论上说,你现在就可以把一个多边形切成两部分了。但是…等等…我们最好用上你刚刚创建的方法! :]

还是在HelloWorldLayer.mm,添加以下修改:

// Add this method
-(void)checkAndSliceObjects
{double curTime = CACurrentMediaTime();for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()){if (b->GetUserData() != NULL) {PolygonSprite *sprite = (PolygonSprite*)b->GetUserData();if (sprite.sliceEntered && curTime > sprite.sliceEntryTime) {sprite.sliceEntered = NO;}else if (sprite.sliceEntered && sprite.sliceExited){[self splitPolygonSprite:sprite];}}}
}// Add this in the update method
[self checkAndSliceObjects];

编译并运行,你可以试着去切割你的西瓜。

等等它…
The Power of Math Cuts the Watermelon

成功了!原来数学公式也能切水果啊!

注意: 如果游戏突然挂掉了请不要着急。在完成了areVerticesAcceptable方法之后,这就会被修复了。

一种更好的Swipe技术

目前,切割感觉有一点不自然,因为玩家的手指可以移动一个曲线,但是我们仅仅把它当作直线来处理了。另外还有一点导致不自然的原因是,必须玩家的手指抬起来,切割才会生效。

为了修复这个问题,打开HelloWorldLayer.mm并作如下修改:

// 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];

你把RayCast方法从ccTouchesEnded移动到了ccTouchesMoved,现在多边形就能够在手指移动过程中被切割了。Box2D ray cast不能被触发太频繁,也不能太不频繁,所以你设置每达到5个坐标长度时触发一次。

使用ccpLengthSQ比较距离只是一种更优化的方式(与distance > 5相比)。处理距离需要用到开方公式,开方操作的消耗比较大,不能很频繁的使用。仅仅把等式两边都平方即可解决。

每当RayCast方法执行,你都把结束点重新当成起始点处理。最后,当玩家结束触摸屏幕时,你清除所有的相交点。

编译并运行,现在滑动感觉更自然了。

A More Natural Swipe

使用这个方法,你将更容易破坏Box2D的规则。尝试创建一个结束点和起始点在同一边的切割线,看看会发生什么。同时还可以尝试能把sprite切割成多少个小片。

这就来处理这些问题,切换到RaycastCallback.h并作如下修改:

// 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 lineb2Fixture *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);}//endiffloat 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}//endforif (onSameLine){ps.entryPoint = ps.exitPoint;ps.sliceEntryTime = CACurrentMediaTime() + 1;ps.sliceExited = NO;}else {ps.sliceExited = YES;}//endif
}

在接受一个结束点之前,这个回调函数检查两点的位置,如果起始点和结束点处在多边形中心点的两侧,那么这次切割是合理的。

如果不在多边形中心点的两侧,那么继续检测切割线起始点和结束点是否在原图形所有的顶点形成的线上。如果他们在一条线上,那么就意味着相交点是另一个起始点,否则,就是一次完整的切割。

切换回HelloWorldLayer.mm并把areVerticesAcceptable方法替换为如下:

-(BOOL)areVerticesAcceptable:(b2Vec2*)vertices count:(int)count
{//check 1: polygons need to at least have 3 verticesif (count < 3){return NO;}//check 2: the number of vertices cannot exceed b2_maxPolygonVerticesif (count > b2_maxPolygonVertices){return NO;}//check 3: Box2D needs the distance from each vertex to be greater than b2_epsilonint32 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_epsilonfloat32 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 concavereturn 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;
}

你做了5步检查来决定一个多边形是否满足Box2D的标准:

  • Check 1: 一个多边形至少需要3个顶点。
  • Check 2: 多边形的顶点数最多不能超过预定义的b2_maxPolygonVertices,目前是8.
  • Check 3: 每个顶点之间的距离必须大于b2_epsilon。
  • Check 4: 多边形的面积必须大于b2_epsilon。这对于我们来说有点太小了,所以你适当调整为0.0001。
  • Check 5: 形状必须的凸的。

前两个检查直截了当,第3个和第4个检查都是Box2D库要求的。最后的一个再次使用了determinants。

一个凸的形状的顶点应该总是想一个方向拐弯。如果方向突然改变了,那么这个形状就会变为凹的。你遍历多边形的顶点并比较determinant结果的符号。如果符号突然改变了,就意味着多边形顶点的方向变了。

编译并运行,切些水果并为你自己做些水果沙拉吧!

Fruit Grinder!

结束调试模式

现在你已经可以确定Box2D部分的工作都如你所料了,所以你不再需要调试绘制模式了。

还是在HelloWorldLayer.mm中,作如下修改:

// Comment these out from the draw method
ccDrawLine(_startPoint, _endPoint);
world->DrawDebugData();// Add inside the init method
[self initBackground];// Add this method
-(void)initBackground
{CGSize screen = [[CCDirector sharedDirector] winSize];CCSprite *background = [CCSprite spriteWithFile:@"bg.png"];background.position = ccp(screen.width/2,screen.height/2);[self addChild:background z:0];
}

编译并运行,你会看到一个漂亮的背景,它是由Vicki为本篇教学创作的。

Monkey Forest

使用CCBlade使切割可视化

没有了调试绘制,你需要一个新方法来显示切割动作。由Ngo Duc Hiep制作的CCBlade是一个完美的解决方案。

下载 CCBlade,解压它,在 Xcode 中按 Option+Command+A 添加 CCBlade.m 和 CCBlade.h
到你的工程中。确保“Copy items into destination group’s folder”和“Create groups for any added folders”是选中的。

CCBlade是由第三方维护的,所以本篇教学所用的版本也许不是最新的。你可以从resource kit的Class文件夹中得到本篇教学所用的CCBlade版本。

你需要把CCBlade更新到Cocos2D 2.X,打开CCBlade.m,将其重命名为CCBlade.mm,并作如下修改:

// Replace everything starting from glDisableClientState in the draw method with this
CC_NODE_DRAW_SETUP();ccGLBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );ccGLBindTexture2D( [_texture name] );    
glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, sizeof(vertices[0]), vertices);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, sizeof(coordinates[0]), coordinates);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 2*[path count]-2);// Add inside the initWithMaximumPoint method
self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture];// Remove from the setWidth method
* CC_CONTENT_SCALE_FACTOR()// Remove from the push method
if (CC_CONTENT_SCALE_FACTOR() != 1.0f) {v = ccpMult(v, CC_CONTENT_SCALE_FACTOR());
}

你使用和之前转换PRFilledPolygon中的drawing代码一样的方式,并移除了缩放系数,因为shader程序已经处理它了。

CCBlade有一个由点组成的path(路径)数组,并穿过这些点绘制一个纹理直线。目前它是在draw方法中更新的这个数组。不过,一种更推荐的方式是只在draw方式中绘制,其他的内容放到update方法中去。

为了更好的管理path数组,你在HelloWorldLayer的update方法中更新它们。

打开CCBlade.h并在@interface中加入以下内容:

@property(nonatomic,retain)NSMutableArray *path;

切换到CCBlade.mm并在@implementation中加入以下内容:

@synthesize path;

接下来,切换到HelloWorldLayer.h并作如下修改:

// Add to top of file
#import "CCBlade.h"// Add inside the @interface
CCArray *_blades;
CCBlade *_blade;
float _deltaRemainder;// Add after the @interface
@property(nonatomic,retain)CCArray *blades;

最后,切换到HelloWorldLayer.mm并做如下修改:

// Add inside the @implementation
@synthesize blades = _blades;// Add inside dealloc
[_blades release];
_blades = nil;// Add inside init, after _raycastCallback
_deltaRemainder = 0.0;
_blades = [[CCArray alloc] initWithCapacity:3];
CCTexture2D *texture = [[CCTextureCache sharedTextureCache] addImage:@"streak.png"];for (int i = 0; i < 3; i++)
{CCBlade *blade = [CCBlade bladeWithMaximumPoint:50];blade.autoDim = NO;blade.texture = texture;[self addChild:blade z:2];[_blades addObject:blade];
}// Add inside update, right after [self checkAndSliceObjects]
if ([_blade.path count] > 3) {_deltaRemainder+=dt*60*1.2;int pop = (int)roundf(_deltaRemainder);_deltaRemainder-=pop;[_blade pop:pop];
}// Add inside ccTouchesBegan
CCBlade *blade;
CCARRAY_FOREACH(_blades, blade)
{if (blade.path.count == 0){_blade = blade;[_blade push:location];break;}
}// Add inside ccTouchesMoved
[_blade push:location];// Add inside ccTouchesEnded
[_blade dim:YES];

你为path数组制作了一个属性,这样就可以在HelloWorldLayer中访问它们了。然后你创建了3个在游戏中公用的CCBlade对象。对每一个blade,你设置最大的点个数为50来防止轨迹太长,并设置blade的纹理为Resources文件夹中的streak。

你设置每个blade的autoDim变量为NO。CCBlade使用术语“Dim”来说明此blade会自动从尾巴到头的渐变消失。CCBlade自动从path数组中移除这些点。

虽然这很方便,但是CCBlade在它自己的draw方法中已经实现了自动弹出效果,所以最好把这个属性设置为NO并由我们自己在update方法中控制它的dim特性。

每当玩家触摸屏幕,你都指定一个目前空闲的CCBlade,并把触摸到的点压入它的path数组中。

最后,当玩家结束触摸屏幕时,你通知CCBlade设置其dim为YES,让其自动渐隐销毁。

你让update方法来处理目前活跃的CCBlade的dimming。你想让它无视帧率来恰当的渐隐,所以你把delta time乘上一个常数。

因为delta time并不一定是一个整数,所以你需要用一个remainder变量将其存储,下次循环到来时再作计算。

编译并运行,试试看你的新的漂亮的刀光效果吧!

Cool Blade Effect

何去何从?

这是到目前为止的教程的示例工程。

这就是第2部分的全部内容了,在第1部分中,你创建了西瓜的纹理多边形,但它最终会落到地上。现在,你已经可以用一个很cool的刀光效果把这只西瓜切成小细块儿了。

在接下来的系列教程的第3部分中,你将会把所有内容合并成一款完整的游戏!


这篇关于如何使用Box2D和Cocos2D制作一款像Fruit Ninja一样的游戏-第2部分的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/817560

相关文章

C语言中联合体union的使用

本文编辑整理自: http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=179471 一、前言 “联合体”(union)与“结构体”(struct)有一些相似之处。但两者有本质上的不同。在结构体中,各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和。而在“联合”中,各成员共享一段内存空间, 一个联合变量

Tolua使用笔记(上)

目录   1.准备工作 2.运行例子 01.HelloWorld:在C#中,创建和销毁Lua虚拟机 和 简单调用。 02.ScriptsFromFile:在C#中,对一个lua文件的执行调用 03.CallLuaFunction:在C#中,对lua函数的操作 04.AccessingLuaVariables:在C#中,对lua变量的操作 05.LuaCoroutine:在Lua中,

Vim使用基础篇

本文内容大部分来自 vimtutor,自带的教程的总结。在终端输入vimtutor 即可进入教程。 先总结一下,然后再分别介绍正常模式,插入模式,和可视模式三种模式下的命令。 目录 看完以后的汇总 1.正常模式(Normal模式) 1.移动光标 2.删除 3.【:】输入符 4.撤销 5.替换 6.重复命令【. ; ,】 7.复制粘贴 8.缩进 2.插入模式 INSERT

Lipowerline5.0 雷达电力应用软件下载使用

1.配网数据处理分析 针对配网线路点云数据,优化了分类算法,支持杆塔、导线、交跨线、建筑物、地面点和其他线路的自动分类;一键生成危险点报告和交跨报告;还能生成点云数据采集航线和自主巡检航线。 获取软件安装包联系邮箱:2895356150@qq.com,资源源于网络,本介绍用于学习使用,如有侵权请您联系删除! 2.新增快速版,简洁易上手 支持快速版和专业版切换使用,快速版界面简洁,保留主

如何免费的去使用connectedpapers?

免费使用connectedpapers 1. 打开谷歌浏览器2. 按住ctrl+shift+N,进入无痕模式3. 不需要登录(也就是访客模式)4. 两次用完,关闭无痕模式(继续重复步骤 2 - 4) 1. 打开谷歌浏览器 2. 按住ctrl+shift+N,进入无痕模式 输入网址:https://www.connectedpapers.com/ 3. 不需要登录(也就是

android一键分享功能部分实现

为什么叫做部分实现呢,其实是我只实现一部分的分享。如新浪微博,那还有没去实现的是微信分享。还有一部分奇怪的问题:我QQ分享跟QQ空间的分享功能,我都没配置key那些都是原本集成就有的key也可以实现分享,谁清楚的麻烦详解下。 实现分享功能我们可以去www.mob.com这个网站集成。免费的,而且还有短信验证功能。等这分享研究完后就研究下短信验证功能。 开始实现步骤(新浪分享,以下是本人自己实现

Toolbar+DrawerLayout使用详情结合网络各大神

最近也想搞下toolbar+drawerlayout的使用。结合网络上各大神的杰作,我把大部分的内容效果都完成了遍。现在记录下各个功能效果的实现以及一些细节注意点。 这图弹出两个菜单内容都是仿QQ界面的选项。左边一个是drawerlayout的弹窗。右边是toolbar的popup弹窗。 开始实现步骤详情: 1.创建toolbar布局跟drawerlayout布局 <?xml vers

C#中,decimal类型使用

在Microsoft SQL Server中numeric类型,在C#中使用的时候,需要用decimal类型与其对应,不能使用int等类型。 SQL:numeric C#:decimal

探索Elastic Search:强大的开源搜索引擎,详解及使用

🎬 鸽芷咕:个人主页  🔥 个人专栏: 《C++干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 引入 全文搜索属于最常见的需求,开源的 Elasticsearch (以下简称 Elastic)是目前全文搜索引擎的首选,相信大家多多少少的都听说过它。它可以快速地储存、搜索和分析海量数据。就连维基百科、Stack Overflow、

flask 中使用 装饰器

因为要完成毕业设计,我用到fountain code做数据恢复。 于是在github上下载了fountain code的python原代码。 github上的作者用flask做了fountain code的demo。 flask是面向python的一个网站框架。 里面有用到装饰器。 今天笔试的时候,我也被问到了python的装饰器。