本篇教程中我们将完成刚体切割效果的制作。
我们需要实现一个切割函数用来切割多边形,下面列出的是切割函数的执行步骤:
· 循环遍历results和endResults中的切点,找到results和endResults中对应于同一个物体的两个切点。
· 通过切点得到的切割线来将原物体(被切割物体)的顶点分为两组,每组切点用于定义一个切割后得到的新物体。
· 对计算得到的两组顶点进行排序(按照逆时针的方向)。
· 使用两组顶点创建切割后的多边形。
上面步骤中,顶点分组使用下面的方法来进行判断:
-(float)checkDet:(CGPoint)pointA
pointB:(CGPoint) pointB
pointC:(CGPoint) pointC {
return (pointA.x * pointB.y + pointB.x * pointC.y + pointC.x * pointA.y -pointA.y * pointB.x - pointB.y * pointC.x - pointC.y * pointA.x);
}
该方法计算切点pointA和pointB与某个顶点pointC的行列式的值,通过值的正负来判断C和直线AB的关系。原理请参考游戏中两个常用的数学运算推导及算法推论 。
创建多边形的方法:
-(void)createPolygon:(NSMutableArray*)vertexes
position:(b2Vec2) pos{
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position = pos;
b2Body* body = world->CreateBody(&bodyDef);
int vertexCount = [vertexes count];
b2Vec2 vers[b2_maxPolygonVertices];
b2PolygonShape* shape = new b2PolygonShape();
for (int i = 0; i < vertexCount; i++) {
b2Vec2 vertex = [self toVec:[[vertexes objectAtIndex:i] CGPointValue]];
vers[i] = vertex;
}
shape->Set(vers, [vertexes count]);
b2FixtureDef fixtureDef;
fixtureDef.shape = shape;
fixtureDef.friction = 0.2f;
fixtureDef.density = 2.0f;
body->CreateFixture(&fixtureDef);
}
注意这里b2_maxPolygonVertices这个常量在b2Settings.h中定义,默认为8,我们知道,如果切掉一个多边形的一角,得到一个三角形和另一个多边形,另一个多边形的顶点数会增加1,因此如果测试过程中发生多边形顶点数超过该最大值的异常,可以在b2Settings.h中将b2_maxPolygonVertices设置的大一些。其他的创建步骤没有什么特殊的,和之前创建用于切割的八边形的方法一样,只不过改成一般形式了。
下面来看对顶点进行逆时针排序的算法:
-(NSMutableArray*)reorderVertexes:(NSMutableArray*)vertexes {
int vertexCount = [vertexes count];
//定义排序后的数组
NSMutableArray* tmpVertexes = [[NSMutableArray alloc]initWithArray:vertexes copyItems:true];
//将顶点按照x由小到大的顺序排序
[vertexes sortUsingComparator:^(id obj1, id obj2) {
if ([obj1 CGPointValue].x > [obj2 CGPointValue].x) {
return (NSComparisonResult)NSOrderedDescending;
}
if ([obj1 CGPointValue].x < [obj2 CGPointValue].x) {
return (NSComparisonResult)NSOrderedAscending;
}
return (NSComparisonResult)NSOrderedSame;
}];
//得到x最小和最大的顶点(最左边和最右边的点)
CGPoint left = [vertexes[0] CGPointValue];
CGPoint right = [vertexes[vertexCount - 1] CGPointValue];
//leftPos为最左边的空位置,rightPos为最右边的空位置
int leftPos = 1;
int rightPos = vertexCount - 1;
//最左边的顶点为排序后index=0的点
tmpVertexes[0] = vertexes[0];
//遍历除最左边和最右边以外的其他顶点,根据顶点和切线的关系添加到排序后的空位置中
for (int i = 1; i < vertexCount - 1; i++) {
if ([self checkDet:left pointB:right pointC:[vertexes[i] CGPointValue]] > 0){
tmpVertexes[rightPos--] = vertexes[i];
} else {
tmpVertexes[leftPos++] = vertexes[i];
}
}
//将最右侧的点添加到最后的空位置中
tmpVertexes[leftPos] = vertexes[vertexCount - 1];
return tmpVertexes;
}
为了便于理解,我们使用下面的图来说明:
图中A-I为原多边形的顶点,P和Q为切点,切割后得到多边形ABCDEQPA和多边形PQFGHIP。而实际切割后得到的两组新顶点的顺序不一定是按照逆时针的顺序的,因此需要重新排序。我们以上面的多边形ABCDEQPA的顶点排序为例。首先将所有顶点按照x从小到大的顺序排序,然后确定最左侧和最右侧的顶点,这个例子中是P和E,连接PE得到一条线,我们可以通过前面定义的checkDet函数来判断剩下的其他顶点在PE的上方还是下方(或者说是顺时针方向还是逆时针方向),我们定义P点排序后的index值为0。因为顶点已经按照从小到大的顺序排列好了,所以我们遍历除了P和E之外的其他顶点(从小到大),对于遍历到的某个顶点M,如果M在PE的上方,那么,M插入到排序后的数组的最后一个空位置,如果M在PE的下方,则M插入到排序后的数组的最前一个空位置。
下面是排序过程:
首先按照x从大到小排序后得到的顺序:
P、A、B、C、D、Q、E
排序后数组(P在index0的位置):
P、空位置、空位置、空位置、空位置、空位置、空位置
接着首先遍历到的是A,即M=A,判断A在PE的上方,因此将A插入到排序后数组最后一个空位置,得到排序后的数组:
P、空位置、空位置、空位置、空位置、空位置、A
接着是B,同样是PE上方,所以插入最后一个空位置,得到:
P、空位置、空位置、空位置、空位置、B、A
同理C和D都插入到最后的空位置:
P、空位置、空位置、D、C、B、A
Q点在PE的下方,因此插入到最前面一个空位置,即P的后面一个位置:
P、Q、空位置、D、C、B、A
最后,将E(x最大的点)插入到最前一个空位置(同时也是最后一个空位置)
P、Q、E、D、C、B、A
排序后的顶点为逆时针方向。
再结合前面排序方法中的注释就比较好理解了。
最后,给出切割多边形的方法:
-(void)dividePolygons{
//两层循环找到results和endResults中对应于同一个物体的两个切点
for (RayCastResult* startResult in rayCastCallback.results) {
for (RayCastResult* endResult in rayCastCallback.endResults) {
//判断是否是同一个装置(即同一个物体)
if (startResult.fixture == endResult.fixture) {
//获取物体
b2Body* body = startResult.fixture->GetBody();
//获取两个切点的坐标(坐标转换为物体的本地坐标系)
CGPoint cutPointA = [self toCGPoint:body->GetLocalPoint(startResult.point)];
CGPoint cutPointB = [self toCGPoint:body->GetLocalPoint(endResult.point)];
//判断两个切点是不是同一个切点(如果切割线正好和物体相切于1点,那么就只有一个切点)
if (cutPointA.x != cutPointB.x) {
//初始化两个数组用来存放切割后得到的两个物体的顶点
NSMutableArray* newShape1Vertexes = [[NSMutableArray alloc] init];
NSMutableArray* newShape2Vertexes = [[NSMutableArray alloc] init];
//得到切割前物体的形状(用来获取顶点信息)
b2PolygonShape* shape = (b2PolygonShape*)startResult.fixture->GetShape();
//获取切割前物体的顶点数(用于遍历)
int vertexCount = shape->GetVertexCount();
//将两个切点先添加到切割后物体顶点的数组中
[newShape1Vertexes addObject:[NSValue valueWithCGPoint:cutPointA]];
[newShape1Vertexes addObject:[NSValue valueWithCGPoint:cutPointB]];
[newShape2Vertexes addObject:[NSValue valueWithCGPoint:cutPointA]];
[newShape2Vertexes addObject:[NSValue valueWithCGPoint:cutPointB]];
//遍历物体原来的顶点,根据顶点在切线的两侧来进行分组,加入到两个新的多边形的顶点数组中
for (int i = 0; i < vertexCount; i++) {
CGPoint vertex = [self toCGPoint:shape->GetVertex(i)];
//根据行列式的计算结果来确定顶点在切线的顺时针一侧还是逆时针一侧
float checkResult = [self checkDet:cutPointA pointB:cutPointB pointC:vertex];
//添加到切割后的物体顶点数组中
if (checkResult > 0) {
[newShape1Vertexes addObject:[NSValue valueWithCGPoint:vertex]];
} else if (checkResult < 0) {
[newShape2Vertexes addObject:[NSValue valueWithCGPoint:vertex]];
}
}
//如果切割后两个物体中任何一个顶点数不足3个(切割线与多边形相切的时候会发生这种情况),不进行切割
if ([newShape1Vertexes count] < 3 || [newShape2Vertexes count] < 3) {
continue;
}
//获取原来物体的位置信息
b2Vec2 position = body->GetPosition();
//移除原来的物体
world->DestroyBody(body);
//对新得到的两组顶点进行排序(按照逆时针方向进行排序),如果不排序创建的时候可能会发生扭曲或者异常
newShape1Vertexes = [self reorderVertexes:newShape1Vertexes];
newShape2Vertexes = [self reorderVertexes:newShape2Vertexes];
//创建切割得到的两个多边形
[self createPolygon:newShape1Vertexes position:position];
[self createPolygon:newShape2Vertexes position:position];
}
}
}
}
}
切割方法在ccTouchEnded方法中调用,并且记得在ccTouchEnded方法最后调用rayCastCallback.ClearResults()清除射线投射的结果,以便下一次投射。
到这里,刚体切割的效果就只做完成了,如有问题欢迎留言讨论~