当前位置: 代码迷 >> 综合 >> 带你一起分析cut the rope(切绳子游戏)中绳子的制作方法
  详细解决方案

带你一起分析cut the rope(切绳子游戏)中绳子的制作方法

热度:43   发布时间:2023-12-14 22:11:54.0
??


因为是要模拟物理效果,所以创建工程的时候使用cocos2d ios with Box2D模板。接着,准备一个用来作为绳子片段的图片,例如:

rope.png:4px×2px的一个橙色小方块,如果你想要带有样式的绳子,可以用PS简单制作),注意,纹理的长宽一定要是2的幂指数(因为我们要用到平铺纹理)。

rope.png导入工程的resources中。

在模拟的时候,实际上是将绳子切分为很多个小线段来处理的,当分段足够细足够多时,绳子的模拟效果就会足够平滑。

为了模拟绳子受力效果,我们为每一个小线段定义节点,然后对这些点施加力(或者说模拟这些点的位移)来模拟绳子的运动效果,同时,我们用这些点来计算和限制每个小线段的移动,防止整个绳子松散掉。

在下面的说明中,我们对于每一部分先给出一些代码,然后做一下解释。

首先按照设计,我们需要定义三个类(都继承NSObject类),一个是Rope类,不必多说,就是我们的“绳子”类,还有一个是Segment类,定义了用来细分绳子的小线段,最后一个是Vertex类,即前面所说的节点。

首先来看一下Rope类的声明:

#import"cocos2d.h"

#import"Vertex.h"

#import"Segment.h"

#import"Box2D.h"

 

@interfaceRope : NSObject {

   NSMutableArray* vertexes;

   NSMutableArray* segments;

   NSMutableArray* segSprites;

   CCSpriteBatchNode* ropeSegBatchNode;

   b2Body* startVertex;

   b2Body* endVertex;

   int segmentCount;

   float segmentConnectionRatio;

}

 

-(id)initWithStart:(b2Body*) startVertex end:(b2Body*) endVertexsegBatchNode:(CCSpriteBatchNode*) ropeSegBatchNode;

-(void)updateSegments:(float)delta;

-(void)updateSegmentSprites;

-(void)createRope;

 

@end

头文件中包括了cocos2d和box2d的头文件,还有Vertex类和Segment类的头文件。在Rope类中定义了节点Vertex和小线段Segment类的数组,用来存储整个绳子中包含的节点和线段,此外,还定义了一个CCSprite对象的数组,因为每一个小线段最终还是要通过精灵来显示的。除了这些绳子的组成部分,CCSpriteBatchNode成员用于共享纹理贴图(因为每一个Segment的CCSprite对象的纹理都是一样的,如果单个渲染效率代价太大)。startVertex和endVertex是两个box2d的刚体对象,用来模拟绳子的两个端点的物理效果。segmentCount用来存储绳子上的Segment个数(用于update循环),segmentConnectionRatio这个系数取值范围在0-0.1之间,我们知道Segment之间通过节点进行连接,如果节点定义在片段的边缘,那么两个片段在成一定角度的时候容易产生锯齿或者不连续的效果,因此将连接的节点向Segment内部移动一定的比例,保证其连续和平滑效果,segmentConnectionRatio就定义了这个比例,参考下面的图片:


 


Rope类中定义了4个方法,initWithStart:end:segBatchNode:方法为初始化方法,通过传入起点和终点的刚体对象来做初始化工作。createRope创建绳子(包括Segments,Vertexes和CCSprite对象),updateSegments用来计算和更新线段的位置和节点的位置,updateSegmentSprites用来重新刷新每个线段对应的精灵的位置和旋转,实现绳子的动画效果。

这里我们先不给出实现,我们先给出Vertex和Segment类的声明和定义。

Vertex.h:

@interfaceVertex : NSObject {

   float oldX;

   float oldY;

}

 

@propertyfloat x;

@propertyfloat y;

 

-(void)setX:(float)xPos andY:(float)yPos;

-(void)updatePos;

-(void)applyGravity:(float) delta;

-(CGPoint)toCGPoint;

 

@end

