~~~~我的生活,我的点点滴滴!!
我们从一个简单的游戏小猪快跑中抽出Box2D代码来讲讲在cocos2dx中使用Box2D,虽然不可能涉及到全部Box2D的应用,但是熟能生巧,
举一反三。还是老规矩,要使用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资源
现在对Box2D有了大概了解,我们开始书写代码:
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; };
撞事件发生或结束,就会回调这两个函数。定义了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); } }
//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;
函数在创建飞猪和子弹对象的时候调用,把飞猪和子弹添加到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; //b2BodyDef中默认初始化为静态物体,所以这里我们不需要特意指定 //不过下面的飞猪与子弹就需要指定了 //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); }
和子弹都不休眠。然后就是创建地面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); } }
拟时不会运动,也不参与碰撞;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); } }
范围在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); } } }
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的边框,方法如下:在cpp-tests(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);
void 在GameScene::draw(Renderer *renderer, const kmMat4 &transform, bool transformUpdated) { // // IMPORTANT: // This is only for debug purposes // It is recommend to disable it // Layer::draw(renderer, transform, transformUpdated); kmGLPushMatrix(); kmGLGetMatrix(KM_GL_MODELVIEW, &_modelViewMV); _customCommand.init(_globalZOrder); _customCommand.func = CC_CALLBACK_0(在GameScene::onDraw, this); renderer->addCommand(&_customCommand); kmGLPopMatrix(); } void 在GameScene::onDraw() { kmMat4 oldMV; kmGLGetMatrix(KM_GL_MODELVIEW, &oldMV); kmGLLoadMatrix(&_modelViewMV); m_world->DrawDebugData(); kmGLLoadMatrix(&oldMV); }