1. 程式人生 > >cocos中的Box2d物理引擎

cocos中的Box2d物理引擎

一些Box2d的基本概念,一些cocos中使用box2d需要注意的地方

1. cocos2d 自帶了兩套物理引擎:Box2D 和Chipmunk。

兩套引擎都是為2D遊戲設計的,可以和cocos2d 完美整合。 Box2D 是用 C++寫的,而 Chipmunk 用的是 C。 Box2D中的變數和方法名都是用全稱命名的,Chipmunk中很多地方用的是隻有一個字母的簡寫。有一些功能只有Box2D提供,Chipmunk是沒有的。比如,Box2D有針對快速移動 物體(例如子彈)直接穿透物體而不進行碰撞測試的解決方法。 ...

2. 剛體:

物理物體叫做“剛體”(Rigid Bodies),這是因為物理引擎在驅動這些物體生成動畫時,把它們當作硬的,不會變形的物體。將物體這樣簡化以後,物理引擎就可以同時計算大量的剛體了。 物理引擎中存在兩種剛體:動態移動的(dynamic )和靜態的(static)剛體。

  • 靜態剛體不會移動,也不應該移動- 因為物理引擎可以依賴靜態剛體不會互相碰撞這個特性來做出一些優化。靜態剛體的密度可以被設為0。
  • 動態剛體,它們會相互碰撞,而且也會和靜態剛體碰撞。動態剛體除了擁有位置 (position)和旋轉(rotation )引數,還有至少3個用於定義動態剛體的引數。 它們分別是:
    • 密度或者質量(density or mass)- 用於衡量剛體有多重。
    • 摩擦力(friction )- 用於表示剛體在平面上移動時遇到阻力的大小或者有多滑。
    • 是復原(restitution )- 用於定義剛體的彈性。

3. 碰撞

現實世界裡的物體在運動時都會丟失能量,但是你可以在物理引擎中生成移動碰撞後不會丟失任何能量的動態剛體,甚至讓剛體在與別的剛體發生碰撞彈回來以後,以更快的速度進行移動(設定彈性係數restitution)。

動態和靜態剛體外面都有一個或者多個形體。這個形體用於判斷剛體之間的碰撞的。每一次碰撞會生成一些碰撞點(contact points ) - 兩個相碰撞剛體之間的交叉點。這些碰撞點可被用於播放粒子效果或者在剛體的碰撞處動態地新增刮痕。

物理引擎通過使用力量,脈衝和扭矩生成動態剛體的動畫。非必要情況下不能直接設定剛體的位置和旋轉。如果手動修改物體位置資訊,物理引擎先前所作的某些假設就會失效

4. 關節

可以使用關節(joints )把多個剛體連線起來,這樣可以用不同的方式限制相互連線著的剛體的活動。某些關節可能配備有馬達,比如它們可被用於驅動汽車的輪子或者給關節產生摩擦力,這樣關節在向某個方向移動以後,會試著回到原來的位置。

關節通過使用b2World類的CreateJoint方法來生成。使用剛體的 GetWorld 方法就可以得到b2World。

// 生成旋轉關節 
b2RevoluteJointDef jointDef; 
jointDef.Initialize(bodyA, bodyB, bodyB->GetWorldCenter()); 
bodyA->GetWorld()->CreateJoint(&jointDef); 

柱狀關節:只允許朝一個方向移動,單筒望遠鏡是一個應用柱狀關節的很好的例子。

b2PrismaticJointDef jointDef; 
b2Vec2 worldAxis(0.0f, 1.0f); 
jointDef.Initialize(bodyA, bodyB, body->GetWorldCenter(), worldAxis); 
jointDef.lowerTranslation = 0.0f; 
jointDef.upperTranslation = 0.75f; 
jointDef.enableLimit = true; 
jointDef.maxMoto rForce = 60.0f; 
jointDef.motorSpeed = 20.0f; 
jointDef.enableMotor = false; 
joint = (b2PrismaticJoint*)body- >GetWorld()->CreateJoint(&jointDef); 

