cocos2dx-3.0 中的物理引擎Box2D使用(三)

~~~~我的生活,我的点点滴滴!!



我们从一个简单的游戏小猪快跑中抽出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 _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::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 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::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::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);

运行程序,可以看到子弹跟飞猪的碰撞很精确了,子弹碰到飞猪后就停止了。


cocos2dx-3.0 中的物理引擎Box2D使用(三)_第1张图片


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,飞猪跟子弹的碰撞检测功能基本完成了,精度也完全可以满足这个游戏的需求了,看下图效果:


cocos2dx-3.0 中的物理引擎Box2D使用(三)_第2张图片


你可能感兴趣的:(Box2D)