~~~~我的生活,我的点点滴滴!!
我们从一个简单的游戏小猪快跑中抽出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有了大概了解,我们开始书写代码:
1、碰撞监听器
我们要创建碰撞监听器,我们自定义一个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中移除。
2、场景处理
在游戏主场景中添加处理相应代码:
//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;
//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);
}
这里创建了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);
运行程序,可以看到子弹跟飞猪的碰撞很精确了,子弹碰到飞猪后就停止了。
3、Box2D碰撞边框
为了看的更清楚,调试更方便,可以激活 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);
实现draw函数,这是3.0以后新的opengl新的渲染方式:
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);
}
OK,飞猪跟子弹的碰撞检测功能基本完成了,精度也完全可以满足这个游戏的需求了,看下图效果: