由于我们的飞猪并不是一个规则的矩形,所以使用一般的矩形碰撞检测方式精度太低,这里我们使用Box2D来进行碰撞检测,Box2D是一款强大的开源的物理引擎,因为我们不需要物理效果,所以只使用它的碰撞检测功能。cocos2d-x中已经集成了box2d,首先我们要在GameScene.h中添加box2d头文件:

#include "Box2D/Box2D.h"

然后添加链接库,右键项目属性-》链接器-》输入-》附加依赖项里添加libB0x2D.lib

下面来简单了解一下怎么把精灵添加到box2d中和进行碰撞检测的过程:
首先需要创建一个world对象,用来管理物理仿真的所有body对象,我们在创建精灵的时候会把精灵添加到world中,每一个添加到world中的精灵都对应一个body对象,比如这里的飞猪和子弹。body对象根据b2BodyDef结构创建,指定body对象的类型,位置等,需要为body对象创建一个或多个fixture对象,fixture对象根据b2FixtureDef结构创建,指定body对象的形状,密度,顶点坐标等。形状根据b2Shape类创建,这里用到了b2PolygonShape(飞猪:多边形)和b2CircleShape(子弹:圆形),定义多边形时需要指定图片的顶点数组(cocos2d-x默认最多8个顶点,不过在b2Settings.h中可以修改),这个下面会讲。之后需要周期性的调用world对象的step函数,进行物理仿真。我们需要自定义一个监听器,继承自b2ContactListener,用来监听body对象的碰撞和结束碰撞,把碰撞的body对象添加到一个容器中。然后在每一帧中遍历容器,对发生碰撞的精灵进行处理。需要注意的是:box2d只更新它内部body对象的位置,所以我们需要自己更新cocos2d-x中Sprite的Position。整个过程就是:
1. 初始化box2d环境
创建world对象,创建地面盒(在该地面上进行物理仿真,可以理解为指定box2d物理仿真区域边界),指定碰撞监听器。
2. 添加精灵到box2d
3. 每帧遍历监听器中的容器,更新精灵的位置,对碰撞的精灵进行处理。
4. 记得释放box2d资源
OK,现在应该对在cocos2d-x中使用box2d有了一个基本的了解了,开始写代码吧:
首先来实现碰撞监听器,创建MyContactListener类:

struct MyContact {
 b2Fixture *fixtureA;
 b2Fixture *fixtureB;

 bool operator==(const MyContact &other) const{
  return (fixtureA == other.fixtureA) && (fixtureB == other.fixtureB);
 }
};

class MyContactListener : public b2ContactListener {
public:
 MyContactListener();

 ~MyContactListener();

 virtual void BeginContact(b2Contact* contact);

 virtual void EndContact(b2Contact* contact);

 std::vector<MyContact> _contacts;

};

MyContactListener 继承自b2ContactListener ,重写了BeginContact(碰撞开始)和EndContact(碰撞结束)两个函数,当box2d检测到有碰撞事件发生或结束,就会回调这两个函数。定义了MyContact 结构体,用来保存发生碰撞检测的对象 ,定义了_contacts容器,用来保存MyContact对象。 下面看看这两个函数的实现:

void MyContactListener::BeginContact(b2Contact* contact)
{
 MyContact myContact = {contact->GetFixtureA(), contact->GetFixtureB()};
 _contacts.push_back(myContact);
}

void MyContactListener::EndContact(b2Contact* contact)
{
 MyContact myContact = {contact->GetFixtureA(), contact->GetFixtureB()};
 std::vector<MyContact>::iterator it = std::find(_contacts.begin(), _contacts.end(), myContact);
 if(it != _contacts.end()) {
  _contacts.erase(it);
 }
}

这里比较简单,就是碰撞发生时把两个对象添加到_contacts中,碰撞结束后从_contacts中移除。
在GameScene.h中添加:

typedef enum
{
 SPRITE_PLANE = 1,
 SPRITE_BULLET
}SPRITE_TAG;

 void initPhysics();

 void addBoxBodyForSprite(cocos2d::Sprite *sprite);

 void updateBoxBody(float dt);

 b2World *_world;
 MyContactListener *_contactListener;

