我们知道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个原因:
创建工程的时候选择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函数前面加入
模板生成的代码是在layer初始化中完成chipmunk的初始化,这样处理并不是很合适,我把它移到这个位置来。
5 HelloWorldLayer.h解析
定义一个PhysicsSprite,他有两个成员变量shape_和space_,space_只是引用,不做生命周期管理。
setPhysicsShape方法把精灵和刚体关联起来。
6 HelloWorldLayer.m解析
6.1 PhysicsSprite的实现
由于cocos2d 2.0 使用OpenGL ES 2.0,这里有了新的改变精灵位置和方向的方法:矩阵变换。
看过泰然OpenGL ES系列教程( 从零开始学习OpenGL ES之七 – 变换和矩阵 )的对这个应该有印象。
在碰撞发生后,bullet精灵消失,与之对应的刚体也需要释放。
在body被free之前,它必须先从space中移出,否则cpSpaceStep会继续计算这个刚体,接下来就是crash~。
shape也是同样的处理。
6.2 HelloWorldLayer初始化
6.3 初始化物理引擎
6.4 Emery精灵的创建
addNewBulletAtPosition的实现流程大致相同,参数不同而已。
最大的不同点: cpBodySetVel(body,cpv(160,0));
我们给bullet刚体设置了一个初速度,cpv构成一个速度矢量,表明了方向和速度大小。
6.5 碰撞检测
整个chipmunk工作流程是这样子:
在space中我们摆放好了各个刚体,刚体有shape,速度等属性。
每个update周期,cpSpaceStep依据时间的推移,计算出新的刚体位置坐标,方向等。
cocos2d依据这些数据来重新绘制精灵。
我们设置的碰撞回调函数会在cpSpaceStep中某个阶段被调用。
assert帮助我们调试,确认下将被销毁的是不是bullet精灵。
removeFromParentAndCleanup把精灵从屏幕上抹去。
6.6 HelloWorldLayer销毁时的内存清理
reset按钮的处理流程,涉及整个space清理,过程如下:
cocos2d会自动去销毁之前的scene,dealloc将被调用。
在处理回调函数spaceBodyCallback和spaceShapeCallback里面我们进行释放操作,在这里不cpSpaceRemoveBody和cpSpaceRemoveShape是安全的,space马上就会被释放,update也不会被调用。
7 最后
运行Demo看下效果吧,点击屏幕任意位置会触发一次子弹。
Demo可能比较简陋,如果能达到 Keep It Simple,Stupid 的效果,那就简陋点吧。
本篇教程实现一个菱形和三角形的碰撞检测,足以演示基本的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个原因:
- cocos2d 2.0使用OpenGL ES2.0,早日过渡到OpenGL ES 2.0是大势所趋(1.x和2.0的区别,详见http://www.ityran.com/thread-338-1-1.html)。
- 目前只有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函数前面加入
初始化整个chipmunk系统,只需做一次。// init chipmunk cpInitChipmunk();
模板生成的代码是在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,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; }
- 初始化CCLayerColor的颜色,作为背景色。
- 使图层接受按键相应和重力感应,Demo中并未使用重力感应。
- 用CCLabelTTF在屏幕上显示一串提示信息。
- 初始化用来复位的reset按钮。
- 初始化物理引擎,后详解。
- 在240,160的位置初始化一个静止不动的emery。
- 一切初始化完毕,进入更新循环周期。
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); }
- cpSpaceNew创建一个新的物理空间。模板代码有staticBody的初始化,在这里我们不需要墙体防止精灵飞出屏幕,去掉相关代码。
注:在chipmunk 5.3之前的版本,需要手动创建一个infinite的body,并用cpSpaceAddStaticShape方法加入到sapce中,现在这些繁琐的过程都由chipmunk自行完成。 - gravity是一个矢量,表示重力的方向与大小,要简单地得到匀速直线运动的子弹,把重力设为0会是个简单的方法。
- 设置碰撞检测回调函数。 cpSpaceAddCollisionHandler的参数很多,按顺序如下:
- c语言的特点,没有self,需要设置当前space,表示在这个空间发生的碰撞。
- 哪个形状和哪个形状发生碰撞。
- 同上,这2个参数可以一样。
- 碰撞开始前的回调。
- 碰撞开始时的回调。
- 碰撞结束时的回调。
- 碰撞分离后的回调。
- 传递给回调函数的参数。
- c语言的特点,没有self,需要设置当前space,表示在这个空间发生的碰撞。
6.4 Emery精灵的创建
-(void) addNewEmeryAtPosition:(CGPoint)pos { PhysicsSprite *sprite = [PhysicsSprite spriteWithFile:@"triangle.png" rect:CGRectMake(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,}; cpBody *body = cpBodyNew(1.0f,cpMomentForPoly(1.0f,num,verts,CGPointZero)); body->p = pos; cpSpaceAddBody(space_,body); cpShape* shape = cpPolyShapeNew(body,CGPointZero); shape->collision_type = kTagEmeryNode; shape->data = sprite; //shape->e = 0.5f; shape->u = 0.5f; cpSpaceAddShape(space_,shape); [sprite setPhysicsShape:shape space:space_]; }
- PhysicsSprite创建一个精灵,triangle.png是一个等腰三角形,三角形外的区域是透明色。
- 设置精灵的tag 为 kTagEmeryNode,在demo中并没有实际的用途,不过这是个好习惯,对调试有帮助。
- 接下来是再熟悉不过的addChild了。
- 给多边形shape描点。num定义点的个数,verts按顺序包含每个点的坐标。首先我们得了解下chipmunk有四种shape:圆形,弧形,多边形,方形。分别有不同的函数来创建这些shape并计算他们的惯性值。我们这里只涉及到多边形。点的坐标是以图片的中心为原点,一个像素为一个单位长度,计算出每个点的坐标值。
- cpBodyNew创建一个刚体,参数如下:
- 刚体质量。
- 刚体惯性值。
- 刚体质量。
- 多边形点数量。
- 多边形点的坐标集。
- 偏移量,会作用到每个点,设为CGPointZero会让事情简单很多。
- 刚体质量。
- 我们让刚体的坐标和精灵的坐标保存一致。chipmunk的坐标值和cocos2d的坐标值是一样的,不用转换,可直接赋值。
- cpSpaceAddBody把刚体放到物理空间中,它将受到重力作用。还记得吗?我已经把重力设为失重状态。
- 我们还需要为刚体定义至少一个shape,cpPolyShapeNew来完成这项工作,参数如下:
- 刚体。
- 多边形点数。
- 多边形点的坐标集。
- 偏移量,会作用到每个点,设为CGPointZero会让事情简单很多。
- shape的collision_type属性,为自定义的一个类型,设为kTagEmeryNode,在设置碰撞回调的时候,我们已经使用了这个值,这里必须设置,否则碰撞回调不会被触发。
- shape的data属性为一个void指针,我们设置为sprite,是为了方便的从shape获取与之对应的精灵。
- cpSpaceAddShape,shape同样需要加入到物理空间中去。
- 最后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,NULL); // The object is dead,don't process the collision further return 0; }
- CP_ARBITER_GET_SHAPES取出是哪两个shape发生了碰撞,a,b是emery还是bullet,与之前设置回调函数时的第2,3参数的顺序有关。
- 我们不能直接在begin函数里面释放刚体或者shape,要等chipmunk做完必要的计算后才能释放。cpSpaceAddPostStepCallback用来安全的完成这个步骤。
- 我们return 0,表示不需要chipmunk处理后面的回调过程了。
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 的效果,那就简陋点吧。