原文地址:http://www.raywenderlich.com/28602/intro-to-box2d-with-cocos2d-2-x-tutorial-bouncing-balls
譯文更新:2013-04-27
更新內容:
- 將body統一譯為剛體
- 將fixture統一譯為夾具
更新日期:2013-01-09
更新內容:完全更新至Cocos2D 2.1-beta4
教程作者:Ray Wenderlich
教程更新:Brian Broom
本教程通過演示一個簡單應用程序的創建過程,幫助您在Cocos2D中使用Box2D。該應用程序顯示一個小球,旋轉iPhone利用加速器能夠讓小球在屏幕上彈來彈去。
游戲截圖如下:
本教程使用的示例程序以iPhoneDev.net上由Kyle編寫的一個非常棒的示例為基礎,更新到最新版本的Cocos2D並對其工作原理做了詳盡的解釋。教程中還會對示例項目中一些由Cocos2D的Box2D應用程序模板提供元素的工作原理逐步進行解釋。
本教程假設您已經學習過《使用Cocos2D 2.X制作一個簡單iPhone游戲教程》,或者具備同等知識。
OK,讓我們開始學習Cocos2D 2.X的Box2D吧!
創建空項目
首先在Xcode中新建一個項目,選擇cocos2d v2.x\cocos2d iOS with Box2d應用程序模板,並將項目命名為Box2D。如果編譯並運行此模板,您將看到一個非常酷的示例展示了Box2D的許多內容。然而,出於本教程的目的,我們准備從零開始創建所有內容,以便我們能夠很好地理解其工作原理。
因此,讓我們先對模板做一下清理,從而使我們有一個良好的起點。使用如下代碼替換HelloWorldLayer.h中的內容:
#import "cocos2d.h" #define PTM_RATIO 32.0 @interface HelloWorldLayer : CCLayer { } + (id) scene; @end
然后使用如下代碼替換HelloWorldLayer.mm的內容:
#import "HelloWorldLayer.h" @implementation HelloWorldLayer + (id)scene { CCScene *scene = [CCScene node]; HelloWorldLayer *layer = [HelloWorldLayer node]; [scene addChild:layer]; return scene; } - (id)init { if ((self=[super init])) { } return self; } @end
再次編譯並運行,您將看到一個空白的屏幕。OK非常好,現在讓我們開始創建自己的Box2D場景吧!
Box2D世界理論
在繼續之前,讓我們先簡單介紹一下在Box2D中是如何工作的。
使用Cocos2D需要做的第一件事情是為Box2D創建一個世界(world)對象。該世界對象是Cocos2D中的主對象,負責管理所有對象和物理仿真。
創建world對象之后,我們需要向世界中添加一些剛體(body)。剛體可以是在游戲中來回移動的對象,如忍者或妖怪,也可以是靜止不動的剛體,如平台或牆壁。
要創建一個剛體,您需要做很多事情——創建一個剛體定義、一個剛體對象、一個形狀、一個夾具定義和一個夾具對象。下面逐一解釋這些讓人抓狂的名詞都分別意味着什么!
- 首先創建一個剛體定義(body definition)用於指定剛體的初始屬性,例如位置或者速度。
- 建立了剛體定義之后,通過指定剛體定義,可以使用世界對象創建一個剛體對象(body object)。
- 然后創建一個希望仿真模擬的幾何形狀(shape)。
- 然后創建一個夾具定義(fixture definition),將夾具定義的形狀設置為您所創建的形狀,並設置其他屬性,例如密度或者摩擦系數。
- 最后,通過指定夾具定義,使用該剛體對象創建一個夾具對象(fixture object)。
- 請注意,可以將任意多個夾具對象添加至單個剛體對象。這一特性在創建復雜對象時非常有用。
將所有需要的剛體添加至世界之后,您只需要周期性地調用Step函數就可以讓Box2D接管工作並開始物理仿真了,因此這會占用一定的處理時間。
但是請注意,Box2D僅僅只更新其內部模型對象的位置,如果想讓Cocos2D的精靈同樣更新至物理仿真的所在位置,您同樣需要周期性地更新精靈的位置。
Ok,現在我們已經對Box2D的工作機制有了基本的認識,接下來讓我們看看在代碼中是如何實現的!
Box2D世界演練
Ok,首先下載我制作的一張小球圖片及其Retina版本,我們准備把這個小球加進場景。下載之后,將它們拖拽至項目中的Resources文件夾,並確保勾選了Copy items into destination group’s folder (if needed)。
接下來,看一下我們此前在HelloWorldLayer.h中添加的這一行代碼:
#define PTM_RATIO 32.0
這行代碼定義了一個像素與“米”之間的比例。當您在Cocos2D中指定剛體放置位置時,需要給定一個單位。雖然您可能會考慮使用像素,但這樣位置是不正確的。根據Box2D參考手冊,Box2D在處理小至0.1單位大至10單位的長度做了優化。按照盡可能長的長度推算,大家通常傾向將其視為“米”,因此0.1差不多是一個杯子大小,而10差不多是一個箱子的大小。
因此,我們不能直接傳遞像素,因為即便是一個很小的對象也差不多會有60×60像素,這已經超出了Box2D優化時限定的最大值。因此,我們需要有一個方法把像素轉換成“米”,於是便就有了上面的比例定義。如果我們有一個64像素的對象,除以PTM_RATIO,可以得到2“米”,這是一個Box2D能夠處理進行物理仿真的長度。
好了,現在可以來點有意思的東西了。在HelloWorldLayer.h的頂部添加如下代碼:
#import "Box2D.h"
在HelloWorldLayer類的接口定義中添加如下成員變量:
b2World *_world; b2Body *_body; CCSprite *_ball;
然后,將如下代碼添加至HelloWorldLayer.mm的init方法:
CGSize winSize = [CCDirector sharedDirector].winSize; // Create sprite and add it to the layer _ball = [CCSprite spriteWithFile:@"ball.png" rect:CGRectMake(0, 0, 52, 52)]; _ball.position = ccp(100, 300); [self addChild:_ball]; // Create a world b2Vec2 gravity = b2Vec2(0.0f, -8.0f); _world = new b2World(gravity); // Create ball body and shape b2BodyDef ballBodyDef; ballBodyDef.type = b2_dynamicBody; ballBodyDef.position.Set(100/PTM_RATIO, 300/PTM_RATIO); ballBodyDef.userData = _ball; _body = _world->CreateBody(&ballBodyDef); b2CircleShape circle; circle.m_radius = 26.0/PTM_RATIO; b2FixtureDef ballShapeDef; ballShapeDef.shape = &circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.2f; ballShapeDef.restitution = 0.8f; _body->CreateFixture(&ballShapeDef); [self schedule:@selector(tick:)];
除了少數幾行通過Cocos2D教程已經熟悉的代碼之外,這里的大部分代碼都很陌生。讓我們一點一點地來解釋。我會一段一段地復述以上代碼,這樣應該會解釋的更清楚一些。
CGSize winSize = [CCDirector sharedDirector].winSize; // Create sprite and add it to the layer _ball = [CCSprite spriteWithFile:@"ball.png" rect:CGRectMake(0, 0, 52, 52)]; _ball.position = ccp(100, 300); [self addChild:_ball];
首先,使用Cocos2D的常規方式將精靈添加至場景。如果您已經學習過之前的Cocos2D教程,這里應該沒有什么問題。
// Create a world b2Vec2 gravity = b2Vec2(0.0f, -8.0f); _world = new b2World(gravity);
接下來,創建世界對象。在創建此對象時,需要指定一個初始重力向量。我們將其設置為延Y軸方向-8的向量,這樣剛體將出現向屏幕底部下落的現象。
// Create ball body and shape b2BodyDef ballBodyDef; ballBodyDef.type = b2_dynamicBody; ballBodyDef.position.Set(100/PTM_RATIO, 300/PTM_RATIO); ballBodyDef.userData = _ball; _body = _world->CreateBody(&ballBodyDef); b2CircleShape circle; circle.m_radius = 26.0/PTM_RATIO; b2FixtureDef ballShapeDef; ballShapeDef.shape = &circle; ballShapeDef.density = 1.0f; ballShapeDef.friction = 0.2f; ballShapeDef.restitution = 0.8f; _body->CreateFixture(&ballShapeDef);
接下來,我們創建小球剛體。
- 將其類型指定為動態剛體(dynamic body)。剛體的默認類型是一個靜態剛體(static body),表示剛體不能移動也不參與仿真。很顯然,我們希望小球參與仿真!
- 把用戶數據(user data)參數設置為小球的CCSprite。可以將剛體上的用戶數據參數設置為任何您所想要的對象,但通常將其設置為精靈會很方便,如此一來您便可以在其他地方訪問到它,例如兩個剛體碰撞時的處理。
- 我們必須定義一個圓形的形狀(shape)。請記住,Box2D不會去查看精靈的圖像,我們必須要告訴它精靈的形狀,這樣它才能夠正確地模擬精靈的運動。
- 最后,設置了一些夾具定義的參數,這些參數的具體含義稍后會介紹。
[self schedule:@selector(tick:)];
方法中最后做的事情是調度一個名為tick的方法被盡可能頻繁地調用。請注意,這並不是最理想的處理方式,比較好的方式是讓tick方法按照一個固定的頻率被調用,例如60次每秒。然而為保證教程內容的簡單,我們先這么處理。
下面,讓我們來編寫tick方法的代碼!在init方法之后添加如下代碼:
- (void)tick:(ccTime) dt { _world->Step(dt, 10, 10); for(b2Body *b = _world->GetBodyList(); b; b=b->GetNext()) { if (b->GetUserData() != NULL) { CCSprite *ballData = (CCSprite *)b->GetUserData(); ballData.position = ccp(b->GetPosition().x * PTM_RATIO, b->GetPosition().y * PTM_RATIO); ballData.rotation = -1 * CC_RADIANS_TO_DEGREES(b->GetAngle()); } } }
方法中我們做的第一件事情是,在世界對象上調用Step函數,使其能夠執行物理仿真。其中兩個參數分別是速度迭代和位置迭代,您通常應該將它們設置為8~10之間的一個值。
接下來的事情是讓精靈與物理仿真匹配。因此我們遍歷世界中的所有剛體,查找設置有用戶數據的剛體。找到之后,將用戶數據轉換成一個精靈(此前是將精靈設置成用戶數據的!),然后更新精靈的位置和角度與物理仿真匹配。
最后一件事情——清理內存!在文件末尾添加如下代碼:
- (void)dealloc { delete _world; _body = NULL; _world = NULL; [_ball release]; _ball = nil; [super dealloc]; }
編譯並運行應用程序,應該能夠看到小球直接從屏幕下方掉出去了。哎呀,我們忘記定義一個地面再把小球彈起來了。
落地反彈
要表示地面,我們在iPhone的屏幕底部創建一個不可見的邊界。按照以下步驟操作即可。
- 創建一個剛體定義(body definition)並指定該剛體應該位於屏幕的左下角。由於剛體類型默認是我們需要的靜態剛體,因此不需要設置。
- 然后使用世界對象創建剛體對象(body object)。
- 然后為屏幕的底邊創建一個邊界形狀(edge shape)。此“形狀”實際上就是一條線。請注意,此處必須使用前面討論過的轉換比例將像素轉換為“米”。
- 創建一個夾具定義(fixture definition)指定邊界形狀。
- 然后使用剛體對象為形狀創建一個夾具對象(fixture object)。
- 另外請注意,一個剛體對象可以包含多個夾具對象!
將如下代碼添加到init方法中創建世界對象和定義小球的代碼之間。
// Create edges around the entire screen b2BodyDef groundBodyDef; groundBodyDef.position.Set(0,0); b2Body *groundBody = _world->CreateBody(&groundBodyDef); b2EdgeShape groundEdge; b2FixtureDef boxShapeDef; boxShapeDef.shape = &groundEdge; //wall definitions groundEdge.Set(b2Vec2(0,0), b2Vec2(winSize.width/PTM_RATIO, 0)); groundBody->CreateFixture(&boxShapeDef);
再次編譯並運行,小球落在地面之后會反彈回空中,往復幾次之后最終靜止停在地面之上。
如何水平運行?
現在我們已經有了基礎的知識,接下來讓我們做一些更有意思的事情——讓一只無形的腳每隔幾秒踢一下球。在HelloWorldLayer.h中定義一個新方法:
- (void)kick;
然后,將該方法的實現添加到HelloWorldLayer.mm:
- (void)kick { b2Vec2 force = b2Vec2(30, 30); _body->ApplyLinearImpulse(force,_body->GetPosition()); }
ApplyLinearImpulse方法可以在小球上作用一個力,使小球移動。移動距離的遠近取決於小球的質量,之前我們在定義小球時曾設置過它的密度(density)屬性。可以嘗試不同的密度以及力的值找出您認為不錯的值。坐標系與Cocos2D相同,X方向向右正向延展,Y方向向上正向延展。
在init方法中增加如下代碼行,每隔5秒運行一次kick方法。
[self schedule:@selector(kick) interval:5.0];
如果現在生成並運行項目,小球被“踢”之后會飛出屏幕。讓我們繼續並定義其他的牆壁。在init方法中找到wall definitions注釋,並在其后添加如下代碼行。注意:每面牆壁需要兩行代碼,一行設置坐標,另一行將邊界添加為ground對象的夾具剛體。
groundEdge.Set(b2Vec2(0,0), b2Vec2(0,winSize.height/PTM_RATIO)); groundBody->CreateFixture(&boxShapeDef); groundEdge.Set(b2Vec2(0, winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO, winSize.height/PTM_RATIO)); groundBody->CreateFixture(&boxShapeDef); groundEdge.Set(b2Vec2(winSize.width/PTM_RATIO, winSize.height/PTM_RATIO), b2Vec2(winSize.width/PTM_RATIO, 0)); groundBody->CreateFixture(&boxShapeDef);
現在生成並運行項目,觀賞小球在屏幕上彈來彈去吧。
集成觸摸
由於HelloWorldLayer仍然是一個Cocos2D圖層,我們可以使用所有的工具,包括曾經學習過的觸摸事件。為了演示如何與Box2D之間交互,讓我們對程序進行一些修改,觸摸屏幕時向左側方向踢球。
要啟用觸摸事件,首先在HelloWorldLayer.mm的init方法中添加如下一行代碼:
[self setTouchEnabled:YES];
然后添加如下方法以處理觸摸事件:
- (void)ccTouchesBegan:(UITouch *)touch withEvent:(UIEvent *)event { b2Vec2 force = b2Vec2(-30, 30); _body->ApplyLinearImpulse(force, _body->GetPosition()); }
與之前類似,我們使用ApplyLinearImpulse方法在小球上作用一個力。給定force的x一個負值將會向左側踢球。
關於仿真的注釋
作為承諾,接下來讓我們介紹一下前文為小球設置的:density、friction和restitution分別有什么用處。
- 密度(Density)是單位體積的質量。因此密度越大質量就越大,移動就越困難。
- 摩擦系數(Friction)是一個系數,用於描述對象表面之間相對滑動的困難程度。其范圍介於0和1之間,0表示沒有摩擦,而1則表示摩擦非常大。
- 恢復系數(Restitution)也是一個系數,用於描述一個對象到底有多“彈”。其范圍通常介於0和1之間,0表示對象不會反彈,而1則表示是完全彈性,也就是說對象會以相同的速度反彈。
可以隨意修改這些數值,看看修改之后會有什么不同的影響。試試看,能不能讓您的小球彈力十足!
收尾
如果我們傾斜屏幕就可以讓小球在屏幕上彈來彈去,會非常酷!有一件事情可以有助於我們接下來的試驗,那就是現在所有的邊界都能正常工作! 剩下的事情就簡單了。在init方法中添加如下代碼:
[self setAccelerometerEnabled:YES];
將如下方法添加在文件中的某一位置:
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { // Landscape left values b2Vec2 gravity(acceleration.y * 30, -acceleration.x * 30); _world->SetGravity(gravity); }
最后,單擊項目導航側邊欄頂部的項目。然后選中TARGETS下的Box2D,並選擇Summary選項卡。在Supported Interface Orientations部分,單擊Landscape Right按鈕取消選中該按鈕。此時Landscape Left按鈕應該是被唯一選中的按鈕。這樣做是因為我們不希望在旋轉手機時iOS改變應用程序的方向。如下圖所示:
我們在這里做的是將用於仿真的重力向量設置為加速器向量的倍數。在設備上編譯並運行應用程序,現在傾斜手機應該能夠讓小球在屏幕上彈來彈去了!
注釋:只有在物理設備上運行程序時,才能夠獲得加速器數據,這需要一個付費的開發者賬號並安裝了開發者證書才可以。詳細信息請參見developer.apple.com的iOS Provisioning Portal。
下一步做些什么?
單擊下載本教程的示例代碼。
如果您希望學習有關Box2D更多的內容,請看下一篇教程How To Create A Breakout Game with Box2D and Cocos2D!
著作權聲明:本文由http://www.cnblogs.com/liufan9翻譯,歡迎轉載分享。請尊重作者勞動,轉載時保留該聲明和作者博客鏈接,謝謝!