SPRITE_TAG枚举用来区分子弹和飞猪,在遍历body对象时用得着。initPhysics函数用来做一些box2d的初始化工作,addBoxBodyForSprite函数在创建飞猪和子弹对象的时候调用,把飞猪和子弹添加到box2d的world中去。updateBoxBody函数在游戏的每一帧中都执行,遍历body对象,然后进行碰撞相关处理。
实现initPhysics函数:

void GameScene::initPhysics()
{
 b2Vec2 gravity;
 gravity.Set(0.0f, 0.0f);
 _world = new b2World(gravity);
 _world->SetAllowSleeping(false);
 b2BodyDef groundBodyDef;
 groundBodyDef.position.Set(0, 0);
 b2Body *groundBody = _world->CreateBody(&groundBodyDef);
 b2EdgeShape groundBox;
 //bottom
 groundBox.Set(b2Vec2(VisibleRect::leftBottom().x / PTM_RATIO, VisibleRect::leftBottom().y / PTM_RATIO), b2Vec2(VisibleRect::rightBottom().x / PTM_RATIO, VisibleRect::rightBottom().y / PTM_RATIO));
 groundBody->CreateFixture(&groundBox, 0);
 //right
 groundBox.Set(b2Vec2(VisibleRect::rightBottom().x / PTM_RATIO, VisibleRect::rightBottom().y / PTM_RATIO), b2Vec2(VisibleRect::rightTop().x / PTM_RATIO, VisibleRect::rightTop().y / PTM_RATIO));
 groundBody->CreateFixture(&groundBox, 0);
 //top
 groundBox.Set(b2Vec2(VisibleRect::leftTop().x / PTM_RATIO, VisibleRect::leftTop().y / PTM_RATIO), b2Vec2(VisibleRect::rightTop().x / PTM_RATIO, VisibleRect::rightTop().y / PTM_RATIO));
 groundBody->CreateFixture(&groundBox, 0);
 //left
 groundBox.Set(b2Vec2(VisibleRect::leftBottom().x / PTM_RATIO, VisibleRect::leftBottom().y / PTM_RATIO), b2Vec2(VisibleRect::leftTop().x / PTM_RATIO, VisibleRect::leftTop().y / PTM_RATIO));
 groundBody->CreateFixture(&groundBox, 0);
 _contactListener = new MyContactListener();
 _world->SetContactListener(_contactListener);
}

这里创建了world对象,指定初始重力向量(0,0),因为我们并不想让飞猪和子弹有物理效果。SetAllowSleeping表示没有参与碰撞时让飞机和子弹都不休眠。然后就是创建地面box,指定物理仿真的边界,最后设置碰撞检测的监听器。这里要注意的是PTM_RATIO,表示“像素/米”的比率,因为在box2d中,body的位置使用的单位是米,根据Box2d参考手册,Box2d在处理大小在0.1到10个单元的对象的时候做了一些优化。这里的0.1米大概就是一个杯子那么大,10的话,大概就是一个箱子的大小。VisibleRect是从TestCpp例子中复制过来的。
实现addBoxBodyForSprite函数:

void GameScene::addBoxBodyForSprite(cocos2d::Sprite *sprite)
{
 b2BodyDef bodyDef;
 bodyDef.type = b2_dynamicBody;
 bodyDef.position.Set(sprite->getPositionX() / PTM_RATIO, sprite->getPositionY() / PTM_RATIO);
 bodyDef.userData = sprite;
 b2Body *body = _world->CreateBody(&bodyDef);

 if(sprite->getTag() == SPRITE_PLANE) {
  int num = 5;
  //顶点数组在windows使用PointHelper制作。
  b2Vec2 verts[] = {
   b2Vec2(-10.9f / PTM_RATIO, 24.3f / PTM_RATIO),
   b2Vec2(-25.6f / PTM_RATIO, 0.0f / PTM_RATIO),
   b2Vec2(-1.6f / PTM_RATIO, -24.0f / PTM_RATIO),
   b2Vec2(26.4f / PTM_RATIO, 2.4f / PTM_RATIO),
   b2Vec2(10.4f / PTM_RATIO, 24.8f / PTM_RATIO)
  };
  b2FixtureDef fixtureDef;
  b2PolygonShape spriteShape;
  spriteShape.Set(verts, num);
  fixtureDef.shape = &spriteShape;
  fixtureDef.density = 10.0f;
  fixtureDef.isSensor = true;
  body->CreateFixture(&fixtureDef);
 }else if(sprite->getTag() == SPRITE_BULLET) {
  b2FixtureDef fixtureDef;
  b2CircleShape spriteShape;
  spriteShape.m_radius = 40.0f / PTM_RATIO;
  fixtureDef.shape = &spriteShape;
  fixtureDef.density = 10.0f;
  fixtureDef.isSensor = true;
  body->CreateFixture(&fixtureDef);
 }
}