把活塞的摩擦力和密度設定為極限值,這樣彈球就不會在碰到活塞的時候彈跳出去了,這可以保證平滑的發射動作。

fixtureDef.friction = 0.99f; 
fixtureDef.restitution = 0.01f; 

5. 物理引擎的侷限性

真實世界過於複雜,完全放到物理引擎中進行模擬是不可能的。這就是為什麼要使用剛體的原因。 在某些極端情況下,物理引擎有可能會捕捉不到某些已經發生的碰撞 – 例如,當剛體以很快的速度移動時,一個剛體可能直接穿透另一個剛體。雖然在量子物理學中這樣的穿透情況會發生。

剛體有時候會相互穿透卡在一起,特別是在使用了關節將它們連線在一起以後。卡在一起的剛體會努力要分開,但是為了滿足關節的連線要求,它們又不得不卡在一起,結果是卡在一起的剛體會產生顫動。

我們也可能碰到遊戲執行的問題。如果我們在遊戲裡使用了很多剛體,你永遠不會知道這些剛體相互作用後的最終結果。最終,有些玩家會把自己卡死在剛體中,或者他們也可能會發現如何利用物理模擬的漏洞,跑到遊戲中他們本來不應該去的區域。

6. Box2D

因為Box2D使用C++寫的,所以必須使用.mm作為所有專案的實現檔案的字尾名(cocos2d中,也可以直接使用純c++來編寫,字尾cpp),而不是通常的.m字尾名。.mm字尾用於告知Xcode把有此後綴名的檔案作為Objective-C++或者C++程式碼來處理。如果你使用了.m字尾,Xcode就會把程式碼以Objective-C和C來處理,Xcode就不能正確處理Box2D的C++程式碼了。因此,如果碰到很多編譯錯誤,首先檢查一下是不是所有的實現檔案都是以.mm作為字尾的。

7. b2World

b2World初始化時使用了一個初始的重力向量值和用於決定是否允許動態剛體“睡眠”的標記變數。 例:對Box2D世界進行初始化

//一個重力為10的世界
b2Vec2 gravity = b2Vec2(0.0f, -10.0f); 
bool allowBodiesToSleep = true; 
world = new b2World(gravity, allowBodiesToSleep); 

會“睡眠”的動態剛體:當施加到某個剛體上的力量小於臨界值一段時間以後,這個剛體將會進入“睡眠”狀態。換句話說,如果某個剛體移動或者旋轉的很慢或者根本不在動的話,物理引擎將會把它標記為“睡眠”狀態,不再對其施加力量,直到新的力量施加到剛體上讓其再次移動或者旋轉。通過把一些剛體標記為“睡眠”狀態,物理引擎可 以省下很多時間。除非你遊戲中的所有動態剛體處於持續的運動中,否則應該把allowBodiesToSleep變數設定為true。

傳入Box2D世界的重力是一個b2Vec2的struct型別。它和CGPoint在本質上是 一樣的,都儲存著x 軸和y 軸的浮點值。和真實世界一樣重力都是一個常量(0,-10)。

8. 剛體b2Body

把活動範圍限制在螢幕之內: 建立一個靜態剛體:

b2BodyDef containerBodyDef; 
b2Body* containerBody = world->CreateBody(&containerBodyDef); 

剛體通常都是使用world的CreateBody方法來生成的,可以確保正確地分配和釋放剛體所佔用的記憶體。預設情況下,一個空的剛體定義會生成一個位於(0,0)位置的靜態剛體。

填充4條邊,以構成空心剛體,螢幕的四個邊需要單獨建立,以便只有四個邊是實心的,中間則是空心的,用於放入其它剛體:

b2PolygonShape screenBoxShape; 
int density = 0; 
// 底部 
screenBoxShape.SetAsEdge(lowerLeftCorner, lowerRightCorner); 
containerBody->CreateFixture(&screenBoxShape, density); 
// 頂部 
screenBoxShape.SetAsEdge(upperLe ftCorner, upperRightCorner); 
containerBody->CreateFixture(&screenBoxShape, density); 
// 左邊 
screenBoxShape.SetAsEdge(upperLeftCorner, lowerLeftCorner); 
containerBody->CreateFixture(&screenBoxShape, density); 
// 右邊 
screenBoxShape.SetAsEdge(upperRi ghtCorner, lowerRightCorner); 
containerBody->CreateFixture(&screenBoxShape, density); 

b2PolygonShape類還有SetAsBox方法,呼叫它會生成的是一個實心的剛體。

使用SetAsBox方法生成的盒子是其接受的引數大小的兩倍,所以提供給這個方法的引數座標值要除以2,或者乘以0.5f

在cocos3.9中,已經刪除掉SetAsEdge函式。可以使用新的b2EdgeShape。

9. 單位

Box2D中距離以米為單位,質量以公斤為單位,時間以秒為單位。

Box2D在0.1米到10米的範圍內工作是最優化的,因為它針對這個範圍做過專門優化,把建立的Box2D世界中的剛體的大小限定在越接近1米越好,太小或者太大的剛體很可能會在遊戲執行過程中產生錯誤和奇怪的行為。所以在cocos中,我們會使用PTM_RATIO巨集來進行轉化,否則cocos預設1畫素=1米。

PTM_RATIO的定義如下:

#define PTM_RATIO 32 

PTM_RATIO用於定義32個畫素在Box2D世界中等同於1米。一個有32畫素寬和高的盒子形狀的剛體等同於1米寬和高的物體。2x32畫素大小的瓷磚的尺寸剛好是1x1米,大小4x4畫素的大小是0.125米x0.125米。

Box2d在處理大小在0.1到10個單元的物件的時候做了一些優化。這裡的0.1米大概就是一個杯子那麼大,10的話,大概就是一個箱子的大小。

螢幕的寬度和高度值除以一個名為 PTM_RATIO的常量,把畫素值轉換成了以米為單位來 計算長度:

Size screenSize = [Director getInstance].winSize; 
float widthInMeters = screenSize.width / PTM_RATIO; 
float heightInMeters = screenSize.height / PTM_RATIO; 
//四個角的座標, 以米為單位: 
b2Vec2 lowerLeftCorner = b2Vec2(0, 0); 
b2Vec2 lowerRightCorner = b2Vec2(widthInMeters, 0); 
b2Vec2 upperLeftCorner = b2Vec2(0, heightInMeters); 
b2Vec2 upperRightCorner = b2Vec2(widthInMeters, heightInMeters) ; 

//在b2Vec2和Point之間轉換:
-(b2Vec2) toMeters:(Point)point 
{ 
    return b2Vec2(point.x / PTM_RATIO, point.y / PTM_RATIO); 
} 
-(Point) toPixels:(b2Vec2)vec 
{ 
    return Point(vec.x, vec.y) * PTM_RATIO; 
} 

10. 生成一個由精靈作為表現的動態剛體示例

-(void) addNewSpriteAt:(CGPoint)pos  
{ 
    CCSpriteBatchNode* batch = (CCSpriteBatchNode*)[self getChildByTag:kTagBatchNode]; 
    int idx = CCRANDOM_0_1() * TILESET_COLUMNS; 
    int idy = CCRANDOM_0_1 () * TILESET_ROWS; 
    CGRect tileRect = CGRectMake(TILESIZE * idx, TILESIZE * idy, TILESIZE, TILESIZE); 
    CCSprite* sprite = [CCSprite sp riteWithBatchNod e:batch rect:tileRect]; 
    sprite.position = pos; 
    [batch addChild:sprite]; 

    //建立一個剛體的定義,並將其設定為動態剛體 
    b2BodyDef  bodyDef; 
    bodyDef.type = b2_dynamicBody; 
    bodyDef.position = [self toMeters:pos]; 
    bodyDef.userData = sprite; 
    b2Body* body = world->CreateBody(&bodyDef); 

    // 定義一個盒子形狀,並將其複製給body fixture 
    b2PolygonShape  dynamicBox; 
    float tileInMeters = TILESIZE / PTM_RATIO; 
    dynamicBox.SetAsBox(tileInMeters  * 0.5f, tileInMe ters * 0.5f); 

    b2FixtureDef fixtureDef; 
    fixtureDef.shape = &dynamicBox; 
    fixtureDef.density = 0.3f; 
    fixtureDef.friction = 0.5f; 
    fixtureDef.restitution = 0.6f; 
    body->CreateFixture(&fixtureDef); 
} 
世界建立剛體,剛體再建立它的附著物。剛體和精靈沒有直接關係,它們只是位置被維持相同罷了