类中声明了节点的位置x和y,另外还定义了前一次计算的位置oldX和oldY(oldX和oldY的用途在类定义的注释中会解释)。setX:andY方法用来更新x和y的值,updatePos方法用于根据oldX和oldY来修正x和y的位置(方法实现中有注释)。applyGravity方法对节点施加一个位移来模拟重力对绳子的影响。toCGPoint方法将Vertex类转换为CGPoint结构。

下面是Vertex的定义(Vertex.mm):

#import"Vertex.h"

 

@implementationVertex

 

@synthesize x;

@synthesize y;

 

-(void)setX:(float)xPos andY:(float)yPos {

   oldX = x = xPos;

   oldY = y = yPos;

}

 

-(void)updatePos {

   //之所以要记录旧的位置,是因为每次调用updatePos方法

   //更新位置之后,还会通过多次迭代计算来再次修正每个节点

   //的位置(因为每个线段不能够被拉伸,而节点在重力和其他

   //线段的影响下会致使线段拉伸,因此要做修正),这些修正

   //并没有立即生效,而在下一次调用Rope的updateSegments

   //方法更新时又会覆盖掉节点的位置调整,因此,需要记录旧

   //的位置,并将之前的调整得到的差值反馈到节点位置上。

   float tmpX = x;

   float tmpY = y;

   x += x - oldX;

   y += y - oldY;

   oldX = tmpX;

   oldY = tmpY;

}

 

-(void)applyGravity:(float)delta {

   y -= delta * 10.0; //10.0是重力的系数,如果要模拟轻绳子,可以适当减小这个系数,建议在5.0-10.0之间

}

 

-(CGPoint)toCGPoint {

   return CGPointMake(x, y);

}

 

@end

下面是Segment的声明(Segment.h):

#import"Vertex.h"

 

@interfaceSegment : NSObject {

   float length;

}

 

@propertyVertex* startVertex;

@propertyVertex* endVertex;

 

-(id)initWithStartVertex:(Vertex*) start andEndVertex:(Vertex*) end;

-(void)adjustVertexPosition;

 

@end

length为线段的初始长度,也是线段的不可变长度,根据这个长度来对线段的两端节点进行修正。startVertex和endVertex为两端节点。adjustVertexPosition用来修正节点。

Segment类定义(Segment.mm):

#import"Segment.h"

#import"cocos2d.h"

 

@implementationSegment

 

@synthesizestartVertex;

@synthesizeendVertex;

 

-(id)initWithStartVertex:(Vertex *)start andEndVertex:(Vertex *)end {

   if (self = [super init]) {

       startVertex = start;

       endVertex = end;

       

       //记录线段的初始长度

       length = ccpDistance([start toCGPoint],[end toCGPoint]);

   }

   

   return self;

}

 

-(void)adjustVertexPosition {

   //x和y方向的距离,用于后面求微调值时计算比例关系

   float xLength = endVertex.x -startVertex.x;

   float yLength = endVertex.y -startVertex.y;

   

   //实际现在线段的长度(这个长度和初始值会有差异,所以需要修正)

   float actualLength =ccpDistance([startVertex toCGPoint], [endVertex toCGPoint]);

   

   //需要修正的长度

   float adjustment = length - actualLength;

   

   //通过三角形相似性来计算x和y方向上的修正长度,乘以0.5是因为起点和终点分别修正一半

   float xAdjustment = adjustment * xLength *0.5 / actualLength;

   float yAdjustment = adjustment * yLength *0.5 / actualLength;

   

   //修正起点和终点位置

   startVertex.x -= xAdjustment;

   startVertex.y -= yAdjustment;

   endVertex.x += xAdjustment;

   endVertex.y += yAdjustment;

}

 

@end

初始化方法中通过两个节点计算出线段的长度。adjustVertexPosition中对各部分代码都做了详细的注释,这里不多解释了。

接着我们来看Rope类的定义。

Rope类的初始化方法定义如下:

-(id)initWithStart:(b2Body *)startVertex end:(b2Body *)endVertexsegBatchNode:(CCSpriteBatchNode *)ropeSegBatchNode {

   if (self = [super init]) {

       self->vertexes = [[NSMutableArrayalloc] init];

       self->segments = [[NSMutableArrayalloc] init];

       self->segSprites = [[NSMutableArrayalloc] init];

       self->startVertex = startVertex;

       self->endVertex = endVertex;

       self->ropeSegBatchNode =ropeSegBatchNode;

       [self createRope];

   }

   

   return self;

}