这里创建body对象,指定type为b2_dynamicBody,一共有三种:b2_staticBody,b2_dynamicBody,b2_kinematicBody。
b2_staticBody在仿真模拟时不会运动,也不参与碰撞;b2_kinematicBody也不参与碰撞,b2_dynamicBody在仿真时可以运动和参与碰撞。
指定飞猪的形状为多边形,子弹形状为圆形,把isSensor设置成true,是希望有碰撞检测但是又不想让它们有碰撞反应。设置飞猪多边形的顶点坐标时是比较麻烦的,需要找一个工具,在mac上有VertexHelper,windows上没有这个工具,我在网上找了个PointHelper,虽然不能直接生成b2Vec2数组,但也很不错了, 效果如下:


接下来实现:

void GameScene::updateBoxBody(float dt)
{
_world->Step(dt, 10, 10); 

 std::vector<b2Body *> toDestroy;

 for(b2Body *body = _world->GetBodyList(); body; body = body->GetNext()) {
  if(body->GetUserData() != NULL) {
   Sprite *sprite = (Sprite*)body->GetUserData();
   b2Vec2 b2Pos = b2Vec2(sprite->getPositionX() / PTM_RATIO, sprite->getPositionY() / PTM_RATIO);
   float b2Angle = -1 * CC_DEGREES_TO_RADIANS(sprite->getRotation());
   body->SetTransform(b2Pos, b2Angle);

   if (sprite->getTag() == SPRITE_BULLET && !_screenRect.containsPoint(sprite->getPosition())) {
    toDestroy.push_back(body);
   }
   }
 }

 std::vector<MyContact>::iterator iter;
 for(iter = _contactListener->_contacts.begin(); iter != _contactListener->_contacts.end(); ++ iter) {
  MyContact contact = *iter;
  b2Body *bodyA = contact.fixtureA->GetBody();
  b2Body *bodyB = contact.fixtureB->GetBody();
  if(bodyA->GetUserData() != NULL && bodyB->GetUserData() != NULL) {
   Sprite *spriteA = (Sprite*)bodyA->GetUserData();
   Sprite *spriteB = (Sprite*)bodyB->GetUserData();

   if(spriteA->getTag() == SPRITE_PLANE && spriteB->getTag() == SPRITE_BULLET) {
    Bullet *bullet = (Bullet*)spriteB;
    bullet->set_is_live(false);
   }else if(spriteB->getTag() == SPRITE_PLANE && spriteA->getTag() == SPRITE_BULLET) {
    Bullet *bullet = (Bullet*)spriteA;
    bullet->set_is_live(false);
 } 
  }
 }

 std::vector<b2Body *>::iterator iter2;
 for(iter2 = toDestroy.begin(); iter2 != toDestroy.end(); ++ iter2) {
  b2Body *body = *iter2;
  if(body->GetUserData() != NULL) {
   Sprite *sprite = (Sprite *)body->GetUserData();
   if(sprite->getTag() == SPRITE_BULLET) {
    _spriteBatch->removeChild(sprite, true);
    _bullets->removeObject(sprite);
   }
  }
  _world->DestroyBody(body);
 }
}

调用world对象的step方法,这样它就可以进行物理仿真了。这里的两个参数分别是“速度迭代次数”和“位置迭代次数”–你应该设置他们的范围在8-10之间,数字越小,精度越小,但是效率更高,数字越大,仿真越精确,但同时耗时更多(8一般是个折中)。首先遍历world中所有body对象,根据body对象更新Sprite对象的Position,把飞出屏幕的子弹对象添加到需要删除的容器中。然后就是遍历_contacts,得到发生碰撞的body对象,这里对飞猪没有做处理,为了看到碰撞效果,给子弹添加了一个_is_live属性,跟飞猪碰撞后,就设置子弹的_is_live属性值为false,子弹就会停止飞行,此时飞猪是无敌状态。最后把飞出屏幕的子弹对象删除,销毁对应的body对象。
修改updateBullet函数,把飞出屏幕外的处理逻辑放到了box2d相关函数中。

