Box2D物理引擎入門
一、什麼是Box2D
Box2D是一個強大的開源物理遊戲引擎,用來模擬2D剛體物體運動和碰撞,由Erin Catto早在2007年用C++語言開發。
Box2D集成了大量的物理力學和運動學的計算,並將物理模擬過程封裝到類物件中,將對物體的操作,以簡單友好的介面提供給開發者。我們只需要呼叫引擎中相應的物件或函式,就可以模擬現實生活中的加速、減速、拋物線運動、萬有引力、碰撞反彈等等各種真實的物理運動。(引用百度百科)
簡單的說,Box2D就是一個物理剛體模擬庫。
二、如何學習使用Box2D
Box2D是一個獨立的引擎框架,它的作用是幫助遊戲開發者進行一些複雜的物理模擬運算,但是很多情況下它是作為某些遊戲引擎的一個子模組存在的
關於原生Box2D的學習資料,網路上面是多如牛毛,部落格的話在CSDN和部落格園上面都有。書籍方面也有諸如《Box2D物理遊戲程式設計初學者指南》之類的東西。至於這些教程寫的到底好不好,這也只能仁者見仁,智者見智了。(反正我沒看)
我主要是基於bbframework框架開發遊戲應用的時候使用到了這款出色的物理引擎,所以本文就在bbframework上進行介紹。
使用的工具:
Sublime Text
quick-x-player
關於bbframework(簡稱:bb),它是基於quick-cocos2d-x框架的一個再封裝框架,其核心應該可以說是cocos2d-Lua引擎(或者cocos2d-X)。而cocos2d其本身並不支援box2d物理引擎(cocos2d-JS除外),所以目前在lua上的box2d介面都是公司通過lua繫結將原生的C++介面繫結到lua上的。(難免有些API和C++原生不太一樣)
三、基本概念
Box2D物理引擎裡面的所有類名都是以“b2”作為字首的,以下是幾個比較重要的類。
1、世界(b2World)
物理世界只是一個抽象的概念,可以將其理解成是一個盒子,盒子裡面放的是各種個樣的數學模型和物理模型(或者說就是N多的數學公式和物理公式),所有的物理模擬都在這個盒子內完成。
物理世界和cocos2d的渲染世界不同,渲染世界由場景、層和精靈等組成,在遊戲執行時,渲染世界是可以看見(渲染顯示)、可以摸到(繫結觸控事件)真實存在的。而物理世界的一切就跟萬物的靈魂一樣,看不見也摸不著,都是默默在後臺執行的一些資料片段。
萬物都是因為混沌初開,世界形成才存在的。同樣,要使用物理引擎裡面的東西,一切也都要從建立世界開始。程式碼如下:
-- 建立世界
local world = b2World(b2Vec2(0, -9.8))
-- 允許靜止的物體休眠
world:SetAllowSleeping(true)
-- 開啟連續物理檢測,使模擬更加的真實
world:SetContinuousPhysics(true)
建立世界可以通過呼叫b2World(gravity)函式進行建立,該函式的引數gravity是物理世界的重力加速度(g)。在物理學中,加速度是具有大小和方向的向量,所以該引數可以使用二維向量來表示,其資料型別是b2Vec2,建立向量可以直接呼叫b2Vec2(x, y)函式。
由於物理運算經常伴隨著平方、立方、開平方和開立方,甚至是更高次的冪運算,所以其計算量是非常大的,對於效能的消耗也是非常可觀。而遊戲恰恰又非常強調執行的流暢性,所以很多時候當物體處於禁止狀態的時候並不需要實時進行物理運算,這時候就可以將其從物理模擬中暫時的剔除出去,以提高整體的計算效率。呼叫物理世界物件的SetAllowSleeping(isSleep)方法就可以設定世界內的物體是否在禁止的時候休眠,處於休眠狀態的物體將不參與物理運算。
同時,為了物理模擬的更加真實,通常還需要開啟物理世界的連續檢測。呼叫物理世界物件的SetContinuousPhysics(bool)方法便可以設定是否開啟連續檢測。(連續檢測會消耗一定的效能)
世界非常的大,可以說是無邊無際,然而遊戲裝置的螢幕是固定大小的,遊戲的渲染畫面也就那麼大,所以為了保證物理模擬的物體處於可見的畫面中,通常還需要給定一個邊緣,用於表示物理模擬的世界大小,所有的物體都新增到這個邊界裡面。
物理世界裡面的東西都可以看成是由剛體組成的,所以世界的邊界我們也可以建立一個四邊形剛體來表示,關於剛體的建立詳見下文。
2、剛體(b2Body)
首先,要知道什麼是剛體?以《憤怒的小鳥》這款遊戲為例,小鳥在離開彈弓之後的執行狀態完全是根據真實世界的物理效果進行變化的,那麼在物理運算的時候,就需要一個剛體來表示小鳥(但不是小鳥本身),以參與物理運算。所以,剛體就是物理世界裡面要進行物理模擬的物體。
在cocos2d中,你可以簡單的把剛體當成是一個數據物件,這個物件裡面包含了各種各樣用於進行物理運算的資料(比如:質量、位置、旋轉角度等)。
那麼,什麼樣的東西適合在物理世界裡面建立成剛體呢?物理世界簡單的可以包含氣體、液體和固體,Box2D是一個剛體模擬庫,對於氣體和液體的模擬並不是它的職責,所以它適合模擬的東西只剩下固體了,而且是那種在物理模擬中不會發生形變的固體(任何物體都會發生形變,這裡只是一種理想狀態)。
由於剛體是現實世界物體的一個模擬模擬,所以剛體也必須包含一些現實物體的物理屬性,這些屬性可以簡單的稱之為對剛體的描述或者是定義。所以在建立剛體之前,需要先建立該剛體的剛體描述,用來描述剛體的物理屬性。
首先來看下Box2D原生對剛體描述的定義:
b2BodyDef()
{
// 使用者資料
userData = NULL;
// 剛體位置
position.Set(0.0f, 0.0f);
// 剛體角度
angle = 0.0f;
// 剛體線性速度
linearVelocity.Set(0.0f, 0.0f);
// 剛體角速度
angularVelocity = 0.0f;
// 剛體線性阻尼
linearDamping = 0.0f;
// 剛體角度阻尼
angularDamping = 0.0f;
// 剛體是否可以進行休眠
allowSleep = true;
// 剛體初始狀態是否處於喚醒狀態
awake = true;
// 剛體是否固定旋轉角度
fixedRotation = false;
// 剛體是否是一個快速移動的物體,為了防止發生擊穿想象,開啟它會增加處理時間
bullet = false;
// 剛體型別
type = b2_staticBody;
// 剛體是否處於活躍狀態
active = true;
// 剛體所受重力加速度影響的倍數
gravityScale = 1.0f;
}
b2BodyDef是剛體描述的結構體型別,它可以包含以上14種物理資訊。建立b2BodyDef的程式碼如下:
local bodyDef = b2BodyDef()
-- 型別:靜態(b2_staticBody),平臺(b2_kinematicBody),動態(b2_dynamicBody)
bodyDef.type = b2_staticBody
bodyDef.position = b2Vec2(0, 0)
bodyDef.angle = math.rad(0)
-- 使用者資料:儲存使用者的資料,可以是任何型別的資料。一般要求儲存的資料的型別是一致的
bodyDef.userData = nil
bodyDef.angularDamping = 0
bodyDef.linearDamping = 0
bodyDef.fixedRotation = false
呼叫b2BodyDef()函式便可以建立一個剛體描述物件,然後我們可以隨意設定一些描述資訊(不設定的時候,它們都有預設值)。
這裡有注意的是:
1、剛體的型別:剛體型別分為靜態剛體、平臺剛體和動態剛體,對應值分別是:0、1和2。靜態剛體是不受力的作用而進行移動的,用於模擬地面、牆面等禁止的物體;平臺剛體可用於模擬遊戲內的移動平臺等物體,這些物體和地面等幾乎一樣,但是可以進行位置移動等;動態剛體是最常見的,所有會動的物體都建立為動態剛體。
2、剛體的位置:在Box2D中可以使用b2Vec2型別的向量來表示座標點。
3、剛體的角度:在Box2D中,剛體的角度是使用弧度制,並非和cocos2d一樣的角度制。
4、使用者資料:使用者資料用於儲存一些程式設計師想要附加給剛體的資訊,任何資料型別都可以,一般我們用於儲存剛體對應的那個精靈節點物件(CCSprite)。
建立完剛體的描述,就可以通過描述物件告訴物理世界需要建立一個什麼樣子的物體了。建立剛體的程式碼如下:
-- 建立一個剛體物件,根據剛體定義建立
local body = world:CreateBody(bodyDef)
通過呼叫物理世界物件的CreateBody(bodyDef)方法,物理世界就可以根據傳遞進去的bodyDef物件建立一個對應的剛體物件。
3、形狀(b2Shap)
建立好的剛體其實只是一個包含一些物理量的一個質點(有質量但是沒有大小的點),然而現實世界中的物體是有各種各樣的大小和形狀的,所以我們還需要為剛體建立對應的形狀。(物體的碰撞模擬也需要藉助於形狀)
Box2D內建了以下幾種簡單形狀:
- 鏈條(b2ChainShape)
- 圓形(b2CircleShape)
- 邊線(b2EdgeShape)
- 多邊形(b2PolygonShape)
除了以上幾種之外,還可以藉助PhysicsEditor等物理形狀編輯器進行描點來建立更加複雜的形狀。
在上文介紹世界的時候說到需要建立一個四邊形當成物理世界的邊界,那麼這裡可以選擇用四條邊首位相連,圍成一個四邊形。程式碼如下:
local shape1 = b2EdgeShape()
shape1:Set(b2Vec2(0 / 32, 0 / 32), b2Vec2(960 / 32, 0 / 32))
local shape2 = b2EdgeShape()
shape2:Set(b2Vec2(0 / 32, 0 / 32), b2Vec2(0 / 32, 540 / 32))
local shape3 = b2EdgeShape()
shape3:Set(b2Vec2(0 / 32, 540 / 32), b2Vec2(960 / 32, 540 / 32))
local shape4 = b2EdgeShape()
shape4:Set(b2Vec2(960 / 32, 0 / 32), b2Vec2(960 / 32, 540 / 32))
建立b2EdgeShape同樣可以通過呼叫b2EdgeShape()函式來實現,然後呼叫b2EdgeShape物件的Set(fromPoint, toPoint)方法來指定邊線的起點和終點。
這裡我遊戲的設計解析度是 960 X 540 ,然後我建立的是一個和遊戲設計分辨率同等尺寸的四邊形,但是可以看到起點和終點的x、y座標值都被我除以了32,這是因為Box2D使用的度量是以“米”為單位,而cocos2d的座標系是以畫素為單位的,通常設定其轉換比例是1:32,也就是32畫素的距離等價於Box2D中的1米,這樣的模擬效果是比較好的。
4、夾具(b2Fixture)
建立好形狀之後,需要將形狀和對應的剛體進行繫結,這樣剛體才能擁有形狀。b2Fixture類就是用於將形狀繫結到剛體上的,b2Fixture我們可以將其稱為“夾具”或者“材質”。
在建立b2Fixture之前,也需要先建立對應的材質描述物件(b2FixtureDef),設定一些材質資訊。材質描述的定義如下:
b2FixtureDef()
{
// 形狀
shape = NULL;
// 使用者資料
userData = NULL;
// 摩擦係數
friction = 0.2f;
// 恢復係數
restitution = 0.0f;
// 密度
density = 0.0f;
// 是否為感測器
isSensor = false;
}
材質資訊中的形狀和使用者資料請參考上文,這裡就不在贅述了。重點看下以下幾個屬性:
摩擦係數:用於影響剛體的運動,取值通常在區間[0, 1],當然也可以更大。
恢復係數:或者稱之為“彈性係數”,用於剛體碰撞後能量的損失計算。取值通常在區間[0, 1],當然也可以更大。0表示發生非彈性碰撞,1表示發生完全彈性碰撞。
密度:密度通常用於計算剛體的質量,間接的影響剛體的慣性。
是否為感測器:當設定isSensor為true時,剛體發生碰撞的時候並不會發生碰撞響應(反彈),但是會接收到碰撞的訊號,所以該屬性可以理解為感測器。
同樣,根據上面建立好的四條邊來建立四個材質定義物件,程式碼如下:
-- 建立材質描述
local fixtureDef1 = b2FixtureDef()
fixtureDef1.shape = shape1
local fixtureDef2 = b2FixtureDef()
fixtureDef2.shape = shape2
local fixtureDef3 = b2FixtureDef()
fixtureDef3.shape = shape3
local fixtureDef4 = b2FixtureDef()
fixtureDef4.shape = shape4
這裡建立材質描述是呼叫b2FixtureDef()函式來實現,然後設定了描述物件的形狀資訊,其它的資訊全部使用預設的即可。
有了材質描述,接下來就可以建立對應的夾具(材質)了,程式碼如下:
– 建立四個夾具
body:CreateFixture(fixtureDef1)
body:CreateFixture(fixtureDef2)
body:CreateFixture(fixtureDef3)
body:CreateFixture(fixtureDef4)
建立夾具的方法是呼叫剛體的CreateFixture(b2FixtureDef)方法來實現的,並且夾具會見材質上的資訊與該剛體進行繫結,一個剛體可以擁有多個夾具。
四、物理除錯(Debug)
上文說過,物理世界的一切都是看不見的,但是有時候為了方便排錯,可以用其它的方法讓物理模擬變得可見。
比如:我們建立好了一個剛體,我們想要知道剛體對應到cocos2d渲染世界裡面的位置,那麼我們可以在cocos2d渲染世界裡面建立一個lable標籤或者一個sprite精靈,並放到剛體的位置上面,這樣我們就等同於是讓剛體可見了。而對於邊線、圓之類的剛體形狀,我們可以使用一些遊戲引擎的繪圖API在渲染世界內對應的進行繪製,這樣形狀也可以看到了。很多時候將這些資料進行視覺化會幫助遊戲開發者更好的進行物理排錯。
在bbframework中,可以使用以下程式碼進行物理的視覺化操作:
local debugDraw = GB2DebugDrawLayer:create(world, 32)
self:add(debugDraw, 9999)
GB2DebugDrawLayer這個類專門用於負責物理物件的視覺化模擬,呼叫該類的create(b2World, PTM_RATIO)方法進行構造時,需要傳入物理世界物件和cocos2d與Box2D的度量單位比例(畫素/米)。然後將GB2DebugDrawLayer的例項物件新增到當前場景的Layer上。
這樣我們便可以在渲染世界裡面看到物理模擬的效果了。
物理模擬視覺化
如上圖所示,我們可以看到螢幕的邊緣有紅色或者綠色的邊線,那就是上面建立的世界邊緣的四條邊。
物理編輯器篇(PhysicsEditor)
在Box2D中,除了可以使用內建的幾種基本形狀之外,它還支援從外部檔案中進行材質(b2Fixture)的建立。在建立一些複雜的剛體形狀時,單純的使用多邊形描點是一個比較費勁的工作,這時候藉助PhysicsEditor這款出色的物理材質編輯器來進行視覺化編輯會大大提高效率。
一、形狀編輯
PhysicsEditor主介面
如上圖所示,PhysicsEditor的主介面大致分為四個區域(工具欄、形狀列表、形狀編輯面板和引數編輯面板)。
1、工具欄(ToolsBar)
工具欄位於介面的頂部(如果Windows版本的話,在工具欄上方還有有一行選單欄),工具欄從左往右依次包含以下6個工具項。
- New:新建檔案。
- Open:開啟一個已有的物理材質編輯檔案。
- Save:儲存當前的編輯檔案。
- Publish:匯出配置檔案。
- Publish As:匯出所有。
- Add Sprites:新增精靈。
2、形狀列表(Shapes)
形狀列表的面板預設在主介面的左邊位置,該面板顯示的是檔案編輯的形狀。為了方便編輯形狀,開發者可以直接將要編輯的形狀對應的圖片拖進該面板鬆開。
形狀列表面板
如果要刪除某個形狀,只需要在Shapes面板上面選中要刪除的形狀,然後點選面板右下角的刪除按鈕即可。(圖片的名字會預設為形狀的名稱,以便匯出到檔案最後在程式碼裡面使用,所以一般使用合法識別符號對圖片命名。)
刪除按鈕
3、形狀編輯面板(Editor)
主介面中央是用於編輯形狀的面板,選中Shapes面板上面的形狀後,該面板會顯示對應的圖片,開發者可以根據圖片的輪廓進行描點,編輯出想要的形狀。
形狀編輯面板
形狀編輯面板的頂部是一組形狀編輯工具,從左往右依次是:圓形、多邊形、魔術棒、左右對稱、上下對稱、頂點的x座標值、頂點的y座標值和一個刪除按鈕。
3.1、魔術棒
魔術棒頂點資訊
在形狀列表裡面選擇一個形狀(我這裡的carBody_1),這樣便可以編輯carBody_1的形狀了。先從魔術棒開始吧,點選魔術棒之後,編輯器會彈窗(如上圖)顯示自動描繪的形狀頂點,直接點選右下角的“OK”按鈕。物理編輯器根據圖片的畫素點自動生成了一個多邊形(如下圖)。
魔術棒生成的形狀
3.2、多邊形
雖然編輯器可以使用魔術棒自動生成多邊形,但是這樣形成的多邊形頂點個數太多,會影響物理模擬的效能(計算量增加了),所以對於多邊形,通常建議開發者使用多邊形手動編輯(不用太過精確)。
點選多邊形按鈕,我們可以看到多生成了一個三角形(多邊形預設為三角形),如下圖:
多邊形
新建形狀後,原本使用魔術棒生成的形狀會變灰掉,開發者可以點選灰掉的形狀(形狀變成紅色選中狀態),然後點選編輯面板右上角的刪除按鈕(y值右邊的那個按鈕)去除不要的形狀。
生成的多邊形預設在圖片的左下角位置,開發者可以手動將其拖拽到合適的位置。然後,可以在形狀相應的邊線附近雙擊新增一個頂點(也可以右鍵建立一個頂點)。
編輯好的多邊形
編輯好形狀之後,可以選中一個頂點,然後頂部的x和y後面的文字框就會現實該頂點的座標值,開發者可以手動輸入進行微調(如上圖)。
3.3、圓形
在形狀列表選擇wheel_1,然後點選形狀編輯面板頂部的圓形按鈕,這樣就可以建立一個圓的形狀。圓形在形狀右邊(3點鐘位置)有一個頂點,拖拽它便可以調整圓形的大小。
3.4、形狀映象顯示
如果要把編輯好的形狀變成它的左右翻轉映象或者上下翻轉的映象,只需要點選一下左右對稱或者上下對稱按鈕,這裡就不再贅述(自己玩去,點個按鈕反正很簡單,不想寫,任性–!)。
3.5、縮放編輯面板
由於編輯面板的大小受限,很多時候如果要編輯的圖片比較大,或者想要描點更加精確,開發者可以使用形狀編輯面板底部的縮放功能,預設是原圖大小。
4、引數面板(Parameters)
引數面板分為:Exporter(匯出)、Image parameters(圖片引數)、Body parameters(剛體引數)和Fixture parameters(材質/夾具引數)四個部分。
在沒有建立形狀的時候,引數面板上面的部分引數是無法進行編輯的。
4.1、Exporter
Exporter用於設定匯出的檔案格式,預設為“AndEngine Exporter(XML)”,該選項是一個下拉選擇列表,開發者可以根據使用情景選擇合適的匯出格式。這裡我選擇“Box2D generic(PLIST)”。
4.2、Gloabl parameters(全域性引數)
當Exporter設定為Box2D generic(PLIST)選項後,原本的Body parameters便沒有了,反而多出了一個Gloabl parameters(全域性引數),全域性引數裡面可以設定“PTM-Ratio”(Cocos2D與Box2D的度量單位的一個縮放係數)。
PTM-Ratio預設值是32,通常在Cocos2D中也是使用32,但是這邊需要將其設定為64,否則在建立的時候會發現剛體過大(具體原因我也沒有去深究)。
4.3、Image parameters
在Image parameters中,我們通常也只需要關注“Relative”這一項,其它的都可以不用管(看看就知道其它引數是什麼意思)。
當Exporter設定為Box2D generic(PLIST)選項後,在形狀編輯面板的左下角便會多出一個小圓圈(如下圖)。
質心
這個圓圈表示的是剛體的質心(搓一點的人就把它當成中心點吧)。我們可以手動拖動這個小圓圈到想要設定的位置,通常是圖片的中間,因為Cocos2D的精靈(Sprite)預設的錨點在中心點,這樣設定方便進行物理世界和渲染世界的效果同步。
在拖動小圓圈的時候就會發現,Relative引數的對應的在變化,開發者可以直接在Relative後面的兩個輸入框裡面把值改成0.5,這樣就肯定居中了。
4.4、Fixture parameters
在材質引數這邊一共有7個個引數要設定。
Density:密度,用於設定形狀的密度,間接的影響剛體的質量。
Restitution:恢復係數,控制剛體發生碰撞時,能量的損失。0表示完全非彈性碰撞,1表示完全彈性碰撞。
Friction:摩擦係數,用於模擬形狀的摩擦效果。
Is Sensor:是否為感測器型別,勾選時表示該形狀發生碰撞時可以接收到碰撞資訊,觸發碰撞回撥,但是沒有碰撞的效果。
Id:剛體的id。
Group:碰撞組,用於碰撞檢測,預設為0。
除了上述6個引數之外,在Group下面還有16個碰撞種群的設定,用於進行碰撞篩選:
Bit’s name:種群名稱。
Cat:剛體所在的種群(可以多選),預設選擇第一個。
Mask:與該形狀發生碰撞的種群(可以多選),勾選後即表示當前形狀可以與對應的種群的剛體發生碰撞。
引數設定
二、檔案匯出和儲存
編輯結束後點擊頂部工具欄的“Publish”按鈕,進行匯出,Publish匯出的是plist格式,可以在Cocos2D中進行解析(關於如何使用該檔案將在下一篇關於關節的篇幅中介紹)。
除此之外,我們還需要“CTRL + S”或者點選工具欄的“Save”按鈕將檔案儲存成pes格式的檔案,pes格式的檔案可以使用PhysicsEditor直接開啟,方便後續開發中進行修改。
物理編輯器的使用
在本系列部落格的第一篇就介紹瞭如何使用Box2D內建的幾種方式建立剛體的材質,然而我們在開發遊戲的時候需要根據很多非常複雜的物體建立對應的形狀,使用內建的多邊形很難去描出合適的圖形,所以Box2D物理遊戲引擎支援外部匯入檔案進行物理剛體的建立。
1.1、根據物理編輯器檔案建立物理剛體
首先使用PhysicsEditor物理編輯器將需要建立的材質資訊編輯好,然後匯出cocos2d支援的plist檔案,假設我們將檔案儲存為“boxEdit.plist”並放在資源目錄下的physics資料夾裡面。
建立精靈
-- 建立車身精靈
self.car = D.img("common/cars/car_1/carBody_1.png"):p(480, 270):to(self)
-- 建立車輪精靈
self.w1 = D.img("common/cars/car_1/wheel_1.png"):p(430, 210):to(self)
self.w2 = D.img("common/cars/car_1/wheel_1.png"):p(530, 210):to(self)
接下來就要使用物理編輯器匯出的檔案了:
1、建立剛體
local bodyDef = b2BodyDef()
-- 型別:靜態(b2_staticBody),平臺(b2_kinematicBody),動態(b2_dynamicBody)
bodyDef.type = b2_dynamicBody
bodyDef.position = b2Vec2(self.car.px() / 32, self.car:py() / 32)
bodyDef.angle = math.rad(0)
-- 使用者資料:儲存使用者的資料,可以是任何型別的資料。一般要求儲存的資料的型別是一致的
bodyDef.userData = self.car
-- 建立一個剛體物件,根據剛體定義建立
local body = world:CreateBody(bodyDef)
2、有了剛體,接下來就要建立材質,程式碼如下:
-- 載入physicsDesigner的.plist格式檔案
GB2ShapeCache:sharedGB2ShapeCache():addShapesWithFile("physics/boxEdit.plist")
-- 把生成的剛體和形狀綁在一起,key即圖片名
GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(body, "carBody_1")
可以使用GB2ShapeCache:sharedGB2ShapeCache():addShapesWithFile(“plist檔名路徑”)來載入物理編輯器匯出的資料,然後通過GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(剛體物件, “精靈列表上的形狀名稱”)來給剛體繫結材質資訊。
根據PhysicsEditor編輯的檔案建立剛體材質
重新整理模擬器就可以看到編輯的車身顯示在場景中(要開啟物理除錯模式)。
3、使用同樣的方式建立汽車的兩個車輪
bodyDef.position = b2Vec2(13.5, 8.5)
bodyDef.userData = self.w1
local bodyWheel1 = world:CreateBody(bodyDef)
GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(bodyWheel1, "wheel_1")
bodyDef.position = b2Vec2(16.5, 8.5)
bodyDef.userData = self.w2
local bodyWheel2 = world:CreateBody(bodyDef)
GB2ShapeCache:sharedGB2ShapeCache():addFixturesToBody(bodyWheel2, "wheel_1")
重新整理模擬器,可以看到如下畫面:
根據PhysicsEditor編輯的檔案建立剛體材質
二、物理更新
2.1、物理更新
在這之前,其實物理世界並沒有開始進行物理模擬,那麼我們可以使用下面這個程式碼來進行一次物理更新:
-- 更新[物理世界]
function M.tick()
if not world then return end
-- 時間步、速度迭代、位置迭代
local velocityIterations, positionIterations = 32, 32
-- 物理引擎進行物理模擬,生成模擬後的資料
world:Step(60, velocityIterations, positionIterations)
end
注意:這個函式裡面的“world”就是之前建立的物理世界,所以我們可以將world定義成全域性的,以保證可以在任何地方訪問。
呼叫物理世界的Step()方法可以進行一次物理模擬,方法的第一個引數是時間步,第二個引數是速度迭代引數,第三個引數是位置迭代引數。
- 時間步用於控制物理模擬的時間間隔,我們一般讓其與渲染世界同步,所以設定為60,裝置效能較低時可以設定為40。
- 速度迭代:迭代次數越高,模擬越逼真,但是越耗效能。
- 位置迭代:迭代次數越高,模擬越逼真,但是越耗效能。
2.2、開啟物理模擬
開啟一個排程器來呼叫上面的這個函式,實現實時模擬:
local timer = SC.open(self.tick, 1.0 / 60)
開啟排程進行物理模擬的時候發現車身和車輪開始做只有落地運動,並且它們散架了,這是因為車身和車輪之間沒有連線到一起,為了讓車身和車輪組成一個整體,可以使用關節(約束)將他們連線到一起。
三、關節(b2Joint)
關節又稱之為“約束”,是用於限制剛體運動的一些條件,在box2D中,常用的關節有:滑鼠關節、距離關節、移動關節、旋轉關節等,其實這些關節的中文名稱並非翻譯的很準確,很多諸如《Cocos2d-JS 遊戲開發》的書籍裡面都不進行翻譯,而是直接使用關節的類名來進行講解。各種關節型別都派生自 b2JointDef。所有關節都連線兩個不同的物體,可能其中一個是靜態物體。如果你想浪費記憶體的話,那就建立一個連線兩個靜態物體的關節。可以為任何一種關節指定使用者資料。很多關節定義需要你提供一些幾何資料。一個關節常常需要一個錨點(anchor point)來定義。在 Box2D 中這點需要在區域性座標系中指定,這樣,即便當前物體的變化違反了關節約束,關節還是可以被指定 —— 在遊戲存取進度時這經常會發生。另外,有些關節定義需要預設的物體之間的相對角度。這樣才能通過關節限制或固定的相對角來正確地約束旋轉。初始化幾何資料可能有些乏味。所以很多關節提供了初始化函式,消除了大部分工作。然而,這些初始化函式通常只應用於原型,在產品程式碼中應該直接地定義幾何資料。這能使關節行為更加穩固。
1、關節描述(b2JointDef)
與建立剛體一樣,建立關節也需要先建立關節的描述,在box2D中,關節描述b2JointDef的定義如下:
struct b2JointDef
{
b2JointDef()
{
type = e_unknownJoint;
userData = NULL;
bodyA = NULL;
bodyB = NULL;
collideConnected = false;
}
/// The joint type is set automatically for concrete joint types.
b2JointType type;
/// Use this to attach application specific data to your joints.
void* userData;
/// The first attached body.
b2Body* bodyA;
/// The second attached body.
b2Body* bodyB;
/// Set this flag to true if the attached bodies should collide.
bool collideConnected;
};
b2JointDef是一個結構體(struct)型別,包含如下五個屬性:
- type:關節型別。
- userData:使用者資料(可以存放任意資料)。
- bodyA:被關節約束的第一個剛體物件。
- bodyB:被關節約束的第二個剛體物件。
- collideConnected:被約束的兩個剛體是否可以發生碰撞,true表示可以發生碰撞。
1、滑鼠關節(b2MouseJoint)
1.1、滑鼠關節描述(b2MouseJointDef)
在建立滑鼠關節之前,需要先建立對應的關節描述,滑鼠關節描述b2MouseJointDef的定義如下:
struct b2MouseJointDef : public b2JointDef
{
b2MouseJointDef()
{
type = e_mouseJoint;
target.Set(0.0f, 0.0f);
maxForce = 0.0f;
frequencyHz = 5.0f;
dampingRatio = 0.7f;
}
/// The initial world target point. This is assumed
/// to coincide with the body anchor initially.
b2Vec2 target;
/// The maximum constraint force that can be exerted
/// to move the candidate body. Usually you will express
/// as some multiple of the weight (multiplier * mass * gravity).
float32 maxForce;
/// The response speed.
float32 frequencyHz;
/// The damping ratio. 0 = no damping, 1 = critical damping.
float32 dampingRatio;
};
b2MouseJointDef繼承自b2JointDef,它的type屬性值為“e_mouseJoint”,指明使用該關節定義的關節是一個滑鼠關節。
b2MouseJointDef相比於b2JointDef多出瞭如下幾個屬性:
maxForce:作用在剛體上的最大的力,力越大拖動剛體越輕鬆。
frequencyHz:響應速度,數值越高,關節響應的速度越快,關節看上去越堅固。
dampingRatio:阻尼率:值越大,關節運動阻尼越大。
典型情況下,關節頻率(響應速度)要小於一半的時間步(time step)頻率。比如每秒執行60次時間步, 關節的頻率就要小於30赫茲。這樣做的理由可以參考Nyquist頻率理論。
影響關節的彈性(阻尼率無單位,典型是在0到1之間, 也可以更大。1是阻尼率的臨界值, 當阻尼率為1時,沒有振動。)
1.2、建立滑鼠關節描述
-- 建立一個滑鼠關節定義
local mouseJointDef = b2MouseJointDef()
-- 要被滑鼠關節約束的剛體
mouseJointDef.bodyA = body
-- bodyB設定為世界的邊緣剛體
mouseJointDef.bodyB = edgeBody
mouseJointDef.frequencyHz = 30
mouseJointDef.collideConnected = true
mouseJointDef.maxForce = 100
-- 關節的位置
mouseJointDef.target = b2Vec2(11, 10)
可以使用b2MouseJointDef()函式建立一個滑鼠關節定義,然後直接設定其屬性值。值得注意的是,關節是用於連線兩個剛體的,而滑鼠關節的主要作用是可以使用滑鼠來對剛體進行拖拽操作,作用的是一個剛體,所以我們可以將一個剛體設定為要拖拽的剛體物件,另一個一般設定為世界邊緣剛體。
1.3、建立滑鼠關節
-- 建立關節,並轉換為b2MouseJoint型別
local mouseJoint = tolua.cast(world:CreateJoint(mouseJointDef), "b2MouseJoint")
物理世界物件的CreateJoint(jointDef)方法可以用於建立一個關節,其引數jointDef為要建立的關節對應的關節描述物件,該方法的返回值型別是b2Joint型別,可以通過tolua.cast(obj, typeName)方法來進行型別轉換。
1.4、移動剛體
滑鼠關節移動剛體的現方式是改變滑鼠關節的位置,程式碼如下:
-- 設定關節的位置
mouseJoint:SetTarget(b2Vec2(10, 10))
只需要在觸控事件的移動回撥中改變滑鼠關節的位置即可實現剛體的拖拽。
2、焊接關節(b2WeldJoint)
2.1、焊接關節描述(b2WeldJointDef)
struct b2WeldJointDef : public b2JointDef
{
b2WeldJointDef()
{
type = e_weldJoint;
localAnchorA.Set(0.0f, 0.0f);
localAnchorB.Set(0.0f, 0.0f);
referenceAngle = 0.0f;
frequencyHz = 0.0f;
dampingRatio = 0.0f;
}
/// Initialize the bodies, anchors, and reference angle using a world
/// anchor point.
void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor);
/// The local anchor point relative to bodyA's origin.
b2Vec2 localAnchorA;
/// The local anchor point relative to bodyB's origin.
b2Vec2 localAnchorB;
/// The bodyB angle minus bodyA angle in the reference state (radians).
float32 referenceAngle;
/// The mass-spring-damper frequency in Hertz. Rotation only.
/// Disable softness with a value of 0.
float32 frequencyHz;
/// The damping ratio. 0 = no damping, 1 = critical damping.
float32 dampingRatio;
};
焊接關節的定義如上,其相比與b2JointDef增加的屬性有:
localAnchorA:關節連線在剛體A上的區域性錨點,或者就理解成是關節在剛體A上的連線點。
localAnchorB:關節連線在剛體B上的區域性錨點,或者就理解成是關節在剛體B上的連線點。
referenceAngle:剛體B相對於剛體A的角度,使用弧度製表示。
frequencyHz:響應速度,數值越高,關節響應的速度越快,關節看上去越堅固。
dampingRatio:阻尼率:值越大,關節運動阻尼越大。
2.2、建立焊接關節描述
-- 建立一個焊接關節定義
local weldJointDef = b2WeldJointDef()
-- 初始化
weldJointDef:Initialize(bodyA, bodyB, bodyB:GetWorldCenter())
weldJointDef.frequencyHz = 30
weldJointDef.collideConnected = false
建立焊接關節的時候,設定關節約束的兩個剛體需要通過呼叫焊接關節定義的Initialize()方法,該方法有三個引數,分別是:剛體A、剛體B和關節的錨點位置。
2.3、建立焊接關節
local weldJoint = tolua.cast(world:CreateJoint(weldJointDef), "b2WeldJoint")
3、距離關節(b2DistanceJoint)
3.1、距離關節定義(b2DistanceJointDef)
struct b2DistanceJointDef : public b2JointDef
{
b2DistanceJointDef()
{
type = e_distanceJoint;
localAnchorA.Set(0.0f, 0.0f);
localAnchorB.Set(0.0f, 0.0f);
length = 1.0f;
frequencyHz = 0.0f;
dampingRatio = 0.0f;
}
/// Initialize the bodies, anchors, and length using the world
/// anchors.
void Initialize(b2Body* bodyA, b2Body* bodyB,
const b2Vec2& anchorA, const b2Vec2& anchorB);
/// The local anchor point relative to bodyA's origin.
b2Vec2 localAnchorA;
/// The local anchor point relative to bodyB's origin.
b2Vec2 localAnchorB;
/// The natural length between the anchor points.
float32 length;
/// The mass-spring-damper frequency in Hertz. A value of 0
/// disables softness.
float32 frequencyHz;
/// The damping ratio. 0 = no damping, 1 = critical damping.
float32 dampingRatio;
};
距離關節的定義如上,其相比與b2JointDef增加的屬性有:
localAnchorA:關節連線在剛體A上的區域性錨點,或者就理解成是關節在剛體A上的連線點。
localAnchorB:關節連線在剛體B上的區域性錨點,或者就理解成是關節在剛體B上的連線點。
length:錨點之間的自然長度。
3.2、建立距離關節
-- 建立一個距離關節定義
local distanceJointDef = b2DistanceJointDef()
distanceJointDef:Initialize(bodyA, bodyB, bodyA:GetWorldCenter(), bodyB:GetWorldCenter())
distanceJointDef.bodyA = bodyA
distanceJointDef.bodyB = bodyB
-- 距離關節的長度
distanceJointDef.length = 2
distanceJointDef.frequencyHz = 30
-- 是否連續碰撞
distanceJointDef.collideConnected = false
-- 建立關節
lcoal distanceJoint = tolua.cast(world:CreateJoint(distanceJointDef), "b2DistanceJoint")
建立距離關節同樣需要呼叫Initialize()方法,具體參照焊接關節。
4、移動關節(b2PrismaticJoint)
4.1、移動關節定義(b2PrismaticJointDef)
struct b2PrismaticJointDef : public b2JointDef
{
b2PrismaticJointDef()
{
type = e_prismaticJoint;
localAnchorA.SetZero();
localAnchorB.SetZero();
localAxisA.Set(1.0f, 0.0f);
referenceAngle = 0.0f;
enableLimit = false;
lowerTranslation = 0.0f;
upperTranslation = 0.0f;
enableMotor = false;
maxMotorForce = 0.0f;
motorSpeed = 0.0f;
}
/// Initialize the bodies, anchors, axis, and reference angle using the world
/// anchor and unit world axis.
void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor, const b2Vec2& axis);
/// The local anchor point relative to bodyA's origin.
b2Vec2 localAnchorA;
/// The local anchor point relative to bodyB's origin.
b2Vec2 localAnchorB;
/// The local translation unit axis in bodyA.
b2Vec2 localAxisA;
/// The constrained angle between the bodies: bodyB_angle - bodyA_angle.
float32 referenceAngle;
/// Enable/disable the joint limit.
bool enableLimit;
/// The lower translation limit, usually in meters.
float32 lowerTranslation;
/// The upper translation limit, usually in meters.
float32 upperTranslation;
/// Enable/disable the joint motor.
bool enableMotor;
/// The maximum motor torque, usually in N-m.
float32 maxMotorForce;
/// The desired motor speed in radians per second.
float32 motorSpeed;
};
距離關節的屬性會相對多一點,其相比與b2JointDef增加的屬性有:
localAnchorA:關節連線在剛體A上的區域性錨點,或者就理解成是關節在剛體A上的連線點。
localAnchorB:關節連線在剛體B上的區域性錨點,或者就理解成是關節在剛體B上的連線點。
localAxisA:在剛體A中的區域性移動單元軸,就是剛體A滑動的方向。
referenceAngle:參考角度,剛體B減去剛體A的角度值。
enableLimit:是否啟用限制。
lowerTranslation:移動的最小限制,與方向同向為正,反向為負。啟用限制後才有效果。
upperTranslation:移動的最大限制,與方向同向為正,反向為負。啟用限制後才有效果。
enableMotor:是否啟動馬達。
maxMotorForce:馬達的最大扭力。
motorSpeed:馬達速度。
4.2、建立移動關節(平移關節)
-- 建立一個移動關節的定義
local prismaticJointDef = b2PrismaticJointDef()
if true then
-- 移動的方向,用向量來表示可以移動的方向,零向量(0, 0)為任意方向
local directVec = b2Vec2(10, 0)
-- 初始化關節
prismaticJointDef:Initialize(bodyA, bodyB, bodyB:GetWorldCenter(), directVec)
end
prismaticJointDef.lowerTranslation = -1.0
prismaticJointDef.upperTranslation = 1.0
prismaticJointDef.enableLimit = true
prismaticJointDef.collideConnected = false
-- 建立關節
local prismaticJoint = tolua.cast(world:CreateJoint(prismaticJointDef), "b2PrismaticJoint")
5、繩索關節(b2RopeJoint)
5.1、繩索關節定義(b2RopeJointDef)
struct b2RopeJointDef : public b2JointDef
{
b2RopeJointDef()
{
type = e_ropeJoint;
localAnchorA.Set(-1.0f, 0.0f);
localAnchorB.Set(1.0f, 0.0f);
maxLength = 0.0f;
}
/// The local anchor point relative to bodyA's origin.
b2Vec2 localAnchorA;
/// The local anchor point relative to bodyB's origin.
b2Vec2 localAnchorB;
/// The maximum length of the rope.
/// Warning: this must be larger than b2_linearSlop or
/// the joint will have no effect.
float32 maxLength;
};
繩索關節其相比與b2JointDef增加的屬性有:
localAnchorA:關節連線在剛體A上的區域性錨點,或者就理解成是關節在剛體A上的連線點。
localAnchorB:關節連線在剛體B上的區域性錨點,或者就理解成是關節在剛體B上的連線點。
maxLength:繩索的最大長度。
5.2、建立繩索關節
-- 建立一個繩索關節的定義
local ropeJointDef = b2RopeJointDef()
-- 繩索的最大長度
ropeJointDef.maxLength = 4
-- 關節連線的剛體A
ropeJointDef.bodyA = bodyA
-- 關節連線的剛體B
ropeJointDef.bodyB = bodyB
-- 關節連線在剛體A上的錨點,其他關節應該也有,預設在中央
ropeJointDef.localAnchorA = b2Vec2(0.5, 0.5)
-- 關節連線在剛體B上的錨點,其他關節應該也有,預設在中央
ropeJointDef.localAnchorB = b2Vec2(0.5, 0.5)
-- 是否連續碰撞
ropeJointDef.collideConnected = true
-- 建立關節
local ropeJoint = tolua.cast(world:CreateJoint(ropeJointDef), "b2RopeJoint")
6、旋轉關節(b2RevoluteJoint)
6.1、旋轉關節定義(b2RevoluteJointDef)
struct b2RevoluteJointDef : public b2JointDef
{
b2RevoluteJointDef()
{
type = e_revoluteJoint;
localAnchorA.Set(0.0f, 0.0f);
localAnchorB.Set(0.0f, 0.0f);
referenceAngle = 0.0f;
lowerAngle = 0.0f;
upperAngle = 0.0f;
maxMotorTorque = 0.0f;
motorSpeed = 0.0f;
enableLimit = false;
enableMotor = false;
}
/// Initialize the bodies, anchors, and reference angle using a world
/// anchor point.
void Initialize(b2Body* bodyA, b2Body* bodyB, const b2Vec2& anchor);
/// The local anchor point relative to bodyA's origin.
b2Vec2 localAnchorA;
/// The local anchor point relative to bodyB's origin.
b2Vec2 localAnchorB;
/// The bodyB angle minus bodyA angle in the reference state (radians).
float32 referenceAngle;
/// A flag to enable joint limits.
bool enableLimit;
/// The lower angle for the joint limit (radians).
float32 lowerAngle;
/// The upper angle for the joint limit (radians).
float32 upperAngle;
/// A flag to enable the joint motor.
bool enableMotor;
/// The desired motor speed. Usually in radians per second.
float32 motorSpeed;
/// The maximum motor torque used to achieve the desired motor speed.
///