實驗4 碰撞檢測與運動模擬(Box2D桌球)
說明:
課程教材《計算機遊戲程式設計》(基礎篇)(第3版) 提供示例程式碼,而課程實驗在示例程式碼的基礎上提出更高的實驗要求。除此之外,本人也會額外加入些個人創意,希望同學們在參考之餘也能加入自己的想法。
實現效果:
實驗報告:
一、實驗目的與要求
1. 瞭解物理模擬、精靈的繪製與移動、觸控事件的應用等。
2. 熟悉基於Box2D的物理引擎。
3. 掌握Box2D觸屏檢測和碰撞檢測機制。
二、實驗內容與方法
1.完成遊戲編譯(70分)
成功編譯並執行教材P150“遊戲物理模擬例項-基於Box2D的遊戲例項”。
2.完成方案一 (10分)
修改遊戲程式碼,實現修改內容一,即修改擊球方式。
3.完成方案二 (10分)
修改遊戲程式碼,實現修改內容二,即增加一個進洞口。
4.完成方案三 (10分)
修改遊戲程式碼,實現修改內容三,即修改球杆跟隨力度變化的狀態。
三、實驗步驟與過程
(記錄關鍵步驟/設計過程/設計結果的截圖)
1.完成遊戲編譯(70分)
老套路編譯例程並執行程式。
圖 1
2.修改擊球方式 (10分)
修改擊球方式為:在擊球區域拖動滑鼠左鍵,可以實現球杆的瞄準以及能量的蓄積,鬆開滑鼠左鍵,實現擊球。
- 仔細研究原始碼,發現“在擊球區域拖動滑鼠左鍵,可以實現球杆的瞄準”這一功能原本就已經實現了,無需多做修改。
- 而對於“在擊球區域拖動滑鼠左鍵,可以實現能量的蓄積”這一功能,只需把原來在“能量滑塊區域”滑動更改為在“擊球區域”中滑動即可。
因此,在拖動滑鼠呼叫的函式onTouchMoved()中,無需再把“球杆瞄準”和“能量積蓄”兩個功能分別寫在“擊球區”和“能量滑動區”中實現,而是統一放到“擊球區”中實現即可。
此時,“當前擊球能量”變數_curPower的值可更改為由“白球”的座標和“目標點”的座標的距離長度(為更好控制數值,可讓它們再除以2)。
即curPower=(touch->getLocation()-changePos(mainBilliards->getPosition())).getLength()/2
- “鬆開滑鼠左鍵,實現擊球”這一功能在原始碼中也已經實現了,只不過它限定了在“能量滑動區域”的範圍裡。因此,只需要把這一個限定條件“_powerRect.containsPoint(touch->getLocation())” 給刪掉就可以了。
修改後程式碼為:
圖 2
圖 3
圖 4
3.增加四個進洞口 (10分)
Ps一張“球洞”圖片,命名為“hole”。
圖 5
- 直接利用BilliardSprite類來建立“球洞”物件,但是與建立“檯球”物件不同的是,由於“球洞”不是“運動物體”而是“靜態物體”,所以在create時要注意引數isStatic設定為true。
圖 6
由於檯球遊戲一般有四個洞,所以我也給這個遊戲設了四個“球洞”。所以,四個球洞除了位置的不同外,還應該有旋轉角度的不同,由上圖可見四個球洞精靈物件分別要旋轉0、90、180、270度。但由於BilliardSprite初始化函式中沒有設定精靈物件的旋轉角度的程式碼。所以可以另外在BilliardSprite類中寫一個方法函式initHole(float rotation)來設定精靈物件的旋轉角度。
但要注意的是,BilliardSprite中的update函式中會不斷地更新精靈的位置以及根據body的角度變化來設定精靈的旋轉。但是在預設情況下,物體都是不動的,所以會不斷地給精靈的旋轉角度設定為0 ,這樣initHole函式設定的“洞口”旋轉角度就前功盡廢了。如下圖update程式碼所示:
圖 7
所以,為了“球洞”精靈不受update函式的影響。我們可以在initHole函式執行this->unscheduleAllSelectors(),解除update定時器,讓“球洞”維持“靜止”。
而“球洞”精靈的位置,要由“球洞”物體的位置來轉換而成。因此,“球洞”初始化函式initHole程式碼有:
圖 8
- 當“白球”進洞時,重新開始遊戲。該功能實現只需要如實驗三一樣,在遊戲主場景PhysicsBox2dScene.cpp中執行如下程式碼,進行與新場景的切換即可:
CCDirector::sharedDirector()->replaceScene(PhysicsBox2dScene::createScene());
於是,問題的關鍵就在於如何判斷“白球進洞”並執行上述程式碼了。可給碰撞監聽類GameContactListener新增一個bool型別的私有變數mainBallInHole,表示白球是否進洞,並新增該變數的get方法。
當物體間發生碰撞後,GameContactListener.cpp的BeginContact函式會被呼叫,該函式通過b2Contact引數獲取到兩個碰撞物件。因此,只需通過獲取這兩個碰撞物件的name並進行比對,如果是“main”和“hole”便能確定是“白球進洞”,然後再把mainBallInHole變數設為true。
而PhysicsBox2dScene.cpp的update函式中通過get方法獲取到mainBallInHole變數再進行if判斷,條件符合後便執行場景切換函式即可。
程式碼為:
GameContactListener.cpp:
圖 9
PhysicsBox2dScene.cpp的update函式:
圖 10
PhysicsBox2dScene.cpp:
圖 11
- 當除了白球的“普通球”進洞時,該球要消失。結合②部分可知,只需在GameContactListener.cpp的BeginContact函式中新增一個name為“ball”與“hole”碰撞的情況即可。其中,最好的實現方法莫過於把要移除的小球給記錄在連結串列ballsRemovalList中,在時間長step執行後及場景被渲染前,執行GameContactListener的函式deleteBody來遍歷ballsRemovalList,對其中記錄的小球物件進行刪除。
然而,由於BilliardSprite物件被呼叫的地方比較多,導致直接刪除該物件(delete BilliardSprite*)程式報錯,本人功力不夠深厚沒能解決這一問題。只能退而求其次,在BilliardSprite類中寫一個新的函式方法deleteBody(),通過呼叫其來刪除BilliardSprite物件中的_body以及移除_sprite 。這樣做雖然不是最佳方案但也能實現同樣的功能。
程式碼為:
GameContactListener.cpp:
圖 12
PhysicsBox2dScene.cpp (要在step後執行刪除操作):
圖 13
GameContactListener.cpp:
圖 14
BilliardSprite.cpp:
圖 15
4.修改球杆跟隨力度變化的狀態 (10分)
重點觀察PhysicsBox2dScene.cpp中的函式updateLine()的“計算球杆位置”部分的程式碼,有如下所示:
圖 16
從這個程式碼中我們很難看得出什麼,我們對其進行展開和轉換。以x軸座標為例,即:
(((end_p - start_p).getLength() + 45) * start_p.x - 45 * end_p.x) / (end_p - start_p).getLength()
我們可以把其轉化為:
start_p.x + 45 * (start_p.x - end_p.x) / (end_p - start_p).getLength()
由轉換後的程式碼我們就比較容易看出start_p.x指白球的x座標,而 (start_p.x - end_p.x) / (end_p - start_p).getLength() 指的是夾角的餘弦值。因此,易知這個45指的應該是球杆與白球的距離(y軸座標分析同理)。
可知,預設情況下球杆離白球的距離為45。因此,我們只需根據力度的數值改變這45的距離即可。引入_curPower,把程式碼中的45都改為45+_curPower/2 (除以2是為了適當控制距離,令球杆不要離白球太遠),即:
圖 17
5.其餘
下面介紹自己新添的內容:
- 由一個洞口新增到四個洞口(前文已說明)
- 視窗解析度調整為1600 x 900
- 小球由5個增添到10個,並對位置進行了調整
- 新增新的背景精靈bg,即為綠色的檯球布。程式碼為:
圖 18
6.程式執行
初始圖:
圖 19
注意球杆和白球距離,以及力量條間的關係:
圖 20
圖 21