Cocos2d-x 使用物理引擎进行碰撞检测
【转自】: http://blog.csdn.net/cbbbc/article/details/38541099
通常在游戏简单逻辑判断和模拟真实的物理世界时,我们只需要在定时器中判断游戏中各个精灵的条件是否满足判断条件就可以了。例如,在飞机大战中,判断我方子弹和敌机是否发生碰撞一般在定时器中通过敌机所在位置的矩形是否包括了子弹的位置来判断是否发生碰撞。在消除类游戏中,判断在y轴或x轴上是否要消除相同物品一般在定时器中通过循环来检测在某个方向上是否有连续的相同物品满足消除个数来移除精灵就可以了。
但是要进行复杂的逻辑判断和模拟真实的物理世界时,完全靠自己手写代码来判断工作量太大了。例如,在愤怒的小鸟中,首先,你要模拟小鸟受力后弹出的轨迹路线。然后,你要判断小鸟和箱子是否发生碰撞。最后,假如小鸟和箱子碰撞后,你还要模拟小鸟和箱子往下倒下的轨迹路线。这时候,物理引擎就帮上忙了,它通过我们设定参数来为刚性物体赋予真实的物理属性的方式来计算物体运动、旋转和碰撞。同时,我们只要在回调函数中编写代码就可以处理不同的情况了。
比较出名的游戏引擎有EA DICE的寒霜引擎 、BigWorld公司的BigWorld引擎、Emergent公司的Gamebryo引擎、EPIC公司的虚幻引擎、搜狐畅游公司的黑火引擎、完美世界公司的Athena引擎等。游戏引擎包含以下系统:渲染引擎(即“渲染器”,含二维图像引擎和三维图像引擎)、物理引擎、碰撞检测系统、音效、脚本引擎、电脑动画、人工智能、网络引擎以及场景管理。而对于手机游戏来说,并不需要功能那么多且复杂的引擎,毕竟手机的资源有限不像电脑那么高性能。
因此,Cocos2d-x采用了Box2D物理引擎和Chipmunk物理引擎来模拟真实的物理世界。Box2D几乎能模拟所有的物理效果,而chipmunk则是个更轻量的引擎等。而在Cocos2d-x3.2版本中默认采用Chipmunk,所以我就以Chip-munk来讲解。并且为了简化物理引擎和Cocos2d-x的交接,Cocos2d-x直接提供函数来设置物体参数,不需要我们采用Chip-munk原生的函数来设置,这大大简化的代码的编写。
Chipmunk是采用C语言编写的2D刚体物理仿真库,包含4种基本的对象类型,分别是:
空间:空间是Chipmunk中模拟对象的容器。你将刚体、形状、关节添加进入一个空间,然后将空间作为一个整体进行更新。空间控制着所有的刚体、形状和约束之间的相互作用。
刚体:一个刚体容纳着一个对象的物理属性(如质量、位置、角度、速度等)。默认情况下,它并不具有任何形状,直到你为它添加一个或者多个碰撞形状进去。如果你以前做过物理粒子,你会发现它们的不同之处是刚体可以旋转。在游戏中,通常刚体都是和一个精灵一一对应关联的。你应该构建你的游戏以便可以使用刚体的位置和角度来绘制你的精灵。
碰撞形状:因为形状与刚体相关联,所以你可以为一个刚体定义形状。为了定义一个复杂的形状,你可以给刚体绑定足够多的形状。形状包含着一个对象的表面属性如摩擦力、弹性等。
约束/关节:约束和关节被用来描述刚体之间是如何关联的 人们经常对Chipmunk中的刚体和碰撞形状以及两者与精灵之间的关系产生混淆。精灵是对象的可视化表现,而碰撞形状是定义对象应该如何碰撞的不可见的属性。精灵和碰撞形状两者的位置和角度都是由刚体的运动控制的。通常你应该创建一个游戏对象类型,把这些东西捆绑在一起。
在Cocos2d-x3.2中,物理世界被融入到Scene中,即当创建一个场景时,就可以指定这个场景是否使用物理引擎。 sprite自带body属性,直接设置body,而形状、约束等属性添加到body中即可。碰撞的检测通过事件分发器来监控,所以你首先要创建监听器--EventListenerPhysicsContact,再把其添加到分发器中。
说了那么多铺垫,接下来通过代码来讲解具体使用。
第一步,新建一个工程,并且删除HelloWorld.h/cpp中代码,在HelloWorld.h中添加代码1,它的作用是声明变量。
代码1:
1 Sprite* ball; 2 PhysicsWorld* m_world; 3 void setPhyWorld(PhysicsWorld* world) {m_world=world;}; 4 static cocos2d::Scene* createScene(); 5 virtual bool init(); 6 CREATE_FUNC(HelloWorld);
在代码1中,m_world是用来保存在scene中创建的物理世界,也就是上面说的空间,以备得到空间中的参数,如重力系数等。
第二步,在createScene()中添加代码2,它的作用是创建一个有物体空间的场景。
代码2:
1 //创建有物理空间的场景 2 Scene* scene=Scene::createWithPhysics(); 3 //设置Debug模式,你会看到物体的表面被线条包围,主要为了在调试中更容易地观察 4 scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL); 5 HelloWorld* layer=HelloWorld::create(); 6 //把空间保持我们创建的层中,就是上面所说m_world的作用,方便后面设置空间的参数 7 layer->setPhyWorld(scene->getPhysicsWorld()); 8 scene->addChild(layer); 9 return scene;
第三步,在init()添加代码3。
代码3:
1 if (!Layer::init()) 2 { 3 return false; 4 } 5 6 Size visibleSize = Director::getInstance()->getVisibleSize(); 7 Point origin = Director::getInstance()->getVisibleOrigin(); 8 9 ballOne = Sprite::create("Ball.jpg"); 10 ballOne->setPosition(visibleSize.width / 2, visibleSize.height / 2); 11 //创建物体,并且物体的形状为圆形,第一参数为半径,第二个参数为物体材质 12 //第三个参数为边的厚度,就是在Debug模式下看到的物体外面线条的厚度,默认为0 13 PhysicsBody* ballBodyOne = PhysicsBody::createCircle(ballOne->getContentSize().width / 2, PHYSICSBODY_MATERIAL_DEFAULT); 14 //是否设置物体为静态 15 //ballBody->setDynamic(false); 16 //设置物体的恢复力 17 ballBodyOne->getShape(0)->setRestitution(1.0f); 18 //设置物体的摩擦力 19 ballBodyOne->getShape(0)->setFriction(0.0f); 20 //设置物体密度 21 ballBodyOne->getShape(0)->setDensity(1.0f); 22 //设置质量 23 //ballBodyOne->getShape(0)->setMass(5000); 24 //设置物体是否受重力系数影响 25 ballBodyOne->setGravityEnable(false); 26 27 //设置物体的冲力 28 Vect force = Vect(500000.0f, 500000.0f); 29 ballBodyOne->applyImpulse(force); 30 //把物体添加到精灵中 31 ballOne->setPhysicsBody(ballBodyOne); 32 //设置标志 33 ballOne->setTag(1); 34 this->addChild(ballOne); 35 36 //设置第二个球 37 ballTwo = Sprite::create("Ball.jpg"); 38 ballTwo->setPosition(visibleSize.width / 3, visibleSize.height / 3); 39 PhysicsBody* ballBodyTwo = PhysicsBody::createCircle(ballOne->getContentSize().width / 2, PHYSICSBODY_MATERIAL_DEFAULT); 40 //是否设置物体为静态 41 //ballBodyTwo->setDynamic(false); 42 ballBodyTwo->getShape(0)->setRestitution(1.0f); 43 ballBodyTwo->getShape(0)->setFriction(0.0f); 44 ballBodyTwo->getShape(0)->setDensity(1.0f); 45 46 ballBodyTwo->setGravityEnable(false); 47 48 49 force = Vect(-500000.0f, -500000.0f); 50 ballBodyTwo->applyImpulse(force); 51 ballTwo->setPhysicsBody(ballBodyTwo); 52 ballTwo->setTag(2); 53 this->addChild(ballTwo); 54 55 //创建一个盒子,用来碰撞 56 Sprite* edgeSpace = Sprite::create(); 57 PhysicsBody* boundBody = PhysicsBody::createEdgeBox(visibleSize, PHYSICSBODY_MATERIAL_DEFAULT, 3); 58 boundBody->getShape(0)->setFriction(0.0f); 59 boundBody->getShape(0)->setRestitution(1.0f); 60 61 edgeSpace->setPhysicsBody(boundBody); 62 edgeSpace->setPosition(Point(visibleSize.width / 2, visibleSize.height / 2)); 63 this->addChild(edgeSpace); 64 edgeSpace->setTag(0); 65 66 return true;
在代码3中,我们看到了物体的创建并且创建是就自带形状,然后物体的物理属性。一个物体可以先创建,然后再添加形状。一个物体可以有多个形状,只要不冲突就可以,并且同一个物体多个形状之间不发生碰撞,因为他们属于同一碰撞组。所以我们看到getShape(0)的调用,因为我们添加形状时默认从零开始,上面我们只添加了一个形状。添加形状代码在Cocos2d-x3.2代码为PhysicsBody::addShape()。你可以在...\cocos2d\cocos\physics目录下CCPhy sicsBody.cpp中看到代码4。
代码4:
1 PhysicsShape* PhysicsBody::addShape(PhysicsShape* shape, bool addMassAndMoment/* = true*/) 2 { 3 if (shape == nullptr) return nullptr; 4 // add shape to body 5 if (_shapes.getIndex(shape) == -1) 6 { 7 shape->setBody(this); 8 // calculate the area, mass, and desity 9 // area must update before mass, because the density changes depend on it. 10 if (addMassAndMoment) 11 { 12 _area += shape->getArea(); 13 addMass(shape->getMass()); 14 addMoment(shape->getMoment()); 15 } 16 if (_world != nullptr) 17 { 18 _world->addShape(shape); 19 } 20 _shapes.pushBack(shape); 21 if (_group != CP_NO_GROUP && shape->getGroup() == CP_NO_GROUP) 22 { 23 shape->setGroup(_group); 24 } 25 } 26 return shape; 27 }
代码4为物体在同一组中添加形状,如果你想再进一步追寻下去,可以继续转到不同函数的定义中,最后你会看到Chipmunk原生的代码。
对于不规则的形状,我们可以使用辅助工具--VertexHelper、PhysicsEditor等等来生成点数组来设置形状,在这里,我就使用简单得形状来做示例,如果后面有时间,我也会写一篇使用辅助工具来生成不规则形状的文章。
在代码3中,恢复力和摩擦力要成对使用,即使你把恢复力设置为1.0而不设置摩擦力为0,依旧不是完全弹性碰撞,自己可以修改调试就可以知道正确与否。关于弹性碰撞可以参考一些物理书。对于恢复力,在Chipmunk中文手册有“0.0表示没有弹性,1.0b表示“富有”弹性。然而由于存在模拟误差,不推荐使用1.0或更高的值,碰撞的弹性是由单个形状的弹性相乘得到”。对于摩擦力,在Chipmunk中文手册有“Chipmunk使用的是库仑摩擦力模型,0.0值表示无摩擦。碰撞间的摩擦是由单个形状的摩擦相乘找到”。
在代码3中,冲力的设置为在x轴和y轴上各为500000.0,最后根据物理中合力来计算结果。applyImpulse在离重心相对偏移量为r的位置施加冲量于物体上,默认偏移量为0,你可以在...\cocos2d\cocos\physics目录下CCPhysics-Body.cpp中看到代码5。并且在绝对坐标系中施加力或者冲量,并在绝对坐标系中产生相对的偏移(偏移量相对于重心位置,但不随刚体旋转)。Chipmunk使用的坐标是x轴向右为正,y轴向上为正。关于摩擦力,你可以在...\cocos2d\cocos\physics目录下CCPhysicsShape.cpp中看到代码6。
代码5:
1 void PhysicsBody::applyImpulse(const Vect& impulse) 2 { 3 applyImpulse(impulse, Vec2()); 4 } 5 6 void PhysicsBody::applyImpulse(const Vect& impulse, const Vec2& offset) 7 { 8 cpBodyApplyImpulse(_info->getBody(), PhysicsHelper::point2cpv(impulse), 9 PhysicsHelper::point2cpv(offset)); 10 }
代码6:
1 void PhysicsShape::setFriction(float friction) 2 { 3 _material.friction = friction; 4 5 for (cpShape* shape : _info->getShapes()) 6 { 7 cpShapeSetFriction(shape, PhysicsHelper::float2cpfloat(friction)); 8 } 9 }
在代码5中的cpBodyApplyImpulse为Chipmunk的原生代码。设置冲力时,Vec2()构造函数产生默认值为(0,0)。
在代码6中的cpShapeSetFriction为Chipmunk的原生代码,作用是设置摩擦力。
在代码3,setDensity的作用是设置密度,其实它最后通过乘以面积转变成质量来设置质量的。根据物理公式在2维中密度乘以面积等于质量。所以,你能在...\cocos2d\cocos\physics目录下CCPhysicsShape.cpp中看到代码7。
代码7:
1 void PhysicsShape::setDensity(float density) 2 { 3 if (density < 0) 4 { 5 return; 6 } 7 _material.density = density; 8 if (_material.density == PHYSICS_INFINITY) 9 { 10 setMass(PHYSICS_INFINITY); 11 } 12 else if (_area > 0) 13 { 14 setMass(PhysicsHelper::float2cpfloat(_material.density * _area)); 15 } 16 }
代码7首先是判断密度是否小于0,然后再判断密度是否是无限大,最后才设置密度。 在代码3中,setTag的作用是当发生碰撞时能让我们知道那两个精灵发生碰撞,具体见下文碰撞检测。
在代码3中,setDynamic函数,此作用为设置物体是不是静态刚体。如果你设置了物体为静态,你再设置其他参数,也是没有作用的,原因是静态物体不更新状态。关于静态刚体,Chipmunk中文手册有这么一段,如下:
静态刚体有两个目的。最初,它们被加入用来实现休眠功能。因为静态刚体不移动,Chipmunk知道让那些与静态刚体接触或者连接的物体安全的进入休眠。接触或连接常规游离刚体的物体从不允许休眠。静态刚体的第二个目的就是让Chipmunk知道,关联到静态刚体的碰撞形状是不需要更新碰撞检测数据的。Chipmunk也不需要操心静态物体之间的碰撞检测。通常所有的关卡几何图形都会被关联到一个静态刚体上除了那些能够移动的东西,例如平台或门等。在Chipmunk5.3版本之前,你要创建一个无限大质量的游离刚体,通过`cpSpaceAddStaticShape()`来添加静态形状。现在你不必这样做了,并且如果你想使用休眠功能也不应该这样做了。每一个空间都有一个专用的静态刚体,你可以使用它来添加静态形状。Chipmunk也会自动将形状作为静态形状添加到静态刚体上。
也就是说,如果要让两个物体发生碰撞,至少有一个不为静态,因为两个都为静态,就不用更新数据了。如果两个物体都是静态的,即使你强行用runAction来发生碰撞,两个物体也不会发生碰撞。具体见下文碰撞检测。这个在我调试一个例子中,花了半天时间去看Chipmunk中文手册后才调试出来的。
现在可以看到效果1。
效果1:
上面我们可以看到两个球在一个盒子中弹来弹去。这里我只是给出了简单的例子来说明,没有给出通过关节和约束来把两个物体关联在一起的例子。如果你对两个物体通过关节和约束关联在一起不是很明白的话,可以在打开以下的视频连接--http://v.youku.com/v_show/id_XNjgxNDIyMzky.html来看看。这里我就截个图1来做示例。
图1:
对于要处理相互碰撞事件来说,还要进行以下步骤。这里我们想看到的效果是两球碰撞后,两球消失。
第四步,我们要在onEnter函数中添加碰撞检测监听器并且把监听器添加到事件分发器中,并且添加处理函数。我们要重写onEnetr函数如代码8,添加处理函数如代码9。
代码8:
1 void HelloWorld::onEnter() 2 { 3 Layer::onEnter(); 4 //添加监听器 5 auto contactListener = EventListenerPhysicsContact::create(); 6 //设置监听器的碰撞开始函数 7 contactListener->onContactBegin = CC_CALLBACK_1(HelloWorld::onContactBegin, this); 8 //添加到事件分发器中 9 _eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this); 10 }
代码9:
1 bool HelloWorld::onContactBegin(const PhysicsContact& contact) 2 { 3 Sprite* spriteA = (Sprite*)contact.getShapeA()->getBody()->getNode(); 4 Sprite* spriteB = (Sprite*)contact.getShapeB()->getBody()->getNode(); 5 int tagA = spriteA->getTag(); 6 int tagB = spriteB->getTag(); 7 if (tagA == 1 && tagB == 2 || tagA == 2 && tagB == 1) 8 { 9 spriteA->removeFromParent(); 10 spriteB->removeFromParent(); 11 } 12 return true; 13 }
对于代码8,你是否有这样的疑问--监听器是否可以在其他地方添加,而不是在onEnter函数中。这是可以的,只要在发生碰撞前添加就可以,这里在onEnter添加也只是为了在发生碰撞前添加而已。关于CC_CALLBACK--回调函数的更详细的讲解,你可以阅读这篇文章《浅析schedule和回调函数》。
代码9中,我只给出了碰撞begin函数,Chipmunk一共有4个回调函数,Chipmunk中文手册如下说明:
1.begin():该步中两个形状刚开始第一次接触。回调返回true则会处理正常碰撞,返回false,Chipmunk会完全忽略碰撞。如果返回false,则preSolve()和postSolve()回调将永远不会被执行,但你仍然会在形状停止重叠的时候接收到一个单独的事件。
2.preSolve():该步中两个形状相互接触。回调返回false,Chipmunk在这一步会忽略碰撞,返回true来正常处理它。此外,你可以使用cpArbiterSetFriction(),cpArbiterSetElasticity()或cpArbiterSetSurfaceVelocity()来提供自定义的摩擦,弹性,或表面速度值来覆盖碰撞值。
3.postSolve():两种形状相互接触并且它们的碰撞响应已被处理。如果你想使用它来计算音量或者伤害值,这时你可以检索碰撞冲力或动能。
4.separate():该步中两个形状刚第一次停止接触。确保begin()/separate()总是被成对调用,当删除接触中的形状时或者析构space时它也会被调用。 碰撞回调都与cpArbiter结构紧密相关。你应该熟悉那些为好。
注1:标记为传感器的形状(cpShape.sensor == true)从来不会得到碰撞处理,所以传感器形状和其他形状间永远不会调用postSolve()回调。它们仍然会调用begin()和separate()回调,而preSolve()仍然会在每帧调用回调,即使这里不存在真正的碰撞。 注2:preSolve()回调在休眠算法运行之前被调用。如果一个对象进入休眠状态,postSolve()回调将不会被调用,直到它被唤醒。
上面所说的都是碰撞处理说明,下面代码10就是它们的API。
代码10:
1 typedef int (*cpCollisionBeginFunc)(cpArbiter *arb, struct cpSpace *space, void *data) 2 typedef int (*cpCollisionPreSolveFunc)(cpArbiter *arb, cpSpace *space, void *data) 3 typedef void (*cpCollisionPostSolveFunc)(cpArbiter *arb, cpSpace *space, void *data) 4 typedef void (*cpCollisionSeparateFunc)(cpArbiter *arb, cpSpace *space, void *data)
碰撞处理函数类型。所有这些函数都附带一个arbiter,space和用户data指针,只有begin()和preSolve()回调会返回值。上面带cp开头的函数都是Chipmunk原生函数,如果你想了解得更多,可以阅读Chipmunk中文手册中相关函数的说明。
那Cocos2d-x3.2又是如何和上面那4个函数联系在一起并且把它们封装到哪些函数中去?我第一感觉是Scene::
createWithPhysics()在创建物理空间时就把Chipmunk原生函数关联了事件监听器的函数。因为除了createWithPhysi cs要进行初始化外,其他地方都是对参数的设置和获取。那我开始逐步追寻下去,在createWithPhysics中找到了initWithPhysics,在initWithPhysics中找到了PhysicsWorld::construct(*this),在construct中找到了PhysicsWorld:: init(),好了,在init()终于发现了代码11。代码11的目录为...\cocos2d\cocos\physics\CCPhysicsWorld.cpp。
代码11:
1 bool PhysicsWorld::init(Scene& scene) 2 { 3 do 4 { 5 _info = new PhysicsWorldInfo(); 6 CC_BREAK_IF(_info == nullptr); 7 _scene = &scene; 8 _info->setGravity(_gravity); 9 cpSpaceSetDefaultCollisionHandler(_info->getSpace(), 10 (cpCollisionBeginFunc)PhysicsWorldCallback::collisionBeginCallbackFunc, 11 (cpCollisionPreSolveFunc)PhysicsWorldCallback::collisionPreSolveCallbackFunc, 12 (cpCollisionPostSolveFunc)PhysicsWorldCallback::collisionPostSolveCallbackFunc, 13 (cpCollisionSeparateFunc)PhysicsWorldCallback::collisionSeparateCallbackFunc, 14 this); 15 return true; 16 } while (false); 17 return false; 18 }
在代码11中,cpSpaceSetDefaultCollisionHandler设置回调函数。cpSpaceSetDefaultCollisionHandler声明如代码12。当没有具体的碰撞处理时Chipmunk会使用一个默认的注册碰撞处理函数。space在创建时被指定了一个默认的处理函数,该函数在begin和preSolve回调中返回true,在postSolve()和separate()回调中不做任何事情。
代码12:
1 void cpSpaceSetDefaultCollisionHandler( 2 cpSpace *space, 3 cpCollisionBeginFunc begin, 4 cpCollisionPreSolveFunc preSolve, 5 cpCollisionPostSolveFunc postSolve, 6 cpCollisionSeparateFunc separate, 7 void *data 8 )
好吧,此时我们知道了Cocos2d-x3.2是如何和上面那4个函数联系在一起并且把它们封装到哪些函数中去。因此,
1 contac tListener->onContactBegin对应cpCollisionBeginFunc, 2 3 contactListener->onContactPreSolve对应cpCollisionPreSo lveFunc, 4 5 contactListener->onContactPostSolve对应cpCollisionPostSolveFunc, 6 7 contactListen er->onContactSeperate对应cpCollisionSeparateFunc。
因此,我们只要在物理监听器中设置好回调函数,并把它添加到事件分发器中就可以触发碰撞检测事件了。
现在可以看到效果2。
效果2:
什么?效果2中小球碰撞后怎么不消失,这不是我预想的结果啊!是不是哪里出了问题。我马上就在onContact-Begin中设置断点进行调试,发现两球即使碰撞也没有触发碰撞函数,这不是坑爹吗?就这个问题,一开始我想是不是每帧刷新的速度太快了导致检测不了碰撞,但是从效果2中可以看到明明两球发生了碰撞,结果就是两球受力弹开了
,这个思路明显是错误的。从效果2中可以看出,碰撞确实是发生了,只是没有触发回调函数。
后来上网查阅其他文章和Chipmunk中文手册,发现这是碰撞过滤的问题。从猜想各种各样的原因到发现是碰撞过滤的问题,这其中花费了我很多时间。下面我就来讲讲碰撞过滤。
首先,大家有没有这样的疑问--为什么需要碰撞过滤?玩过游戏的人都知道,自己的队友放出的技能会在敌方身上发生效果,不会作用在自己身上。此时,碰撞过滤就发挥作用了,它使得物体A和物体B会发生碰撞,而物体A和物体C不会发生碰撞,并且决定了发生了碰撞后回调函数是否接收此碰撞事件。由于上面默认碰撞过滤中没有发出通知,所以回调函数收不到两球碰撞的事件。
关于碰撞过滤,Chipmunk中文手册中有如下描述:在空间索引找出彼此靠近的形状对后,将它们传给space,然后再执行一些额外的筛选。在进行任何操作前,Chipmunk会执行几个简单的测试来检测形状是否会发生碰撞。包围盒测试:如果形状的包围盒没有重叠,那么形状便没发生碰撞。对象如对角线线段会引发许多误报,但你不应该担心。层测试:如果形状不在同一层内则不会发生碰撞。(他们的层掩码按位与运算结果为0)群组测试:在相同的非零群组中的形状不会发生碰撞。对应代码在...\cocos2d\external\chipmunk\src\cpSpaceStep.c中,如代码13。
代码13:
1 static inline cpBoolqueryReject(cpShape *a, cpShape *b) 2 { 3 return ( 4 // BBoxes must overlap 5 //形状a和b的包围盒是否重叠 6 !cpBBIntersects(a->bb, b->bb) 7 //形状a和b是否附着在同一物体身上 8 // Don't collide shapes attached to the same body. 9 || a->body == b->body 10 // Don't collide objects in the same non-zero group 11 //形状a和b是否在同一非零群组中 12 || (a->group && a->group == b->group) 13 // Don't collide objects that don't share at least on layer. 14 //形状a和b是否在同一层中 15 || !(a->layers & b->layers) 16 // Don't collide infinite mass objects 17 //形状a和b的质量是否为无限大 18 || (a->body->m == INFINITY && b->body->m == INFINITY) 19 ); 20 }
在代码12中,我给出了中文的翻译注释,如有错误,请见谅。如果代码12返回值为真,则不会发生碰撞。 但是Cocos2d-x3.2封装了Chipmunk的碰撞过滤,方式与Chipmunk原生代码不同。Cocos2d-x3.2有三个碰撞过滤标志categoryBitmask、contactTestBitmask和collisionBitmask。这三个标志在目录...\cocos2d\cocos\physics\CCPhysicsShape.h下有英文的注释,下面是我的翻译和加上我的一些理解。
categoryBitmask:
分类掩码,定义了物体属于哪个分类。场景中的每个物理刚体可以被赋值一个多达32位的值(因为categoryBitmask为int型),每个对应32位掩码中的每一位,你在你的游戏中定义掩码值。结合collisionBitMask和contactTestBitMask属性, 你可以定义哪些物理刚体相互作用并且你的游戏何时接受这些相互作用的通知。默认值为0xFFFFFFFF(所有位都被设置)。
contactTestBitmask:
接触测试掩码,定义哪些刚体分类可以与本刚体产生相互作用的通知。当两个刚体在同一个空间,即物理世界中,每个刚体的分类掩码会和其他刚体的接触测试掩码进行逻辑与的运算。如果任意一个比较结果为非零值,产生一个PhysicsContact对象并且传递到物理世界协议中,这里协议指我们的监听器对应的回调函数。 为了最好的性能,仅设置你感兴趣的接触测试掩码中的位,也就是说通过设置接触测试掩码,你可以决定发生碰撞后,回调函数是否有响应。默认值为0x00000000(所有位都被清除)。
collisionBitmask:
碰撞掩码,定义了哪些物理刚体分类可以和这个物理刚体发生碰撞。当两个物理刚体相互接触时,可能发生碰撞。这个刚体的碰撞掩码和另一个刚体的分类掩码进行逻辑与运算比较。如果结果是一个非零值,这个刚体会发生碰撞。每个刚体独立选择接受与哪个刚体发生碰撞。例如,你可以使用此掩码来忽略那些对于本刚体的速度有影响的刚体碰撞,也就是说你可以使用此掩码使得本刚体与某些刚体碰撞不会对本刚体产生影响。默认值为0xFFFFFFFF(所有位都被设置)。
从上面三个掩码的说明中,我们可以做一个小结。假设刚体A的接触测试掩码和碰撞掩码已知,刚体B的分类掩码决定了能否和A进行碰撞和在碰撞的前提下能否发出PhysicsContact对象触发回调函数。如果B的分类掩码与A的碰撞掩码做逻辑与运算的结果为0,则不会发生碰撞,因此也不会继续和A的接触测试掩码进行逻辑与运算。如果B的分类掩码与A的碰撞掩码做逻辑与运算的结果非0,则发生碰撞,并且B的分类掩码继续与A的接触测试掩码做逻辑与运算,如果结果非0,则发出PhysicsContact对象触发回调函数。
在上面两球碰撞的例子中,由于categoryBitmask的默认值为0xFFFFFFFF,contactTestBitmask的默认值为0x00000000,collisionBitmask默认值为0xFFFFFFFF,所以ballOne的分类掩码与ballTwo的碰撞掩码做逻辑与的结果为0xFFFFFFFF即非0,即可以发生碰撞,ballOne的分类掩码继续与ballTwo的接触测试掩码做逻辑与运算,结果为0,因此不会发出PhysicsContact对象,不会触发回调函数,所以我们上面的onContactBegin不会被触发,所以两球碰撞后不会消失。
现在,我把ballTwo的分类掩码设置为0x0001,碰撞掩码设置为0x0001,把ballTwo的分类掩码设置为0x0010,碰撞掩码设置为0x0010,使用16进制表示只是为了更好观察逻辑与运算的结果,边缘盒子的值保留为默认值。在init()函数设置每个刚体的属性后面添加代码14。
代码14:
1 ballBodyOne->setCategoryBitmask(0x0001); 2 ballBodyOne->setCollisionBitmask(0x0001); 3 ballBodyTwo->setCategoryBitmask(0x0010); 4 ballBodyTwo->setCollisionBitmask(0x0010);
我们可以看到效果3。
效果3:
从效果3中可以到两球不会发生碰撞,会相互穿过对方。因为ballOne的分类掩码和ballTwo的碰撞掩码做逻辑与的结果为0,ballTwo的分类掩码和ballOne的碰撞掩码做逻辑与的结果为0,所以不会发生碰撞。注意,即使ballOne的ballOne的分类掩码和ballTwo的碰撞掩码做逻辑与的结果非0,但是ballTwo的分类掩码和ballOne的碰撞掩码做逻辑与的结果为0,依旧不会发生碰撞的。这正如物理书上所说,作用力和反作用力是相互的。读者可以自己修改代码,观察运行效果。
下面我们再次修改代码如代码15,使得两球可以碰撞,并且碰撞后发出PhysicsContact对象并触发回调函数使得两球消失。
代码15:
1 ballBodyOne->setCategoryBitmask(0x0001); 2 ballBodyOne->setCollisionBitmask(0x0001); 3 ballBodyOne->setContactTestBitmask(0x0001); 4 5 ballBodyTwo->setCategoryBitmask(0x0001); 6 ballBodyTwo->setCollisionBitmask(0x0001); 7 ballBodyTwo->setContactTestBitmask(0x0001);
我们可以看到效果4。
效果4:
从效果4中,我们看到两球相撞后就消失了,终于和我们想要的效果是一样了。因为ballOne的分类掩码和
ballTwo的碰撞掩码做逻辑与的结果为非0,ballTwo的分类掩码和ballOne的碰撞掩码做逻辑与的结果为非0,所以ballOne的分类掩码继续和ballTwo的接触测试掩码做逻辑与运算,结果为非0,ballTwo的分类掩码继续和ballOne的接触测试掩码做逻辑与运算,结果为非0,因此会发出PhysicsContact对象并触发回调函数,在onContactBegin函数中判断两个精灵是否为ballOne和ballTwo,如果是,则移除它们,所以ballOne和ballTwo会消失。注意,即使ballOne的分类掩码继续和ballTwo的接触测试掩码做逻辑与运算,结果为非0,但是ballTwo的分类掩码继续和ballOne的接触测试掩码做逻辑与运算,结果为0,依旧不会发出PhysicsContact对象和触发回调函数。读者可以自己修改代码,观察运行效果。
到此为止,通过上面详细的讲述,使用Cocos2d-x3.2物理引擎进行碰撞检测算正式讲完了。下面我将讨论一个比较趣的例子。
大家有没有想过一个精灵设置了刚体后,再使用runAction来运行动作?如果像上面例子中的又受到冲力又运行动作,小球的运行轨迹,我都不知道怎么样分析,如果谁知道,请告诉我一下。下面的例子的小球是没有受到冲力的。
我们修改代码,如代码16。
代码16:
1 //ballBodyOne->applyImpulse(force); 2 ballOne->runAction(RepeatForever::create( 3 Sequence::create(MoveTo::create(1.0f, Vec2(300, 200)), 4 MoveTo::create(1.0f, Vec2(300, 400)), 5 MoveTo::create(1.0f, Vec2(200, 400)), 6 MoveTo::create(1.0f, Vec2(200, 200)), 7 NULL))); 8 9 //ballBodyTwo->applyImpulse(force); 10 ballTwo->runAction(RepeatForever::create( 11 Sequence::create(MoveTo::create(1.0f, Vec2(100, 400)), 12 MoveTo::create(1.0f, Vec2(700, 400)), 13 NULL)));
此时,我们编译运行,可以看到效果5。
从效果5中,我们可以看到精灵运行动作也是可以使小球相互碰撞并且触发回调函数。到底精灵的runAction是如何和物理碰撞关联在一起的,之间是如何相互影响的?目前,我也不太清楚,如果谁知道,请告诉我一下。
现在我强行用runAction使得小球运动。什么是强行使小球运动?你是否还记得我在前面说过setDynamic函数的作用?默认情况下,刚体是为动态的。现在我把两个小球都设置为静态刚体,即setDynamic(false)。由于,静态刚体是在空间中不会更新状态,但是我用精灵的runAction使小球运动,所以叫做强行。
我现在在两球设置代码中添加setDynamic(false),运行效果如效果6。
效果6:
从效果6中,我们看到两球没有发生碰撞的效果且没有触发回调函数。
以上就是我这周学习物理引擎的总结,真心累,看资料有点累,而到最后写这篇总结时思考要怎么写,要总结些什么就更累了。最后我就以图2来做整个流程的总结。
图2: