转载请注明来源:http://www.jb51.cc/article/p-dxevjywf-ya.html
Box2d物理引擎还提供一个很重要的功能:碰撞检测。如马里奥游戏中,需要检测马里奥与怪物、蘑菇、金币等的碰撞,通过判断不同的碰撞点、碰撞对象做出不同的处理。
我们要在马里奥中实现的碰撞效果如下:
一、理论
Box2d通过设置碰撞监听来处理碰撞,在创建世界后,可以进行设置监听:
_b2World->SetContactListener(this);
设置的监听对象需要继承b2ContactListener类,实现函数
virtual void BeginContact(b2Contact* contact):碰撞开始时的回调函数,一般简单的碰撞检测使用; virtual void EndContact(b2Contact* contact):碰撞发生后的回调函数,一般简单的碰撞检测使用; virtual void PreSolve(b2Contact* contact,const b2Manifold* oldManifold):碰撞求解前的回调函数,求解就是指计算碰撞产生的冲击力,需要计算碰撞冲击力造成的破坏等效果时,需要使用此回调函数; virtual void PostSolve(b2Contact* contact,const b2ContactImpulse* impulse):碰撞求解后的回调函数,需要计算碰撞冲击力造成的破坏等效果时,需要使用此回调函数。
这四个回调方法中,前两个功能有限但使用起来简单,后两个提供的信息量大,但使用起来比较复杂,这个要根据游戏的具体要求而定,如果我们的游戏过程对物理要求不高,仅仅是实现碰撞检测功能,那么我们主要使用BeginContact(b2Contact* contact)这个回调函数就足够了,如果我们要处理碰撞之前和碰撞之后的效果,根据碰撞中产生的相互作用力来计算物理碰撞后的移动,则我们必须好好的利用全部这四个函数,它们联合作用起来,可以模拟出比较真实而复杂的物理碰撞效果。
发生碰撞后,Box2d会自动调用对应函数,开发者进行具体实现。
二、实践
1.规划
- 设置一个调度者,所有碰撞由其进行分发
- 所有需要进行碰撞处理的类,继承自相同的基类,方便调度者处理
- 各自的碰撞事件各自处理
2.碰撞基类实现
其实就是一个虚类,定义碰撞处理函数,后面为了方便处理,还加了一个碰撞方向检测函数
class BaseContactNode : public Node{ public: typedef enum{ DIRECTION_UP = 1,DIRECTION_DOWN = -1,DIRECTION_RIGHT = 2,DIRECTION_LEFT = -2 }DIRECTION; public: //碰撞处理函数 virtual void beginContact(Node*,b2Contact*) = 0; //获取碰撞方向,这里只简单判断上下左右 virtual int getContactDirection(Node* node,b2Contact* contact){ int isReverse = 1; auto manifold = contact->GetManifold(); Node* other = static_cast<Node*>(contact->GetFixtureB()->GetBody()->GetUserData()); //判断碰撞参考者 if((node != other && manifold->type == b2Manifold::e_faceB) || (node == other && manifold->type == b2Manifold::e_faceA)){ isReverse = -1; } int ret = 0; if (manifold->localNormal.y == -1) { ret = DIRECTION_UP ; }else if(manifold->localNormal.y == 1){ ret = DIRECTION_DOWN; }else if(manifold->localNormal.x == 1){ ret = DIRECTION_RIGHT; }else if(manifold->localNormal.x == -1){ ret = DIRECTION_LEFT; } return ret * isReverse; } };简单说一下其中用到的属性,Box2d会将碰撞的大量信息保存下来,包括但不限于
- contact->GetFixtureB()->GetBody()获取碰撞者body
- contact->GetManifold()获取碰撞相关信息,返回的b2Manifold中包括
- localNormal法向量,可以判断受力方向
- points碰撞位置
- type,碰撞参考者类型,可能是相对物体A——e_faceA,或B——e_faceB
- 可以通过contact->GetWorldManifold(&worldManifold)获取碰撞点相对于世界坐标的信息
3.调度者实现
实现很简单,直接分发^_^
void MarioScene::BeginContact(b2Contact *contact){ if (contact && contact->IsTouching()) { auto A = static_cast<Node*>(contact->GetFixtureA()->GetBody()->GetUserData()); auto B = static_cast<Node*>(contact->GetFixtureB()->GetBody()->GetUserData()); auto pBaseBoxSprite = static_cast<BaseContactNode*>(A); if(pBaseBoxSprite){ pBaseBoxSprite->beginContact(B,contact); } pBaseBoxSprite = static_cast<BaseContactNode*>(B); if(pBaseBoxSprite){ pBaseBoxSprite->beginContact(A,contact); } } }
4.碰撞处理
不同对象的碰撞处理不相同,这里拿马里奥和怪物两个做例子。
4.1 马里奥碰撞处理
注意:
下面的代码中可以看到,在碰到问号后,我的逻辑是生成一个蘑菇,本来我是想直接生成,同时添加body到Box2d世界中,但直接在一个Assert上报错了b2Assert(m_world->IsLocked() == false),这是因为在Box2d的碰撞处理中(世界步中in the middle of a time step),会将世界锁定,不允许修改,所以我们只能将状态缓存下来,在后续的update中进行添加
然后直接上代码:
void MarioPlayer::beginContact(Node* node,b2Contact* contact){ int direction =getContactDirection(this,contact); if(direction == DIRECTION_UP){ //受力方向是上,说明已经碰到了地面,可以重新起跳 delStatus(MarioPlayer::STATUS_JUMP); } if (node && node->getTag() == MarioScene::CONTACT_MONSTER) { //如果碰到了怪物 auto monster = static_cast<MarioMonster*>(node); if(direction != DIRECTION_UP && monster->getStatus() != MarioMonster::STATUS_DIED){ //如果不是踩到怪物头上且怪物不是死亡状态,则Game Over MarioScene::die(); }else if(direction == DIRECTION_UP && monster->getStatus() == MarioMonster::STATUS_DIED){ //碰撞死后的乌龟顶端,则乌龟会以更快的速度移动,当然,这样写是因为我当前场景里只有乌龟 if(this->getPosition().x >= monster->getPosition().x){ monster->diedImpluse(DIRECTION_RIGHT); }else{ monster->diedImpluse(DIRECTION_LEFT); } } }else if(node && node->getTag() == MarioScene::CONTACT_BONUS){ //如果碰到问号,则生出一个蘑菇 auto marioScene = static_cast<MarioScene*>(this->getParent()); if(marioScene){ marioScene->addBonusList(node); } } }
4.2 怪物碰撞处理
相对来说比较简单,直接上代码:
void MarioMonster::beginContact(Node* node,b2Contact* contact){ int direction = getContactDirection(this,contact); if(node && node->getTag() == MarioScene::CONTACT_PLAYER){ if(direction == DIRECTION_DOWN){ //如果被马里奥踩到,死亡 die(); } }else if(node && node->getTag() == MarioScene::CONTACT_MONSTER){ //如果被其他怪物碰到,死亡(其他乌龟是可以被用来做炮弹的) die(false); } }
三、结语
至此,马里奥世界中,和怪物等的碰撞交互也实现了,已经很接近真实的马里奥世界,剩余的就是继续丰满,添加不同的怪物、增加不同的马里奥状态如开枪等、设计更大的地图、增加更多的地图原素了。