void GameScene::updateBullet(float dt) 
{
 Object *bulletObj = NULL;
 CCARRAY_FOREACH(_bullets, bulletObj)
 {
  Bullet *bullet = (Bullet*)bulletObj;
  if(bullet->get_is_live()) {
   Point position = bullet->getPosition();
   Point new_pos = Point(position.x + bullet->get_speed_x(), position.y + bullet->get_speed_y());
   bullet->setPosition(new_pos);
  }
 }
}

在GameScene::init函数中添加:
this->initPhysics();
this->schedule(schedule_selector(GameScene::updateBoxBody));

设置子弹和飞猪精灵的tag,并添加到box2d world中:
_plane->setTag(SPRITE_PLANE);
this->addBoxBodyForSprite(_plane);

this->addBoxBodyForSprite(bullet);
运行程序,可以看到子弹跟飞猪的碰撞很精确了,子弹碰到飞猪后就停止了:

为了看的更清楚,调试更方便,可以
激活 Box2D 的Debug Draw,绘制出子弹body的边框,方法如下:
在TestCpp(TestCpp\Classes\Box2DTestBed)例子目录下找到GLES-Render.h和GLES-Render.cpp两个文件,拷贝到项目中。
在GameScene.h中添加:

#include "GLES-Render.h"

 void draw();
 GLESDebugDraw *_debugDraw;

在GameScene::initPhysics函数最后添加:

  _debugDraw = new GLESDebugDraw(PTM_RATIO); 
 _world->SetDebugDraw(_debugDraw); 
    uint32 flags = b2Draw::e_shapeBit; 
    _debugDraw->SetFlags(flags);

实现draw函数:

void GameScene::draw() 
{ 
 glDisable(GL_TEXTURE_2D); 
 glDisableClientState(GL_COLOR_ARRAY); 
 glDisableClientState(GL_TEXTURE_COORD_ARRAY); 

 _world->DrawDebugData(); 

 glEnable(GL_TEXTURE_2D); 
 glEnableClientState(GL_COLOR_ARRAY); 
 glEnableClientState(GL_TEXTURE_COORD_ARRAY); 
}

运行看看效果:

这里飞猪没有绘制出边框,换成其他静态的飞机图片就可以,不知道是不是顶点坐标的问题,没有去深究了,暂时不知道原因。
ok,飞猪跟子弹的碰撞检测功能基本完成了,精度也完全可以满足这个游戏的需求了。

cocos2d-x手游性能优化总结

近段时间在使用cocos2d-x开发2D手游,技术方案使用的是cocos2d-x+lua,因为游戏使用的是cocos2d-x 2.1.5版本,有些优化方案在最新版的cocos2d-x版本已经实现...

阅读全文

cocos2dx-html5 实现网页版flappy bird游戏

我也是第一次使用cocos2d_html5,对js和html5也不熟,看引擎自带的例子和引擎源码,边学边做,如果使用过cocos2d-x的话,完成这个游戏还是十分简单的。游戏体...

阅读全文

【cocos2d-x开发实战 特训99-终结篇】移植到android平台和添加admob广告

上一篇已经完成特性99在win32平台下的开发,现在把它移植到android上,首先修改Android.mk文件,内容如下: LOCAL_PATH := $(call my-dir) include $(CLEAR_...

阅读全文

3 条评论

  1. 飞猪的轮廓不见了,会不会是因为轮廓太小了,我的PTM_RATIO设置为32,明显比图小了

    1. நீங்கள் என்ன இந்த நாட்டில்தான் இருக்கிறீர்களா? ஏதெனும் சமூக நடப்பை பார்ப்பதுண்டா? அல்லது கேள்விப்படுவதுண்டா? அல்லது வினவு தளத்தையாவது படிப்பதுண்டா? எதையுமே அறà001000®¿à®¯à®¾à®®à®²à¯ பின்னூட்டம் போடுகிறீர்கள் என்றால் நீங்கள் உண்மையிலேயே கில்லாடிதான்.

欢迎留言