方法初始化了数组元素,将传入的两个端点的刚体对象赋给类的属性,传入纹理参数,调用createRope方法来初始化绳子中的元素(节点,线段,精灵等等)。

createRope方法:

-(void)createRope {

   //获取两个端点的位置

   b2Vec2 startVec =startVertex->GetPosition();

   b2Vec2 endVec =endVertex->GetPosition();

   CGPoint startPos = ccp(startVec.x *PTM_RATIO, startVec.y * PTM_RATIO);

   CGPoint endPos = ccp(endVec.x * PTM_RATIO,endVec.y * PTM_RATIO);

   

   //计算绳子长度

   float totalLength = ccpDistance(startPos,endPos);

   

   //定义每个线段的长度,减小这个值可以更平滑,但是太小容易导致不连续的效果

   float segmentLength = 8;

   

   //计算线段总个数

   segmentCount = totalLength / segmentLength;

   

   //计算表示绳子方向的基向量

   CGPoint directionVector =ccpNormalize(ccpSub(endPos, startPos));

   

   //定义线段连接处的缩进值(0-0.1之间)

   segmentConnectionRatio = 0.1f;

   

   //计算所有的节点的位置并加入数组

   for (int i = 0; i < segmentCount + 1;i++) {

       //通过起点和计数器来计算第i个节点的位置

       CGPoint vPos = ccpAdd(startPos,ccpMult(directionVector, segmentLength * i * (1 - segmentConnectionRatio)));

       Vertex* vertex = [[Vertex alloc] init];

       [vertex setX:vPos.x andY:vPos.y];

       [vertexes addObject:vertex];

   }

   

   //初始化所有的线段并加入数组

   for (int i = 0; i < segmentCount; i++) {

       Segment* seg = [[Segment alloc]initWithStartVertex:[vertexes objectAtIndex:i] andEndVertex:[vertexesobjectAtIndex:i+1]];

       [segments addObject:seg];

   }

   

   //初始化精灵数组

   if (ropeSegBatchNode != nil) {

       for (int i = 0; i < segmentCount;i++) {

           Segment* seg = [segmentsobjectAtIndex:i];

           CGPoint startPoint =[seg.startVertex toCGPoint];

           CGPoint endPoint = [seg.endVertextoCGPoint];

           

           //初始化精灵

           CCSprite* segSprite = [CCSpritespriteWithTexture:ropeSegBatchNode.texture rect:CGRectMake(0, 0, segmentLength,[[[ropeSegBatchNode textureAtlas] texture] pixelsHigh])];

           

           //线段方向向量

           CGPoint directionVector =ccpSub(startPoint, endPoint);

           //线段的角度

           float segmentAngle =ccpToAngle(directionVector);

           

           //设置重复纹理

           ccTexParams param = { GL_LINEAR,GL_LINEAR, GL_REPEAT, GL_REPEAT };

           [segSprite.texturesetTexParameters:&param];

           

           //设置线段的位置和角度

           [segSpritesetPosition:ccpMidpoint(startPoint, endPoint)];

           [segSpritesetRotation:-CC_RADIANS_TO_DEGREES(segmentAngle)];

           

           [ropeSegBatchNodeaddChild:segSprite];

           [segSprites addObject:segSprite];

       }

   }

}

方法计算了线段的个数,初始化了节点、线段和精灵数组。方法中做了详细的注释,这里不过多的做解释了。

创建完成Rope之后,接下来我们来看更新绳子运动模拟的方法:

-(void)updateSegments:(float)delta {

   //获取两个端点的位置

   b2Vec2 startVec =startVertex->GetPosition();

   b2Vec2 endVec =endVertex->GetPosition();

   CGPoint startPos = ccp(startVec.x *PTM_RATIO, startVec.y * PTM_RATIO);

   CGPoint endPos = ccp(endVec.x * PTM_RATIO,endVec.y * PTM_RATIO);

   

   //更新绳子两个端点的位置

   [[vertexes objectAtIndex:0] setX:startPos.xandY:startPos.y];

   [[vertexes objectAtIndex:segmentCount]setX:endPos.x andY:endPos.y];

   

   //对绳子施加重力效果,更新绳子每个节点的位置

   for (int i = 1; i < segmentCount; i++) {

       Vertex* vertex = [vertexesobjectAtIndex:i];

       [vertex applyGravity:delta];

       [vertex updatePos];

   }

   

   //8次迭代(迭代次数越多,效率越低,动画越细腻)来修正线段节点的位置

   int iterationCount = 8;

   for (int i = 0; i < iterationCount; i++){

       for (int j = 0; j < segmentCount;j++) {

           [[segments objectAtIndex:j]adjustVertexPosition];

       }

   }

}