可以把b2FixtureDef 理解成包含著剛體需要用到的所有資料的容器。這些資料包括:剛體的形狀(最重要的一項),密度,摩擦力和復原(這項會影響剛體在world中移動和彈跳的方式)。

11. box2d的動畫

剛體看不見但摸的著,精靈看的見但摸不著

在update方法中遍歷每個剛體,把剛體的userData返回,並且轉換成Sprite指標。把剛體的位置資訊轉換成畫素值,賦值給精靈的位置屬性,讓精靈可以隨著剛體一起移動。同樣地,設定精靈的角度值。

Box2D的world是通過定期地呼叫Step方法來實現動畫的。

Step方法需要三個引數。

第一個是timeStep,它會告訴Box2D自從上次更新以後已經過去多長時間了,直接影響著剛體會在這一步移動多長距離。建議使用delta time來作為timeStep的值,因為delta time會上下浮動,剛體就不能以相同的速度移動了

第二和第三個引數是迭代次數。它們被用於決定物理模擬的精確程度,也決定著計算剛體移動所需要的時間。

示例:更新每個剛體相關聯的精靈的位置和旋轉資訊

-(void) update:(ccTime)delta 
{ 
    //使用固定的時間間隔將物理模擬向前推進一步 
    float timeStep = 0.03f; 
    int32 velocityIterations = 8; 
    int32 positionIterations = 1; 
    world->Step(timeStep, velocityIterati ons, positionIterations); 

    for (b2Body* body = world->GetBodyList(); body != nil; body = body->GetNext()) 
    { 
        CCSprite* sprite = (CCSprite*)body->GetUserData(); 
        if (sprite!= NULL) 
        { 
            sprite.position = [self toPixels:body->GetPosition()]; 
            float angle = body ->GetAngle(); 
            sprite.rotation = CC_RADIAN S_TO_DEGREES(angle) * -1; 
        } 
    } 
} 
12. 碰撞測試

建立一個繼承自b2ContactListener的新類,重寫BeginContact和EndContact這兩個方法,任何時候兩個剛體發生碰撞時都會呼叫這兩個方法。

將它的例項設為world的contact listener:

contactListener = new ContactListener(); 
world->SetContactListener(contactListener); 

在b2ContactListener例項的回撥中不能進行任何更改遊戲物理世界的操作,所以我們可以儲存兩個碰撞物體的引用,然後待會在update中做其它處理。

13. 凸面體

凸面體特點是:在凸面體裡面找任意兩個點,這兩個點連成的線的任何部分都不會落在凸面體外面。

凹面體(Concave)裡任意兩個點連成的線可能有一部分會落在外面。

組成多邊形的頂點在定義是要以反時針方向來進行。

使用VertexHelper工具,通過把頂點一個接一個畫出來的方式來 生成碰撞測試用的多邊形。 VertexHelper的原始碼是通過GitHub來共享的: http://github.com/jfahrenkrug/VertexHelper.,下載下來是一個xcode mac工程,

