(翻譯)介紹Box2D的Cocos2D 2.X教程:彈球


原文地址:http://www.raywenderlich.com/28602/intro-to-box2d-with-cocos2d-2-x-tutorial-bouncing-balls

譯文更新:2013-04-27

更新內容:

  1. body統一譯為剛體
  2. 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翻譯,歡迎轉載分享。請尊重作者勞動,轉載時保留該聲明和作者博客鏈接,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM