当前位置:   article > 正文

cocos2d中使用chipmunk做碰撞检测_chipmunk2d碰撞检测

chipmunk2d碰撞检测
我们知道cocos2d本身不包含物理引擎,但是它集成了2个开源物理引擎可供我们选择:box2d和chipmunk。对于一般的正规矩形精灵的碰撞检测,可以简单的使用Core Graphics提供的CGRectIntersectsRect接口实现。其他情况要想减少工作量,最好的方法就是借助物理引擎。

本篇教程实现一个菱形和三角形的碰撞检测,足以演示基本的chipmunk用法。
Demo基于cocos2d 2.0 beta,chipmunk6.0.2.

教程截图:



Demo下载地址: http://ityran.com/thread-904-1-1.html

1 Demo流程  
首先创建一个三角形的精灵,我把它命名为emery,它静止在屏幕上。每次点击屏幕,从左边固定位置会发射出一个菱形的bullet,bullet匀速直线运动碰到emery后消失。Reset重新开新游戏。

2 创建一个chipmunk的工程  
首先需要安装cocos2d 2.0 beta,之所以选择2.0,有以下2个原因:
  1. cocos2d 2.0使用OpenGL ES2.0,早日过渡到OpenGL ES 2.0是大势所趋(1.x和2.0的区别,详见http://www.ityran.com/thread-338-1-1.html)。
  2. 目前只有cocos2d 2.0才集成了chipmunk 6,而chipmunk 6相对于5接口上简化了很多,更易使用。

创建工程的时候选择cocos2d_chipmunk模板,
这样我们就得到一个包含了chipmunk的工程,并自带了测试代码。
而HelloWorldLayer将是我们的主战场,修改它的实现达到前述Demo效果。

3 chipmunk基本概念  
space:物理空间,可容纳body, shape,joint。
body:刚体,可被赋予shape。刚体具有质量,转动惯量,位置,线性速度,加速度,角度,角速度,角加速度等属性。刚体之间可通过joint连接。
shape:决定刚体的碰撞外形。一个刚体上可覆盖上多个shape,同属于一个刚体的shape不会互相发生碰撞。
joint: 用于连接刚体。

本篇教程不涉及joint,也不涉及一个body多个shape。
也就是一个精灵对于一个刚体有一个外形。有点绕口~。~

关于space,默认有一个staticBody属性,staticBody是在整个物理检测中永远保持禁止不动的刚体。这通常用来把屏幕四周设置为墙体,以避免精灵飞出到屏幕外面。

接下来我们看代码实现。

4 chipmunk系统初始化
在AppDelegate.m的applicationDidFinishLaunching函数前面加入
// init chipmunk
cpInitChipmunk();
初始化整个chipmunk系统,只需做一次。
模板生成的代码是在layer初始化中完成chipmunk的初始化,这样处理并不是很合适,我把它移到这个位置来。

5 HelloWorldLayer.h解析
@interface HelloWorldLayer : CCLayerColor
{
	cpSpace *space_; // strong ref
}
@end


@interface PhysicsSprite : CCSprite
{
	cpShape *shape_;	// strong ref
	cpSpace *space_;	// weak ref
}

-(void) setPhysicsShape:(cpShape *)shape space:(cpSpace *)space;
HelloWorldLayer包含了一个cpSpace,每个layer对应一个space,这很好理解,layer自己管理自己的space,不同的layer的space会不一样。space的生命周期由layer管理。
定义一个PhysicsSprite,他有两个成员变量shape_和space_,space_只是引用,不做生命周期管理。

setPhysicsShape方法把精灵和刚体关联起来。

6 HelloWorldLayer.m解析

6.1 PhysicsSprite的实现
由于cocos2d 2.0 使用OpenGL ES 2.0,这里有了新的改变精灵位置和方向的方法:矩阵变换。
看过泰然OpenGL ES系列教程( 从零开始学习OpenGL ES之七 – 变换和矩阵 )的对这个应该有印象。

-(BOOL) dirty
{
	return YES;
}
重写dirty方法,返回YES,目的是让每次layer的update调用后重新绘制PhysicsSprite精灵。

-(CGAffineTransform) nodeToParentTransform
{
	CGFloat x = shape_->body->p.x;
	CGFloat y = shape_->body->p.y;
	
	if ( !isRelativeAnchorPoint_ ) {
		x += anchorPointInPoints_.x;
		y += anchorPointInPoints_.y;
	}
	
	// Make matrix
	CGFloat c = shape_->body->rot.x;
	CGFloat s = shape_->body->rot.y;
	
	if( ! CGPointEqualToPoint(anchorPointInPoints_, CGPointZero) ){
		x += c*-anchorPointInPoints_.x + -s*-anchorPointInPoints_.y;
		y += s*-anchorPointInPoints_.x + c*-anchorPointInPoints_.y;
	}
	
	// Translate, Rot, anchor Matrix
	transform_ = CGAffineTransformMake( c,  s,
									   -s,	c,
									   x,	y );
	
	return transform_;
}
重写精灵的矩阵变换方法nodeToParentTransform,模板提供的这个实现,能改变精灵的位置和角度。shape_->body->p和shape_->body->rot分别是刚体的位置坐标和角度。我们会看到,在update函数中,调用了chipmunk的cpSpaceStep方法,这个方法根据时间流逝计算出每个刚体的新位置和角度,然后在这里被使用最终达到精灵移动旋转的目的。

(void) removeFromParentAndCleanup:(BOOL)cleanup
{
	cpSpaceRemoveBody(space_, shape_->body);
	cpBodyFree(shape_->body);
	
	cpSpaceRemoveShape(space_, shape_);
	cpShapeFree(shape_);
	
	[super removeFromParentAndCleanup:cleanup];
}
重写removeFromParentAndCleanup方法。
在碰撞发生后,bullet精灵消失,与之对应的刚体也需要释放。
在body被free之前,它必须先从space中移出,否则cpSpaceStep会继续计算这个刚体,接下来就是crash~。
shape也是同样的处理。

6.2 HelloWorldLayer初始化
-(id) init
{
	if( (self=[super initWithColor:ccc4(166, 166, 166, 255)])) {
		
		// enable events
		self.isTouchEnabled = YES;
		self.isAccelerometerEnabled = YES;
		
		CGSize s = [[CCDirector sharedDirector] winSize];
		
		// title
		CCLabelTTF *label = [CCLabelTTF labelWithString:@"Touch the screen"
fontName:@"Marker Felt" fontSize:36]; label.position = ccp( s.width / 2, s.height - 30); [self addChild:label z:0]; // reset button [self createResetButton]; // init physics [self initPhysics]; [self addNewEmeryAtPosition:ccp(240,160)]; [self scheduleUpdate]; } return self; }
  1. 初始化CCLayerColor的颜色,作为背景色。
  2.  使图层接受按键相应和重力感应,Demo中并未使用重力感应。
  3. 用CCLabelTTF在屏幕上显示一串提示信息。 
  4. 初始化用来复位的reset按钮。 
  5. 初始化物理引擎,后详解。 
  6. 在240,160的位置初始化一个静止不动的emery。
  7. 一切初始化完毕,进入更新循环周期。

6.3 初始化物理引擎
-(void) initPhysics
{
	//CGSize s = [[CCDirector sharedDirector] winSize];
	
	space_ = cpSpaceNew();
	
	// set to zero
	space_->gravity = ccp(0, 0);
	cpSpaceAddCollisionHandler(space_, kTagBulletNode, kTagEmeryNode, begin, NULL, NULL, NULL, NULL);
}
  1. cpSpaceNew创建一个新的物理空间。模板代码有staticBody的初始化,在这里我们不需要墙体防止精灵飞出屏幕,去掉相关代码。
    注:在chipmunk 5.3之前的版本,需要手动创建一个infinite的body,并用cpSpaceAddStaticShape方法加入到sapce中,现在这些繁琐的过程都由chipmunk自行完成。 
  2. gravity是一个矢量,表示重力的方向与大小,要简单地得到匀速直线运动的子弹,把重力设为0会是个简单的方法。
  3. 设置碰撞检测回调函数。 cpSpaceAddCollisionHandler的参数很多,按顺序如下: 
    • c语言的特点,没有self,需要设置当前space,表示在这个空间发生的碰撞。 
    • 哪个形状和哪个形状发生碰撞。
    • 同上,这2个参数可以一样。
    • 碰撞开始前的回调。 
    • 碰撞开始时的回调。 
    • 碰撞结束时的回调。 
    • 碰撞分离后的回调。 
    • 传递给回调函数的参数。 
kTagBulletNode, kTagEmeryNode是2个枚举变量,标识不同刚体的形状的,后面还会使用到。 在这个Demo中我们只是用到碰撞开始前的回调begin。

6.4 Emery精灵的创建
-(void) addNewEmeryAtPosition:(CGPoint)pos
{
	PhysicsSprite *sprite = [PhysicsSprite spriteWithFile:@"triangle.png" rect:CGRectMake(0, 0, 50, 50)]; 
	sprite.position = pos;
	sprite.tag = kTagEmeryNode;
	[self addChild:sprite];
	
	int num = 3;
	CGPoint verts[] = {
		ccp(-25,-25),
		ccp(0, 25),
		ccp(25, -25),
	};
	
	cpBody *body = cpBodyNew(1.0f, cpMomentForPoly(1.0f, num, verts, CGPointZero));
	
	body->p = pos;
	cpSpaceAddBody(space_, body);
	
	cpShape* shape = cpPolyShapeNew(body, num, verts, CGPointZero);
	shape->collision_type = kTagEmeryNode;
	shape->data = sprite;
	//shape->e = 0.5f; shape->u = 0.5f;
	cpSpaceAddShape(space_, shape);
	
	[sprite setPhysicsShape:shape space:space_];
}
  1. PhysicsSprite创建一个精灵,triangle.png是一个等腰三角形,三角形外的区域是透明色。 
  2. 设置精灵的tag 为 kTagEmeryNode,在demo中并没有实际的用途,不过这是个好习惯,对调试有帮助。
  3. 接下来是再熟悉不过的addChild了。 
  4. 给多边形shape描点。num定义点的个数,verts按顺序包含每个点的坐标。首先我们得了解下chipmunk有四种shape:圆形,弧形,多边形,方形。分别有不同的函数来创建这些shape并计算他们的惯性值。我们这里只涉及到多边形。点的坐标是以图片的中心为原点,一个像素为一个单位长度,计算出每个点的坐标值。 
  5. cpBodyNew创建一个刚体,参数如下: 
    • 刚体质量。 
    • 刚体惯性值。 
    惯性值的计算使用chipmunk提供的cpMomentForPoly助手函数,参数如下:
    • 刚体质量。
    • 多边形点数量。
    • 多边形点的坐标集。
    • 偏移量,会作用到每个点,设为CGPointZero会让事情简单很多。 
  6. 我们让刚体的坐标和精灵的坐标保存一致。chipmunk的坐标值和cocos2d的坐标值是一样的,不用转换,可直接赋值。 
  7. cpSpaceAddBody把刚体放到物理空间中,它将受到重力作用。还记得吗?我已经把重力设为失重状态。 
  8. 我们还需要为刚体定义至少一个shape,cpPolyShapeNew来完成这项工作,参数如下:
    • 刚体。
    • 多边形点数。
    • 多边形点的坐标集。
    • 偏移量,会作用到每个点,设为CGPointZero会让事情简单很多。
  9. shape的collision_type属性,为自定义的一个类型,设为kTagEmeryNode,在设置碰撞回调的时候,我们已经使用了这个值,这里必须设置,否则碰撞回调不会被触发。 
  10. shape的data属性为一个void指针,我们设置为sprite,是为了方便的从shape获取与之对应的精灵。 
  11. cpSpaceAddShape,shape同样需要加入到物理空间中去。 
  12. 最后setPhysicsShape把刚体关联到精灵中去。

addNewBulletAtPosition的实现流程大致相同,参数不同而已。 
最大的不同点: cpBodySetVel(body, cpv(160, 0)); 
我们给bullet刚体设置了一个初速度,cpv构成一个速度矢量,表明了方向和速度大小。

6.5 碰撞检测
整个chipmunk工作流程是这样子:
在space中我们摆放好了各个刚体,刚体有shape,速度等属性。
每个update周期,cpSpaceStep依据时间的推移,计算出新的刚体位置坐标,方向等。
cocos2d依据这些数据来重新绘制精灵。
我们设置的碰撞回调函数会在cpSpaceStep中某个阶段被调用。
static int
begin(cpArbiter *arb, cpSpace *space, void *unused)
{
	// Get pointers to the two bodies in the collision pair and define local variables for them.
	// Their order matches the order of the collision types passed
	// to the collision handler this function was defined for
	CP_ARBITER_GET_SHAPES(arb, a, b);
	
	// additions and removals can't be done in a normal callback.
	// Schedule a post step callback to do it.
	// Use the hook as the key and pass along the arbiter.
	cpSpaceAddPostStepCallback(space, (cpPostStepFunc)postStepRemove, a, NULL);
	
	// The object is dead, don't process the collision further
	return 0;
}
  1. CP_ARBITER_GET_SHAPES取出是哪两个shape发生了碰撞,a,b是emery还是bullet,与之前设置回调函数时的第2,3参数的顺序有关。 
  2. 我们不能直接在begin函数里面释放刚体或者shape,要等chipmunk做完必要的计算后才能释放。cpSpaceAddPostStepCallback用来安全的完成这个步骤。
  3. 我们return 0,表示不需要chipmunk处理后面的回调过程了。
postStepRemove的实现如下:
static void
postStepRemove(cpSpace *space, cpShape *shape, void *unused)
{
	PhysicsSprite *sprite = shape->data;
	assert(sprite.tag == kTagBulletNode);
	if( sprite ) {
		[sprite removeFromParentAndCleanup:YES];
	}
}
shape->data保存了精灵实例。
assert帮助我们调试,确认下将被销毁的是不是bullet精灵。
removeFromParentAndCleanup把精灵从屏幕上抹去。

6.6 HelloWorldLayer销毁时的内存清理
reset按钮的处理流程,涉及整个space清理,过程如下:
CCScene *s = [CCScene node];
id child = [HelloWorldLayer node];
[s addChild:child];
[[CCDirector sharedDirector] replaceScene: s];
按钮事件触发以后,创建了一个新的包含HelloWorldLayer 的Scene,来取代当前Scene。
cocos2d会自动去销毁之前的scene,dealloc将被调用。

- (void)dealloc
{
	cpSpaceEachBody(space_, spaceBodyCallback, NULL);
	cpSpaceEachShape(space_, spaceShapeCallback, NULL);
	cpSpaceFree( space_ );
	
	[super dealloc];
}
cpSpaceEachBody和cpSpaceEachShape是两个遍历方法,遍历space中的所有body和shape,并为每个找到的body或shape调用处理回调函数。
在处理回调函数spaceBodyCallback和spaceShapeCallback里面我们进行释放操作,在这里不cpSpaceRemoveBody和cpSpaceRemoveShape是安全的,space马上就会被释放,update也不会被调用。

7 最后
运行Demo看下效果吧,点击屏幕任意位置会触发一次子弹。
Demo可能比较简陋,如果能达到 Keep It Simple, Stupid 的效果,那就简陋点吧。
声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号