VertexHelper只是一個幫助尋找凸面體頂點陣列的工具,我們需要的是它生成的多邊形頂點陣列,對於一個物體,可以把它當作一個凸面體,而對於一個地圖,要在其中找到多個凸面體,在VertexHelper中生成的頂點陣列程式碼,可以一段一段的拷貝,當作多個多邊形,只要認為某一段的頂點若連起來後是凸面體就好了。

頂點的位置是相對於body中心的,最大頂點數量不能超過8,數量越大,越費記憶體,效能也越差。

14. 步驟

載入場景,場景中有一個Layer

  • 在Layer中初始化世界:設定邊界,設定監聽
  • 啟用渲染除錯
  • 載入紋理貼圖,*.plist
  • 在Layer中載入背景顏色層 LayerColor
  • 在Layer中新增精靈批處理,SpriteBatchNode,(可以在每個Node中新增同一個紋理貼圖生成的精靈批處理,以方便此node中快速生成sprite)
  • 新增靜態元素層Node,此node中可以新增一些Sprite。
  • 使用world建立剛體,然後建立和剛體對應的sprite新增到精靈批處理,並關聯給剛體的userdata,此處程式碼也可以封裝一下。
  • 在預約的update回撥方法中,遍歷世界中的所有剛體,把精靈的位置和剛體的位置及角度同步。
  • 示例中的BodyNode被新增到了TableSetup中,只是起到了儲存例項引用的作用。

15. 形狀

b2CircleShape shape; 
float radiusInMeters =  (tempSprite.contentS ize.width / PTM_RATIO) * 0.5f; 
shape.m_radius = radiusInMeters; 

將b2BodyDef的angularDamping的域設為0.9f,它會讓球對轉彎的動作產生更大的阻力。這樣彈球在滑過彈球桌面的時候不會產生很多的自身旋轉,這對於用金屬製作的有一定重量的彈球來說是標準的移動方式。

16. Box2D具體是如何運作的。

       建立了world物件,接下來需要往裡面加入一些body物件。body物件可以隨意移動,可以是怪物或者飛鏢什麼的,只要是參與碰撞的遊戲物件都要為之建立一個相應的body物件。當然,也可以建立一些靜態的body物件,用來表示遊戲中的臺階或者牆壁等不可以移動的物體。

       為了建立一個body物件,首先,建立一個body定義結構,然後是body物件,再指定一個shape,再是fixture定義,然後再建立一個fixture物件。下面詳細介紹這個過程:

  • 首先建立一個body定義結構體,用以指定body的初始屬性,比如位置或者速度。
  • 然後呼叫world物件來建立一個body物件。
  • 然後為body物件定義一個shape,用以指定想要模擬的物體的幾何形狀。
  • 接著建立一個fixture定義,同時設定之前建立好的shape為fixture的一個屬性,並且設定其它的屬性,比如質量或者摩擦力。
  • 最後,可以使用body物件來建立fixture物件,通過傳入一個fixture的定義結構就可以了。

       請注意,可以往單個body物件裡面新增很多個fixture物件。這個功能在建立特別複雜的物件的時候非常有用。比如自行車,可能要建立2個輪子,車身等等,這些fixture可以用關節連線起來。

       只要把所有需要建立的body物件都建立好之後,box2d接下來就會接管工作,並且高效地進行物理模擬—只要你週期性地呼叫world物件的step函式就可以了。

       但是,請注意,box2d僅僅是更新它內部模型物件的位置–如果想讓cocos2d裡面的sprite的位置也更新,並且和物理模擬中的位置相同的話,那麼你也需要週期性地更新精靈的位置

17. density,friction和restitution引數的意義。

  • Density 就是單位體積的質量(密度)。因此,一個物件的密度越大,那麼它就有更多的質量,當然就會越難以移動.
  • Friction 就是摩擦力。它的範圍是0-1.0, 0意味著沒有摩擦,1代表最大摩擦,幾乎移不動的摩擦。
  • Restitution 回覆力。它的範圍也是0到1.0. 0意味著物件碰撞之後不會反彈,1意味著是完全彈性碰撞,會以同樣的速度反彈。