提要
今天要學習的是cocos2dx中的Box2d物理引擎。
Box2D是一款開元的C++編寫的物理引擎,起初是用於Flash的遊戲開發中,隨後推出了C++版本,其開發和升級工作一直非常活躍,幾乎成了2D遊戲產品的必備物理引擎。Box2D引擎為開發人員提供了一個二維剛體的物理類比庫,藉助Box2D的強大功能,遊戲中的動畫可以更加真實,讓遊戲世界更具互動性。
Box2D物理引擎所建立的物理引擎具有下面的三個特點:
1.物理世界存在一個重力場;
2.物理世界可以是一個存在邊界的範圍(有碰撞效果);
3.在世界中加入靜態和動態物體,符合現實的運動規律;
今天要學習的內容還是以執行個體為主。有不會的細節留言或者自行google。
最簡單的Box2D的例子
先看最終效果
這個例子的實現思路如下:
1.定義物理世界,定義地面;
2.響應觸控操作,在點擊的位置建立一個物理世界中的正方塊,並用debugdraw顯示;
3.將帶紋理的精靈和物理世界中的正方塊相串連;
關於DebugDraw
當開發人員使用物理引擎開發時,需要將物理世界繪製出來,但是我們都知道物理引擎中只存在碰撞檢測和物理類比,沒有任何繪製功能,為了方便調試,就加入了調試繪圖功能,這個繪圖都是一些先狂,並不能作為遊戲的畫面,只是為了方便開發人員,看到物體世界的樣子。
利用DebugDraw,開發人員可以輕易地看到物體的形狀,關節,用於連續碰撞的核心形狀,AABB包圍盒,接觸,質心。
還是按前面兩篇教程那樣建立工程,然後將cocos2d-x-2.2/samples/Cpp/TestCpp/Classes/Box2DTestBed/GLES-Render.h和cocos2d-x-2.2/samples/Cpp/TestCpp/Classes/Box2DTestBed/GLES-Render.cpp複製到工程的Classes檔案夾中,修改pro.linux 的MakeFile和pro.android 的Android.mk,將檔案包含進去,接下來是具體的代碼實現。
HelloWorld.h
#ifndef __HELLOWORLD_SCENE_H__#define __HELLOWORLD_SCENE_H__#include "cocos2d.h"#include "Box2D/Box2D.h"#include "GLES-Render.h"using namespace cocos2d;class HelloWorld : public cocos2d::CCLayer{private: b2World *world; GLESDebugDraw * m_debugDraw; CCTexture2D* m_pSpriteTexture;public: HelloWorld(); ~HelloWorld(); // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone virtual bool init(); void initPhysics(); virtual void draw(); void addNewSpriteAtPosition(CCPoint p); void update(float dt); virtual void ccTouchesEnded(CCSet* touches, CCEvent* event); // there's no 'id' in cpp, so we recommend returning the class instance pointer static cocos2d::CCScene* scene(); // a selector callback void menuCloseCallback(CCObject* pSender); // implement the "static node()" method manually CREATE_FUNC(HelloWorld);};#endif // __HELLOWORLD_SCENE_H__
private成員裡面,world是一個b2World,Box2D程式都是從建立一個b2World對象開始的。b2World就像一個管理記憶體,物體以及類比的物理樞紐。你可以在堆,棧或者資料區段上建立物理世界。m_debugDraw用於調試繪製,m_pSpriteTexture是一個2d紋理,在繪製Spirit的時候會用到。
public的函數裡面,有三個函數要注意,update是更新物體的位置,ccTouchesEnded是在觸控結束,也就是手指離開螢幕時觸發。addNewSpiriteAtPosition是在某個點添加一個Spirite。函數實現如下:
HelloWorld.cpp
#include "HelloWorldScene.h"USING_NS_CC;#define PTM_RATIO 32enum { kTagParentNode = 1,};HelloWorld::HelloWorld(){ setTouchEnabled( true ); setAccelerometerEnabled( true ); this->initPhysics(); m_pSpriteTexture = CCTextureCache::sharedTextureCache()->addImage("blocks.png"); scheduleUpdate();}HelloWorld::~HelloWorld(){ CC_SAFE_DELETE(world); world = NULL; delete m_debugDraw;}CCScene* HelloWorld::scene(){ // 'scene' is an autorelease object CCScene *scene = CCScene::create(); // 'layer' is an autorelease object HelloWorld *layer = HelloWorld::create(); // add layer as a child to scene scene->addChild(layer); // return the scene return scene;}// on "init" you need to initialize your instancebool HelloWorld::init(){ ////////////////////////////// // 1. super init first if ( !CCLayer::init() ) { return false; } CCSize visibleSize = CCDirector::sharedDirector()->getVisibleSize(); CCPoint origin = CCDirector::sharedDirector()->getVisibleOrigin(); ///////////////////////////// // 2. add a menu item with "X" image, which is clicked to quit the program // you may modify it. // add a "close" icon to exit the progress. it's an autorelease object CCMenuItemImage *pCloseItem = CCMenuItemImage::create( "CloseNormal.png", "CloseSelected.png", this, menu_selector(HelloWorld::menuCloseCallback)); pCloseItem->setPosition(ccp(origin.x + visibleSize.width - pCloseItem->getContentSize().width/2 , origin.y + pCloseItem->getContentSize().height/2)); // create menu, it's an autorelease object CCMenu* pMenu = CCMenu::create(pCloseItem, NULL); pMenu->setPosition(CCPointZero); this->addChild(pMenu, 1); ///////////////////////////// // 3. add your codes below... // add a label shows "Hello World" // create and initialize a label CCLabelTTF* pLabel = CCLabelTTF::create("Box2d Test", "Arial", 24); // position the label on the center of the screen pLabel->setPosition(ccp(origin.x + visibleSize.width/2, origin.y + visibleSize.height - pLabel->getContentSize().height)); // add the label as a child to this layer this->addChild(pLabel, 1); setTouchEnabled( true ); setAccelerometerEnabled( true ); // init physics this->initPhysics(); return true;}void HelloWorld::initPhysics(){ CCLOG("Init physics!"); b2Vec2 gravity; gravity.Set(0.0f, -10.0f); world = new b2World(gravity); // Do we want to let bodies sleep? world->SetAllowSleeping(true); world->SetContinuousPhysics(true); m_debugDraw = new GLESDebugDraw( PTM_RATIO ); world->SetDebugDraw(m_debugDraw); uint32 flags = 0; flags += b2Draw::e_shapeBit; m_debugDraw->SetFlags(flags); // Define the ground body. b2BodyDef groundBodyDef; groundBodyDef.position.Set(0, 0); // bottom-left corner CCSize screenSize = CCDirector::sharedDirector()->getVisibleSize(); // Call the body factory which allocates memory for the ground body // from a pool and creates the ground box shape (also from a pool). // The body is also added to the world. b2Body* groundBody = world->CreateBody(&groundBodyDef); // Define the ground box shape. b2PolygonShape groundBox; groundBox.SetAsBox(screenSize.width, 1.0f); groundBody->CreateFixture(&groundBox, 0.0f);}void HelloWorld::menuCloseCallback(CCObject* pSender){#if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT) || (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) CCMessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert");#else CCDirector::sharedDirector()->end();#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) exit(0);#endif#endif}void HelloWorld::update(float dt){ int velocityIterations = 8; int positionIterations = 1; world->Step(dt, velocityIterations, positionIterations); for (b2Body* b = world->GetBodyList(); b; b = b->GetNext()) { if (b->GetUserData() != NULL) { CCSprite* myActor = (CCSprite*)b->GetUserData(); //擷取精靈 myActor->setPosition( CCPointMake( b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO) ); myActor->setRotation( -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()) ); } }}void HelloWorld::draw(){ // This is only for debug purposes // It is recommend to disable it CCLayer::draw(); ccGLEnableVertexAttribs( kCCVertexAttribFlag_Position ); kmGLPushMatrix(); world->DrawDebugData(); kmGLPopMatrix();}void HelloWorld::ccTouchesEnded(CCSet* touches, CCEvent* event){ //Add a new body/atlas sprite at the touched location CCSetIterator it; CCTouch* touch; for( it = touches->begin(); it != touches->end(); it++) { touch = (CCTouch*)(*it); if(!touch) break; CCPoint location = touch->getLocation(); addNewSpriteAtPosition( location ); }}void HelloWorld::addNewSpriteAtPosition(CCPoint p){ CCLOG("Add sprite %0.2f x %02.f",p.x,p.y); // Define the dynamic body. //Set up a 1m squared box in the physics world b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO); b2Body *body = world->CreateBody(&bodyDef); // Define another box shape for our dynamic body. b2PolygonShape dynamicBox; dynamicBox.SetAsBox(.5f, .5f);//These are mid points for our 1m box // Define the dynamic body fixture. b2FixtureDef fixtureDef; fixtureDef.shape = &dynamicBox; fixtureDef.density = 1.0f; fixtureDef.friction = 0.3f; body->CreateFixture(&fixtureDef);// //We have a 64x64 sprite sheet with 4 different 32x32 images. The following code is// //just randomly picking one of the images int idx = (CCRANDOM_0_1() > .5 ? 0:1); int idy = (CCRANDOM_0_1() > .5 ? 0:1); CCSprite *sprite = CCSprite::createWithTexture(m_pSpriteTexture,CCRectMake(32 * idx,32 * idy,32,32)); this->addChild(sprite,1); body->SetUserData(sprite);}
具體解析下addNewSpiriteAtPosition。
CCLOG("Add sprite %0.2f x %02.f",p.x,p.y);
在eclipse終端列印log顯示添加spirit的位置。
b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(p.x/PTM_RATIO, p.y/PTM_RATIO); b2Body *body = world->CreateBody(&bodyDef);
建立一個剛體。首先需要b2BodyDef 剛體的定義(包含著剛體的座標,剛體的類型:動態),而創造一個剛體需要世界來創造。
這裡說一下world裡的座標,world是個相對來說比較真實的世界,這個世界裡剛體用的參數是MKS ,也就是說米/千克/秒 ,而我們精靈用到的是像素,要相互轉換,這裡的PTM_RATIO也就是代表32個像素是一米,
// Define another box shape for our dynamic body. b2PolygonShape dynamicBox; dynamicBox.SetAsBox(.5f, .5f);//These are mid points for our 1m box // Define the dynamic body fixture. b2FixtureDef fixtureDef; fixtureDef.shape = &dynamicBox; fixtureDef.density = 1.0f; fixtureDef.friction = 0.3f; body->CreateFixture(&fixtureDef);
定義一個四邊形,設定它的物理參數。
int idx = (CCRANDOM_0_1() > .5 ? 0:1); int idy = (CCRANDOM_0_1() > .5 ? 0:1); CCSprite *sprite = CCSprite::createWithTexture(m_pSpriteTexture,CCRectMake(32 * idx,32 * idy,32,32)); this->addChild(sprite,1);
通過紋理創造一個物體(注意不說剛體),idx和idy隨機播放0.5和1,因為紋理是64*64的,而正方形是32*32的,需要再螢幕上體現出來,得創造一個精靈,因為剛體並不能具象的在螢幕上表示。
body->SetUserData(sprite);
將剛體和sprite串連起來。
還要注意一下Update裡面的一句:
int velocityIterations = 8; int positionIterations = 1; world->Step(dt, velocityIterations, positionIterations);
執行一個時間步。這個操作中會執行碰撞檢測(collision detection)、整合(integration)、求解約束(constraint solution)。
timeStep 類比的時間量,這不應該是一個變化的量。
velocityIterations 速度的約束求解量。
positionIterations 位置的約束求解量。
Box2D的物理世界通常包括這樣幾個對象
world:一個物理世界,所有的剛體都將存在在這個世界裡面,這個世界以米為距離單位。盡量貼近真實世界的度量。
body:剛體,存在在物理世界的理想物體,比任何物體都硬,不會發生形變。body對應著一個bodyDef(剛體定義),剛體定義指定了剛體的類型(動態、靜態、軌跡運動的)和剛體的位置,world通過剛體定義建立剛體。
fixture:剛體修飾物,描述剛體的一些特徵。fixture對應著fixtureDef(修飾物定義),它將形狀綁定到剛體上,使剛體具有一些表現特徵,如密度、摩擦係數、彈性等等。body通過fixtureDef建立fixture。
shape:一個幾何形狀,比如圓和多邊形。形狀是修飾物fixture的一個屬性,描述了剛體的碰撞邊界。
解釋一下b2World, b2Body, b2BodyDef, b2Fixture, b2FixtureDef, shpae之間的關係
1.b2World通過b2BodyDef建立b2Body,沒有b2BodyDef,b2Body不知道是什麼類型,放在世界什麼位置。
2.b2Body通過b2FixtureDef建立b2Fixture,沒有b2Fixture,b2Body不知道是什麼形狀,摩擦、彈性、密度都不知道。shpae提供了碰撞檢測的外邊框。
學習小節 好不容易,Android遊戲開發十日通系列也可以告一段落了. 從當初學習libgdx到現在的cocos2d-x,我也從A成到了B城。 libgdx是一款很不錯的引擎,也有一些成熟的作品,但和cocos2d-x這個全世界都在用的遊戲引擎相比還是相去太多,特別是文檔的稀缺,無疑增大了學習成本。後面這幾篇的cocos2d-x的學習感覺作為入門的教程還是不錯的,基本美篇都有一個執行個體。 本想在這篇blog裡面實現一個簡單的憤怒的小鳥的demo,本來想把這個系列寫滿10篇,本來想用一個狂拽炫酷的執行個體來作為結尾....可是我已經不是那個可以幸福地決定自己想學什麼就學什麼的少年了。 肯定會繼續學習遊戲編程,可能是Unity,可能是ogre,又或許還是cocos2d-x,或者是將來的cocos3d。到時候肯定還會和電腦面前的你一起快樂地學習、分享。 最後,今天是2014年的第一天,大家都新年快樂咯!
參考Box2D教程1-建立碰撞世界 - http://www.comingx.com/blog-html/1579.html
學習 Box2D 個人筆記(二)b2body - http://blog.csdn.net/adrianous/article/details/8435156
Cocos2d-x遊戲開發技術精解
Cocos2d-x by Example Beginner's Guide