Box2d中刚体的纹理可以有很多种实现方式(参考Box2d中刚体的纹理的几种实现方式),但是这几种实现方式都是在我们已知刚体形状并且保证刚体形状不变的情况下,通过提前将刚体的纹理绘制好并“附加”到刚体上来完成的。
因此如果遇到下面这些情况:
1. 刚体形状不确定,例如随机生成的形状或者某些游戏中玩家可以手工绘制物体等等。
2. 刚体形状可能会发生变化,例如切割游戏中,一个大的刚体被切割成若干个小的刚体,或者游戏中一些能够变形的物体或软体受力发生形变等等。
这时,我们通过提前绘制好纹理的那种解决方案就失效了。
那么这种情况下,我们的思路是根据任意路径去创建一个多边形,然后为多边形填充一个纹理,让多边形外边的区域不显示纹理呢。Box2d和Cocos2d中都没有为我们提供这样的类或者方法,因此我们使用开源库PRKit来实现。PRKit由PrecognitiveReserch的开发人员实现和维护,可以处理纹理映射和纹理填充。
PRKit的下载地址:https://github.com/asinesio/cocos2d-PRKit,点击右侧的“downloadzip”下载即可。
下载后解压,将子文件夹“PRKit”中的文件全部添加到项目中即可。
我们利用PRKit来制作一个简单的小例子:通过绘制来制作刚体,并为其填充纹理。
看来一下最终的效果:
首先创建cocos2d iOS withBox2d模板的工程(Box2d的版本是2.3.1),创建完成后,把我们不需要的默认创建出来的东东都给注释或者删掉,例如菜单啊,默认创建的box啊,点击创建box的事件响应方法呀等等这些,就剩下一个空白的场景就好了。
然后我们准备一张用于平铺的纹理贴图(注意纹理贴图在制作的时候左右和上下要能对接上,不然会不连续),将其添加到工程中,stone_texture.png:
接着我们在HelloWorldLayer中添加下面两个成员:
intvertex_min_distance;
NSMutableArray*pathVertexes;
vertex_min_distance定义了两个端点之间的取样最小距离,我们的思路是,当用户开始绘制刚体时,时时地获取绘制点的位置并记录,但是有一个问题是,如果取样过于密集,会导致最终得到的多边形边数过多(虽然这样看起来非常细腻,但是对于效率和模拟效果反而不利),因此通过定义一个端点之间的取样最小距离,当绘制点和上一个顶点的距离大于取样最小距离时,才进行记录,这样就能很好的限制顶点的数量(当然越大的图形顶点数量自然越多)。
另一个成员pathVertexes用来记录用户所绘制出的所有节点。
接下来我们让HelloWorldLayer继承CCTouchOneByOneDelegate接口,并重写下面的方法来注册touch事件:
-(void)onEnterTransitionDidFinish {
[[[CCDirector sharedDirector]touchDispatcher] addTargetedDelegate:self priority:1 swallowsTouches:true];
}
-(void) onExit{
[[[CCDirector sharedDirector]touchDispatcher] removeDelegate:self];
}
在初始化方法中初始化两个成员变量(对于取样的最小距离可以根据自己喜好来设置):
pathVertexes =[[NSMutableArray alloc] init];
vertex_min_distance= 30;
另外说明一点,Box2d中多边形的最大顶点数是通过b2Settings.h中的b2_maxPolygonVertices定义的,默认为8,如果在添加多边形的时候顶点数超过了这个上线,会抛错,因此,我们需要把这个值调大一些,这里我设置成50,如果觉得不够,可以再调大一些(对于这里例子来说,也不用太大了吧~~)。
接着我们来实现下面的三个touch事件响应函数:
-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [[CCDirectorsharedDirector] convertToGL:[touch locationInView:[touch view]]];
[pathVertexes addObject:[NSValuevalueWithCGPoint:location]];
return YES;
}
-(void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event {
int vertexCount = [pathVertexes count];
CGPoint location = [[CCDirectorsharedDirector] convertToGL:[touch locationInView:[touch view]]];
CGPoint lastVertex =[pathVertexes[vertexCount - 1] CGPointValue];
float distance = ccpDistance(location,lastVertex);
if (distance > vertex_min_distance) {
[pathVertexes addObject:[NSValuevalueWithCGPoint:location]];
}
}
-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
CGPoint location = [[CCDirectorsharedDirector] convertToGL:[touch locationInView:[touch view]]];
CGPoint firstVertex = [pathVertexes[0]CGPointValue];
if (ccpDistance(location, firstVertex) <vertex_min_distance) {
[self createPolygon];
}
[pathVertexes removeAllObjects];
}
ccTouchBegan方法比较简单,获取用户绘制的第一个点,然后添加到节点数组中。ccTouchMoved方法,首先获取当前绘制点的位置,通过ccpDistance计算这个接触点与上一个被记录的绘制点之间的距离,如果这个距离大于接触点取样的最小距离,则记录这个点。
最后在ccTouchEnded方法中,判断如果最后一个绘制点的位置与起始点之间的距离小于取样的最小距离,则认为我们绘制了一个“闭合的”多边形,然后调用createPolygon方法(后面会给出实现,这里编译出错可以先把这句注释掉)来进行多边形的创建。最后把用来存储节点的数组元素清空以便用于下一次绘制。
这里我们完成了基本的逻辑框架,但是我们在屏幕上拖动鼠标的时候看不到任何效果,我们希望在绘制的过程中能够看到我们拖拽出来的路径才对,在HelloWorldLayer的draw方法中添加下面的代码:
ccDrawColor4F(255,255, 255, 255);
intvertexCount = [pathVertexes count];
for (int i =0; i < vertexCount - 1; i++) {
CGPoint startPoint = [pathVertexes[i]CGPointValue];
CGPoint endPoint = [pathVertexes[i+1]CGPointValue];
ccDrawLine(startPoint, endPoint);
}
这段代码首先使用ccDrawColor4F方法设置用于绘制图形的颜色(255,255,255,255),即白色,不透明。接着遍历我们所有得到的取样点,然后使用ccDrawLine方法在相邻的取样点之间绘制直线。由于draw方法在每次刷新的时候都会调用,因此在我们绘制过程中,绘制的路径会一直存在,当绘制结束的时候,即ccTouchEnded方法被调用的时候,由于pathVertexes会被清空,此时路径会自动消失。效果下如图所示:
最后,我们来看一下createPolygon方法的实现:
-(void)createPolygon {
int vertexCount = [pathVertexes count];
//如果绘制的定点数不足三个,无法构成多边形
if (vertexCount < 3) {
return;
}
//由于绘制的方向可能是顺时针也可能是逆时针的,而Box2d中顶点需要按照逆时针方向,因此通过下面的循环来判断
//之所以使用循环,是因为3个取样点有可能共线,导致行列式的计算结果为0
BOOL isAntiClockwise = false;
for (int i = 0; i < vertexCount - 2;i++) {
float det = [selfcalculateDet:[pathVertexes[i] CGPointValue] pointB:[pathVertexes[i + 1]CGPointValue] pointC:[pathVertexes[i + 2] CGPointValue]];
if (det == 0) {
continue;
}
isAntiClockwise = det > 0 ? true :false;
break;
}
//如果是顺时针方向,则将所有节点反向排列
if (!isAntiClockwise) {
NSMutableArray* reversedArray =[[NSMutableArray alloc] init];
for (int i = vertexCount - 1; i >-1; i--) {
[reversedArrayaddObject:pathVertexes[i]];
}
pathVertexes = reversedArray;
}
//获取起始点的位置(多边形的位置也基于这个点的位置)
CGPoint startVertex = [pathVertexes[0]CGPointValue];
//定义刚体
b2BodyDef bodyDef;
bodyDef.type = b2_dynamicBody;
bodyDef.position = [selftoVec2:startVertex];
b2Body* body =world->CreateBody(&bodyDef);
//定义装置
b2FixtureDef fixtureDef;
fixtureDef.density = 2.0;
fixtureDef.friction = 0.2f;
//定义一个用于创建多边形形状的b2Vec2数组,需要将全局坐标系的点转换为本地坐标系的点
b2Vec2 vertexes[b2_maxPolygonVertices];
int j = 0;
for (int i = 0; i < vertexCount; i++) {
//通过与起始点坐标相减,将点坐标转换为本地坐标系
CGPoint ver = ccpSub([pathVertexes[i]CGPointValue], startVertex);
//下面这行语句非常重要,pathVertexes我们要用来初始化PRFilledPolygon,必须
//要使用本地坐标,而pathVertexes中的节点是全局坐标系
pathVertexes[i] = [NSValuevalueWithCGPoint:ver];
vertexes[j++] = [self toVec2:ver];
}
//定义形状
b2PolygonShape* shape = newb2PolygonShape();
shape->Set(vertexes, vertexCount);
fixtureDef.shape = shape;
body->CreateFixture(&fixtureDef);
//载入纹理
CCTexture2D* texture = [[CCTextureCachesharedTextureCache] addImage:@"stone_texture.png"];
PRFilledPolygon* prPolygon =[[PRFilledPolygon alloc] initWithPoints:pathVertexes andTexture:texture];
//纹理多边形的位置和body的位置相同,使用像素作为单位
prPolygon.position = [selftoCGPoint:body->GetPosition()];
//将纹理多边形添加到场景中
[self addChild:prPolygon];
//设置body的UserData指向填充多边形(用于在update方法中同步更新其位置)
body->SetUserData(prPolygon);
}
代码中做了详细的注释,这里关于顺时针和逆时针的判断,请参考游戏中两个常用的数学运算推导即算法推论。calculateDet方法的实现如下:
-(float)calculateDet:(CGPoint)p1 pointB:(CGPoint) p2 pointC:(CGPoint) p3 {
return (p1.x * p2.y + p2.x * p3.y + p3.x *p1.y - p1.y * p2.x - p2.y * p3.x - p3.y * p1.x);
}
此外用到的CGPoint和b2Vec2的转换函数如下:
-(CGPoint)toCGPoint:(b2Vec2) vec {
return ccp(vec.x * (float)PTM_RATIO, vec.y* (float)PTM_RATIO);
}
-(b2Vec2)toVec2:(CGPoint) point {
b2Vec2 vec(point.x / (float)PTM_RATIO,point.y / (float)PTM_RATIO);
return vec;
}
完成了,我们运行一下,发现绘制完成后,确实创建了纹理多边形,但是它并没有随着物体运动。我们需要在update方法最后添加如下的代码:
for (b2Body*body = world->GetBodyList(); body; body = body->GetNext()) {
if (body != NULL) {
PRFilledPolygon* polygon =(PRFilledPolygon*)body->GetUserData();
polygon.position = [selftoCGPoint:body->GetPosition()];
polygon.rotation =-CC_RADIANS_TO_DEGREES(body->GetAngle());
}
}
这样每次更新所有刚体的位置之后,会遍历所有的刚体更新其对应的纹理多边形。
另外要说明的一点是,在Box2d中正常只支持凸多边形,因此在绘制的时候,尽量避免绘制出凹多边形,否则凹面的碰撞会有问题。凹多边形可以通过多个凸多边形组合而成,这部分内容不在本文范围内。
好了,就说到这里,如果有不清楚的地方,欢迎留言讨论。