方法updateSegments首先更新两个端点的位置,然后对绳子施加重力,最后再对节点进行修正。这里注意,我们调用updateSegments方法实际上只是对虚拟的节点和线段做了调整,并没有更新精灵,所以此时绳子并没有表现出任何变化。updateSegmentSprites方法根据updateSegments中修正的节点的位置来更新精灵:

-(void)updateSegmentSprites {

   if (ropeSegBatchNode != nil) {

       for (int i = 0; i < segmentCount;i++) {

           Segment* seg = [segmentsobjectAtIndex:i];

           CGPoint startPoint =[seg.startVertex toCGPoint];

           CGPoint endPoint = [seg.endVertextoCGPoint];

           float angle =ccpToAngle(ccpSub(startPoint, endPoint));

           CCSprite* sprite = [segSpritesobjectAtIndex:i];

           [spritesetPosition:ccpMidpoint(startPoint, endPoint)];

           [spritesetRotation:-CC_RADIANS_TO_DEGREES(angle)];

       }

   }

}

方法中对于每一个线段,根据两端节点的位置来更新精灵的位置和旋转,产生动画效果。

接着我们来修改主场景的层HelloWorldLayer,在HelloWorldLayer声明中添加三个成员:

CCSpriteBatchNode*segmentBatchNode;

b2Body*anchorPoint;

NSMutableArray*ropes;

其中,segmentBatchNode用于在Segment之间共享纹理贴图。anchorPoint为绳子固定的旋转轴(一个虚拟的节点),ropes为绳子数组。

HelloWorldLayer的init方法中添加初始化的语句:

ropes =[[NSMutableArray alloc] init];

segmentBatchNode= [CCSpriteBatchNode batchNodeWithFile:@"rope.png" capacity:100];

[selfaddChild:segmentBatchNode];

初始化绳子数组和纹理对象。

initPhysics(初始化物理模拟)方法中添加绳子的虚拟旋转轴anchorPoint的初始化语句:

b2BodyDefanchorPointDef;

anchorPointDef.position.Set(s.width* 0.5 / PTM_RATIO, s.height * 0.8 / PTM_RATIO);

anchorPoint =world->CreateBody(&anchorPointDef);

接着我们在addNewSpriteAtPosition(不同的Box2D框架的版本不同,该方法的名称可能不同,这里我们Box2D的版本为2.3.0,该方法用于向场景中添加刚体盒子)方法最后添加下面的代码:

b2RopeJointDefjointDef;

jointDef.bodyA= anchorPoint;

jointDef.bodyB= body;

jointDef.localAnchorA= b2Vec2(0, 0);

jointDef.localAnchorB= b2Vec2(0, 0);

jointDef.maxLength=(body->GetPosition() - anchorPoint->GetPosition()).Length();

world->CreateJoint(&jointDef);

   

Rope* rope =[[Rope alloc] initWithStart:anchorPoint end:bodysegBatchNode:segmentBatchNode];

[ropesaddObject:rope];

该方法定义了一个绳索关节,将盒子对象和绳子的旋转轴软连接在一起,然后在他们之间创建绳子对象。

最后,我们在update方法的最后添加下面的语句,更新所有添加进场景中的绳子:

for (Rope*rope in ropes) {

   [rope updateSegments:dt];

}

 

for (Rope*rope in ropes) {

   [rope updateSegmentSprites];

}

接着我们可以做一下测试,下面是运行效果:


 

我们可以增大绳子的长度,并且将盒子变为静态对象来看下效果:


 

大功告成,有问题欢迎留言讨论。

官网示例代码下载地址:http://www.cocoachina.com/bbs/job.php?action=download&aid=17114

??