這兩天看了下,所以當個筆記,轉載自:點擊打開鏈接
通常在遊戲簡單邏輯判斷和模擬真實的物理世界時,我們只需要在定時器中判斷遊戲中各個精靈的條件是否滿足判斷條件就可以了。例如,在飛機大戰中,判斷我方子彈和敵機是否發生碰撞一般在定時器中通過敵機所在位置的矩形是否包括了子彈的位置來判斷是否發生碰撞。在消除類遊戲中,判斷在y軸或x軸上是否要消除相同物品一般在定時器中通過循環來檢測在某個方向上是否有連續的相同物品滿足消除個數來移除精靈就可以了。
但是要進行復雜的邏輯判斷和模擬真實的物理世界時,完全靠自己手寫代碼來判斷工作量太大了。例如,在憤怒的小鳥中,首先,你要模擬小鳥受力後彈出的軌跡路線。然後,你要判斷小鳥和箱子是否發生碰撞。最後,假如小鳥和箱子碰撞後,你還要模擬小鳥和箱子往下倒下的軌跡路線。這時候,物理引擎就幫上忙了,它通過我們設定參數來爲剛性物體賦予真實的物理屬性的方式來計算物體運動、旋轉和碰撞。同時,我們只要在回調函數中編寫代碼就可以處理不同的情況了。
比較出名的遊戲引擎有EA DICE的寒霜引擎 、BigWorld公司的BigWorld引擎、Emergent公司的Gamebryo引擎、EPIC公司的虛幻引擎、搜狐暢遊公司的黑火引擎、完美世界公司的Athena引擎等。遊戲引擎包含以下系統:渲染引擎(即“渲染器”,含二維圖像引擎和三維圖像引擎)、物理引擎、碰撞檢測系統、音效、腳本引擎、電腦動畫、人工智能、網絡引擎以及場景管理。而對於手機遊戲來說,並不需要功能那麼多且複雜的引擎,畢竟手機的資源有限不像電腦那麼高性能。
因此,Cocos2d-x採用了Box2D物理引擎和Chipmunk物理引擎來模擬真實的物理世界。Box2D幾乎能模擬所有的物理效果,而chipmunk則是個更輕量的引擎等。而在Cocos2d-x3.2版本中默認採用Chipmunk,所以我就以Chip-munk來講解。並且爲了簡化物理引擎和Cocos2d-x的交接,Cocos2d-x直接提供函數來設置物體參數,不需要我們採用Chip-munk原生的函數來設置,這大大簡化的代碼的編寫。
Chipmunk是採用C語言編寫的2D剛體物理仿真庫,包含4種基本的對象類型,分別是:
空間:空間是Chipmunk中模擬對象的容器。你將剛體、形狀、關節添加進入一個空間,然後將空間作爲一個整體進行更新。空間控制着所有的剛體、形狀和約束之間的相互作用。
剛體:一個剛體容納着一個對象的物理屬性(如質量、位置、角度、速度等)。默認情況下,它並不具有任何形狀,直到你爲它添加一個或者多個碰撞形狀進去。如果你以前做過物理粒子,你會發現它們的不同之處是剛體可以旋轉。在遊戲中,通常剛體都是和一個精靈一一對應關聯的。你應該構建你的遊戲以便可以使用剛體的位置和角度來繪製你的精靈。
碰撞形狀:因爲形狀與剛體相關聯,所以你可以爲一個剛體定義形狀。爲了定義一個複雜的形狀,你可以給剛體綁定足夠多的形狀。形狀包含着一個對象的表面屬性如摩擦力、彈性等。
約束/關節:約束和關節被用來描述剛體之間是如何關聯的
人們經常對Chipmunk中的剛體和碰撞形狀以及兩者與精靈之間的關係產生混淆。精靈是對象的可視化表現,而碰撞形狀是定義對象應該如何碰撞的不可見的屬性。精靈和碰撞形狀兩者的位置和角度都是由剛體的運動控制的。通常你應該創建一個遊戲對象類型,把這些東西捆綁在一起。
在Cocos2d-x3.2中,物理世界被融入到Scene中,即當創建一個場景時,就可以指定這個場景是否使用物理引擎。
sprite自帶body屬性,直接設置body,而形狀、約束等屬性添加到body中即可。碰撞的檢測通過事件分發器來監控,所以你首先要創建監聽器--EventListenerPhysicsContact,再把其添加到分發器中。
說了那麼多鋪墊,接下來通過代碼來講解具體使用。
第一步,新建一個工程,並且刪除HelloWorld.h/cpp中代碼,在HelloWorld.h中添加代碼1,它的作用是聲明變量。
代碼1:
在代碼1中,m_world是用來保存在scene中創建的物理世界,也就是上面說的空間,以備得到空間中的參數,如重力系數等。第二步,在createScene()中添加代碼2,它的作用是創建一個有物體空間的場景。
代碼2:
第三步,在init()添加代碼3。代碼3: 在代碼3中,我們看到了物體的創建並且創建是就自帶形狀,然後物體的物理屬性。一個物體可以先創建,然後再添加形狀。一個物體可以有多個形狀,只要不衝突就可以,並且同一個物體多個形狀之間不發生碰撞,因爲他們屬於同一碰撞組。所以我們看到getShape(0)的調用,因爲我們添加形狀時默認從零開始,上面我們只添加了一個形狀。添加形狀代碼在Cocos2d-x3.2代碼爲PhysicsBody::addShape()。你可以在...\cocos2d\cocos\physics目錄下CCPhy sicsBody.cpp中看到代碼4。
代碼4:
代碼4爲物體在同一組中添加形狀,如果你想再進一步追尋下去,可以繼續轉到不同函數的定義中,最後你會看到Chipmunk原生的代碼。
對於不規則的形狀,我們可以使用輔助工具--VertexHelper、PhysicsEditor等等來生成點數組來設置形狀,在這裏,我就使用簡單得形狀來做示例,如果後面有時間,我也會寫一篇使用輔助工具來生成不規則形狀的文章。
在代碼3中,恢復力和摩擦力要成對使用,即使你把恢復力設置爲1.0而不設置摩擦力爲0,依舊不是完全彈性碰撞,自己可以修改調試就可以知道正確與否。關於彈性碰撞可以參考一些物理書。對於恢復力,在Chipmunk中文手冊有“0.0表示沒有彈性,1.0b表示“富有”彈性。然而由於存在模擬誤差,不推薦使用1.0或更高的值,碰撞的彈性是由單個形狀的彈性相乘得到”。對於摩擦力,在Chipmunk中文手冊有“Chipmunk使用的是庫侖摩擦力模型,0.0值表示無摩擦。碰撞間的摩擦是由單個形狀的摩擦相乘找到”。
在代碼3中,衝力的設置爲在x軸和y軸上各爲500000.0,最後根據物理中合力來計算結果。applyImpulse在離重心相對偏移量爲r的位置施加衝量於物體上,默認偏移量爲0,你可以在...\cocos2d\cocos\physics目錄下CCPhysics-Body.cpp中看到代碼5。並且在絕對座標系中施加力或者衝量,並在絕對座標系中產生相對的偏移(偏移量相對於重心位置,但不隨剛體旋轉)。Chipmunk使用的座標是x軸向右爲正,y軸向上爲正。關於摩擦力,你可以在...\cocos2d\cocos\physics目錄下CCPhysicsShape.cpp中看到代碼6。
代碼5:
代碼6:
在代碼5中的cpBodyApplyImpulse爲Chipmunk的原生代碼。設置衝力時,Vec2()構造函數產生默認值爲(0,0)。
在代碼6中的cpShapeSetFriction爲Chipmunk的原生代碼,作用是設置摩擦力。
在代碼3,setDensity的作用是設置密度,其實它最後通過乘以面積轉變成質量來設置質量的。根據物理公式在2維中密度乘以面積等於質量。所以,你能在...\cocos2d\cocos\physics目錄下CCPhysicsShape.cpp中看到代碼7。
代碼7:
在代碼3中,setTag的作用是當發生碰撞時能讓我們知道那兩個精靈發生碰撞,具體見下文碰撞檢測。
在代碼3中,setDynamic函數,此作用爲設置物體是不是靜態剛體。如果你設置了物體爲靜態,你再設置其他參數,也是沒有作用的,原因是靜態物體不更新狀態。關於靜態剛體,Chipmunk中文手冊有這麼一段,如下:
靜態剛體有兩個目的。最初,它們被加入用來實現休眠功能。因爲靜態剛體不移動,Chipmunk知道讓那些與靜態剛體接觸或者連接的物體安全的進入休眠。接觸或連接常規遊離剛體的物體從不允許休眠。靜態剛體的第二個目的就是讓Chipmunk知道,關聯到靜態剛體的碰撞形狀是不需要更新碰撞檢測數據的。Chipmunk也不需要操心靜態物體之間的碰撞檢測。通常所有的關卡幾何圖形都會被關聯到一個靜態剛體上除了那些能夠移動的東西,例如平臺或門等。在Chipmunk5.3版本之前,你要創建一個無限大質量的遊離剛體,通過`cpSpaceAddStaticShape()`來添加靜態形狀。現在你不必這樣做了,並且如果你想使用休眠功能也不應該這樣做了。每一個空間都有一個專用的靜態剛體,你可以使用它來添加靜態形狀。Chipmunk也會自動將形狀作爲靜態形狀添加到靜態剛體上。
也就是說,如果要讓兩個物體發生碰撞,至少有一個不爲靜態,因爲兩個都爲靜態,就不用更新數據了。如果兩個物體都是靜態的,即使你強行用runAction來發生碰撞,兩個物體也不會發生碰撞。具體見下文碰撞檢測。這個在我調試一個例子中,花了半天時間去看Chipmunk中文手冊後才調試出來的。
現在可以看到效果1。
效果1:
上面我們可以看到兩個球在一個盒子中彈來彈去。這裏我只是給出了簡單的例子來說明,沒有給出通過關節和約束來把兩個物體關聯在一起的例子。如果你對兩個物體通過關節和約束關聯在一起不是很明白的話,可以在打開以下的視頻連接--http://v.youku.com/v_show/id_XNjgxNDIyMzky.html來看看。這裏我就截個圖1來做示例。
圖1:
對於要處理相互碰撞事件來說,還要進行以下步驟。這裏我們想看到的效果是兩球碰撞後,兩球消失。
第四步,我們要在onEnter函數中添加碰撞檢測監聽器並且把監聽器添加到事件分發器中,並且添加處理函數。我們要重寫onEnetr函數如代碼8,添加處理函數如代碼9。
代碼8:
代碼9:
代碼9中,我只給出了碰撞begin函數,Chipmunk一共有4個回調函數,Chipmunk中文手冊如下說明:
1.begin():該步中兩個形狀剛開始第一次接觸。回調返回true則會處理正常碰撞,返回false,Chipmunk會完全忽略碰撞。如果返回false,則preSolve()和postSolve()回調將永遠不會被執行,但你仍然會在形狀停止重疊的時候接收到一個單獨的事件。
2.preSolve():該步中兩個形狀相互接觸。回調返回false,Chipmunk在這一步會忽略碰撞,返回true來正常處理它。此外,你可以使用cpArbiterSetFriction(),cpArbiterSetElasticity()或cpArbiterSetSurfaceVelocity()來提供自定義的摩擦,彈性,或表面速度值來覆蓋碰撞值。
3.postSolve():兩種形狀相互接觸並且它們的碰撞響應已被處理。如果你想使用它來計算音量或者傷害值,這時你可以檢索碰撞衝力或動能。
4.separate():該步中兩個形狀剛第一次停止接觸。確保begin()/separate()總是被成對調用,當刪除接觸中的形狀時或者析構space時它也會被調用。
碰撞回調都與cpArbiter結構緊密相關。你應該熟悉那些爲好。
注1:標記爲傳感器的形狀(cpShape.sensor == true)從來不會得到碰撞處理,所以傳感器形狀和其他形狀間永遠不會調用postSolve()回調。它們仍然會調用begin()和separate()回調,而preSolve()仍然會在每幀調用回調,即使這裏不存在真正的碰撞。
注2:preSolve()回調在休眠算法運行之前被調用。如果一個對象進入休眠狀態,postSolve()回調將不會被調用,直到它被喚醒。
上面所說的都是碰撞處理說明,下面代碼10就是它們的API。
代碼10:
碰撞處理函數類型。所有這些函數都附帶一個arbiter,space和用戶data指針,只有begin()和preSolve()回調會返回值。上面帶cp開頭的函數都是Chipmunk原生函數,如果你想了解得更多,可以閱讀Chipmunk中文手冊中相關函數的說明。
那Cocos2d-x3.2又是如何和上面那4個函數聯繫在一起並且把它們封裝到哪些函數中去?我第一感覺是Scene::
createWithPhysics()在創建物理空間時就把Chipmunk原生函數關聯了事件監聽器的函數。因爲除了createWithPhysi cs要進行初始化外,其他地方都是對參數的設置和獲取。那我開始逐步追尋下去,在createWithPhysics中找到了initWithPhysics,在initWithPhysics中找到了PhysicsWorld::construct(*this),在construct中找到了PhysicsWorld:: init(),好了,在init()終於發現了代碼11。代碼11的目錄爲...\cocos2d\cocos\physics\CCPhysicsWorld.cpp。
代碼11:
代碼12:
現在可以看到效果2。
效果2:
什麼?效果2中小球碰撞後怎麼不消失,這不是我預想的結果啊!是不是哪裏出了問題。我馬上就在onContact-Begin中設置斷點進行調試,發現兩球即使碰撞也沒有觸發碰撞函數,這不是坑爹嗎?就這個問題,一開始我想是不是每幀刷新的速度太快了導致檢測不了碰撞,但是從效果2中可以看到明明兩球發生了碰撞,結果就是兩球受力彈開了
,這個思路明顯是錯誤的。從效果2中可以看出,碰撞確實是發生了,只是沒有觸發回調函數。
後來上網查閱其他文章和Chipmunk中文手冊,發現這是碰撞過濾的問題。從猜想各種各樣的原因到發現是碰撞過濾的問題,這其中花費了我很多時間。下面我就來講講碰撞過濾。
首先,大家有沒有這樣的疑問--爲什麼需要碰撞過濾?玩過遊戲的人都知道,自己的隊友放出的技能會在敵方身上發生效果,不會作用在自己身上。此時,碰撞過濾就發揮作用了,它使得物體A和物體B會發生碰撞,而物體A和物體C不會發生碰撞,並且決定了發生了碰撞後回調函數是否接收此碰撞事件。由於上面默認碰撞過濾中沒有發出通知,所以回調函數收不到兩球碰撞的事件。
關於碰撞過濾,Chipmunk中文手冊中有如下描述:在空間索引找出彼此靠近的形狀對後,將它們傳給space,然後再執行一些額外的篩選。在進行任何操作前,Chipmunk會執行幾個簡單的測試來檢測形狀是否會發生碰撞。包圍盒測試:如果形狀的包圍盒沒有重疊,那麼形狀便沒發生碰撞。對象如對角線線段會引發許多誤報,但你不應該擔心。層測試:如果形狀不在同一層內則不會發生碰撞。(他們的層掩碼按位與運算結果爲0)羣組測試:在相同的非零羣組中的形狀不會發生碰撞。對應代碼在...\cocos2d\external\chipmunk\src\cpSpaceStep.c中,如代碼13。
代碼13:
但是Cocos2d-x3.2封裝了Chipmunk的碰撞過濾,方式與Chipmunk原生代碼不同。Cocos2d-x3.2有三個碰撞過濾標誌categoryBitmask、contactTestBitmask和collisionBitmask。這三個標誌在目錄...\cocos2d\cocos\physics\CCPhysicsShape.h下有英文的註釋,下面是我的翻譯和加上我的一些理解。
categoryBitmask:
分類掩碼,定義了物體屬於哪個分類。場景中的每個物理剛體可以被賦值一個多達32位的值(因爲categoryBitmask爲int型),每個對應32位掩碼中的每一位,你在你的遊戲中定義掩碼值。結合collisionBitMask和contactTestBitMask屬性, 你可以定義哪些物理剛體相互作用並且你的遊戲何時接受這些相互作用的通知。默認值爲0xFFFFFFFF(所有位都被設置)。
contactTestBitmask:
接觸測試掩碼,定義哪些剛體分類可以與本剛體產生相互作用的通知。當兩個剛體在同一個空間,即物理世界中,每個剛體的分類掩碼會和其他剛體的接觸測試掩碼進行邏輯與的運算。如果任意一個比較結果爲非零值,產生一個PhysicsContact對象並且傳遞到物理世界協議中,這裏協議指我們的監聽器對應的回調函數。 爲了最好的性能,僅設置你感興趣的接觸測試掩碼中的位,也就是說通過設置接觸測試掩碼,你可以決定發生碰撞後,回調函數是否有響應。默認值爲0x00000000(所有位都被清除)。
collisionBitmask:
碰撞掩碼,定義了哪些物理剛體分類可以和這個物理剛體發生碰撞。當兩個物理剛體相互接觸時,可能發生碰撞。這個剛體的碰撞掩碼和另一個剛體的分類掩碼進行邏輯與運算比較。如果結果是一個非零值,這個剛體會發生碰撞。每個剛體獨立選擇接受與哪個剛體發生碰撞。例如,你可以使用此掩碼來忽略那些對於本剛體的速度有影響的剛體碰撞,也就是說你可以使用此掩碼使得本剛體與某些剛體碰撞不會對本剛體產生影響。默認值爲0xFFFFFFFF(所有位都被設置)。
從上面三個掩碼的說明中,我們可以做一個小結。假設剛體A的接觸測試掩碼和碰撞掩碼已知,剛體B的分類掩碼決定了能否和A進行碰撞和在碰撞的前提下能否發出PhysicsContact對象觸發回調函數。如果B的分類掩碼與A的碰撞掩碼做邏輯與運算的結果爲0,則不會發生碰撞,因此也不會繼續和A的接觸測試掩碼進行邏輯與運算。如果B的分類掩碼與A的碰撞掩碼做邏輯與運算的結果非0,則發生碰撞,並且B的分類掩碼繼續與A的接觸測試掩碼做邏輯與運算,如果結果非0,則發出PhysicsContact對象觸發回調函數。
在上面兩球碰撞的例子中,由於categoryBitmask的默認值爲0xFFFFFFFF,contactTestBitmask的默認值爲0x00000000,collisionBitmask默認值爲0xFFFFFFFF,所以ballOne的分類掩碼與ballTwo的碰撞掩碼做邏輯與的結果爲0xFFFFFFFF即非0,即可以發生碰撞,ballOne的分類掩碼繼續與ballTwo的接觸測試掩碼做邏輯與運算,結果爲0,因此不會發出PhysicsContact對象,不會觸發回調函數,所以我們上面的onContactBegin不會被觸發,所以兩球碰撞後不會消失。
現在,我把ballTwo的分類掩碼設置爲0x0001,碰撞掩碼設置爲0x0001,把ballTwo的分類掩碼設置爲0x0010,碰撞掩碼設置爲0x0010,使用16進製表示只是爲了更好觀察邏輯與運算的結果,邊緣盒子的值保留爲默認值。在init()函數設置每個剛體的屬性後面添加代碼14。
代碼14:
我們可以看到效果3。
效果3:
從效果3中可以到兩球不會發生碰撞,會相互穿過對方。因爲ballOne的分類掩碼和ballTwo的碰撞掩碼做邏輯與的結果爲0,ballTwo的分類掩碼和ballOne的碰撞掩碼做邏輯與的結果爲0,所以不會發生碰撞。注意,即使ballOne的ballOne的分類掩碼和ballTwo的碰撞掩碼做邏輯與的結果非0,但是ballTwo的分類掩碼和ballOne的碰撞掩碼做邏輯與的結果爲0,依舊不會發生碰撞的。這正如物理書上所說,作用力和反作用力是相互的。讀者可以自己修改代碼,觀察運行效果。
下面我們再次修改代碼如代碼15,使得兩球可以碰撞,並且碰撞後發出PhysicsContact對象並觸發回調函數使得兩球消失。
代碼15:
我們可以看到效果4。
效果4:
從效果4中,我們看到兩球相撞後就消失了,終於和我們想要的效果是一樣了。因爲ballOne的分類掩碼和
ballTwo的碰撞掩碼做邏輯與的結果爲非0,ballTwo的分類掩碼和ballOne的碰撞掩碼做邏輯與的結果爲非0,所以ballOne的分類掩碼繼續和ballTwo的接觸測試掩碼做邏輯與運算,結果爲非0,ballTwo的分類掩碼繼續和ballOne的接觸測試掩碼做邏輯與運算,結果爲非0,因此會發出PhysicsContact對象並觸發回調函數,在onContactBegin函數中判斷兩個精靈是否爲ballOne和ballTwo,如果是,則移除它們,所以ballOne和ballTwo會消失。注意,即使ballOne的分類掩碼繼續和ballTwo的接觸測試掩碼做邏輯與運算,結果爲非0,但是ballTwo的分類掩碼繼續和ballOne的接觸測試掩碼做邏輯與運算,結果爲0,依舊不會發出PhysicsContact對象和觸發回調函數。讀者可以自己修改代碼,觀察運行效果。
到此爲止,通過上面詳細的講述,使用Cocos2d-x3.2物理引擎進行碰撞檢測算正式講完了。下面我將討論一個比較趣的例子。
大家有沒有想過一個精靈設置了剛體後,再使用runAction來運行動作?如果像上面例子中的又受到衝力又運行動作,小球的運行軌跡,我都不知道怎麼樣分析,如果誰知道,請告訴我一下。下面的例子的小球是沒有受到衝力的。
我們修改代碼,如代碼16。
代碼16:
從效果5中,我們可以看到精靈運行動作也是可以使小球相互碰撞並且觸發回調函數。到底精靈的runAction是如何和物理碰撞關聯在一起的,之間是如何相互影響的?目前,我也不太清楚,如果誰知道,請告訴我一下。
現在我強行用runAction使得小球運動。什麼是強行使小球運動?你是否還記得我在前面說過setDynamic函數的作用?默認情況下,剛體是爲動態的。現在我把兩個小球都設置爲靜態剛體,即setDynamic(false)。由於,靜態剛體是在空間中不會更新狀態,但是我用精靈的runAction使小球運動,所以叫做強行。
我現在在兩球設置代碼中添加setDynamic(false),運行效果如效果6。
效果6:
從效果6中,我們看到兩球沒有發生碰撞的效果且沒有觸發回調函數。
以上就是我這周學習物理引擎的總結,真心累,看資料有點累,而到最後寫這篇總結時思考要怎麼寫,要總結些什麼就更累了。最後我就以圖2來做整個流程的總結。
圖2: