本教程基於子龍山人翻譯的cocos2d的IPHONE教程,用cocos2d-x for XNA引擎重寫,加上我一些加工製作。教程中大多數文字圖片都是原作者和翻譯作者子龍山人,還有不少是我自己的理解和加工。感謝原作者的教程和子龍山人的翻譯。本教程僅供學習交流之用,切勿進行商業傳播。
子龍山人翻譯的Iphone教程地址:http://www.cnblogs.com/zilongshanren/archive/2011/05/29/2059467.html
Iphone教程原文地址:http://www.raywenderlich.com/505/how-to-create-a-simple-breakout-game-with-box2d-and-cocos2d-tutorial-part-22
程序截圖:
這是《如何使用cocos2d和box2d製作一個簡單的breakout遊戲》的第二部分,也是最後一部分教程。如果你還沒有讀過第一部分,請先閱讀《<cocos2d-x for wp7>使用cocos2d-x和BOX2D來製作一個BreakOut(打磚塊)遊戲(一)》。
在上一個教程中,我們創建了一個屏幕盒子,球可以在裏面彈跳,同時,我們可以用手指拖着paddle移動。這部分教程中,我們將添加一些遊戲邏輯,當籃球碰到屏幕底部的時候,就Gameover。
Box2D 和碰撞檢測
在Box2D裏面,當一個fixture和另一個fixture相互碰撞的時候,我們怎麼知道呢?這就需要用到碰撞偵聽器了(contact listener)。一個碰撞偵聽器是一個對象,它繼承至box2d的IContactListner接口的實現,並且要設置給world對象。這樣,當有兩個對象發生相互碰撞的時候,world對象就會回調contact listener對象的方法,這樣我們就可以在那些方法裏面做相應的碰撞處理了。
如何使用contact listener呢?根據BOX2D用戶手冊,在一個仿真週期內,你不能執行任何修改遊戲物理的操作。因爲,在那期間,我們可能需要做一些額外的處理(比如,當兩個對象碰撞的時候銷燬另一個對象)。因此, 我們需要保存碰撞的引用,這樣後面就可以使用它。
另外一點值得注意的是,我們不能存儲傳遞給contact listener的碰撞點的引用,因爲,這些點被BOX2D所重用。因此,我們不得不存儲這些點的拷貝。
好了,說得夠多了,讓我們親手實踐一下吧!
當我們碰到屏幕底部的時候
這裏我們添加一個類到Classes文件夾。並且命名爲MyContactListener.cs。並且使之繼承於接口IContactListener。並修改這個命名空間內的代碼爲:
class MyContact
{
public Fixture fixtureA;
public Fixture fixtureB;
}
class MyContactListener : IContactListener
{
public List<MyContact> contacts = new List<MyContact>();
public void BeginContact(Contact contact)
{
MyContact myContact = new MyContact()
{
fixtureA = contact.GetFixtureA(),
fixtureB = contact.GetFixtureB()
};
contacts.Add(myContact);
}
public void EndContact(Contact contact)
{
contacts.Clear();
}
public void PostSolve(Contact contact, ref ContactImpulse impulse)
{
}
public void PreSolve(Contact contact, ref Manifold oldManifold)
{
}
}
這裏,我們定義了一個類MyContact來保存數據,當碰撞通知到達的時候,用來保存碰撞點信息。再說一遍,我們需要存儲其拷貝,因爲它們會被重用,所以不能保存指針。這裏有一個問題,就是如果在EndContact的時候要從list裏面找到那個點的話,基本是找不到的。我經過多次試驗,每次在EndContact的時候,只有一個點在list裏面,如果不移除的話就會出問題。但是用什麼IndexOf方法壓根就找不到。所以這裏用了一個很鬱悶的方法,直接將其清空。關於爲什麼找不到的問題。我想了想,估計是在GetFixtureA和GetFixtureB這兩個方法的問題,感覺可能是返回的是引用,並不是拷貝。如果是這樣的話,我們在這裏做的僅僅是Contact的淺拷貝。這樣的話,應該做個深拷貝纔行。不過我也沒有看源碼中GetFixtureA是怎麼實現的。所以這裏僅僅是猜測。有興趣的朋友可以去看看GetFixtureA是怎麼實現的。然後來解決這個問題吧。
PS:C#中的引用,淺拷貝,深拷貝要特別注意,不然會發現很多很奇怪的問題。
好了,現在可以使用它吧。打開BreakOutLayer類,然後添加一個聲明:
MyContactListener contactListener;
然後在init方法中增加下列代碼:
//Create contact listener
contactListener = new MyContactListener();
world.ContactListener = contactListener;
這裏,我們創建了contact listener對象,然後調用world對象把它設置爲world的contact listener。
最後,在tick方法底部添加下列代碼:
foreach (var item in contactListener.contacts)
{
if ((item.fixtureA == bottomFixture && item.fixtureB == ballFixture) ||
(item.fixtureA == ballFixture && item.fixtureB == bottomFixture))
{
Debug.WriteLine("Ball hit the bottom!");
}
}
這裏遍歷所有緩存的碰撞點,然後看看是否有一個碰撞點,它的兩個碰撞體分別是籃球和屏幕底部。目前爲止,我們只是使用NSLog來打印一個消息,因爲我們只想測試這樣是否可行。
因此,在debug模式下編譯並運行,你會發現,不管什麼時候,當球和底部有碰撞的時候,你會看到控制檯輸出一句話“Ball hit the bottom"!
新建一個類添加到Classes文件夾,命名爲GameOverScene.cs。並且使之繼承於CCScene
修改代碼爲:
class GameOverScene:CCScene
{
public GameOverScene(bool isWin)
{
string msg;
if (isWin)
msg = "YOU WIN";
else
msg = "YOU LOSE";
CCLabelTTF label = CCLabelTTF.labelWithString(msg, "Arial", 24);
label.position = new CCPoint(CCDirector.sharedDirector().getWinSize().width / 2, CCDirector.sharedDirector().getWinSize().height - 100);
this.addChild(label);
}
}
然後,把Debug語句替換成下列代碼:
GameOverScene pScene = new GameOverScene(false);
CCDirector.sharedDirector().replaceScene(pScene);
好了,我們已經實現得差不多了。但是,如果你遊戲你永遠不能贏,那有什麼意思呢?
增加一些方塊
下載我製作的方塊圖片,然後把它添加到images文件夾下面.
然後往init方法中添加下列代碼:
for (int i = 0; i < 4; i++)
{
int padding = 20;
//Create block and add it to the layer
CCSprite block = CCSprite.spriteWithFile(@"images/Block");
float xOffset = padding + block.contentSize.width / 2 + (block.contentSize.width + padding) * i;
block.position = new CCPoint(xOffset, 400);
block.tag = 2;
this.addChild(block);
//Create block body
BodyDef blockBodyDef = new BodyDef();
blockBodyDef.type = BodyType.Dynamic;
blockBodyDef.position = new Vector2((float)(xOffset / PTM_RATIO), (float)(400 / PTM_RATIO));
blockBodyDef.userData = block;
Body blockBody = world.CreateBody(blockBodyDef);
//Create block shape
PolygonShape blockShape = new PolygonShape();
blockShape.SetAsBox((float)(block.contentSize.width / PTM_RATIO / 2), (float)(block.contentSize.height / PTM_RATIO / 2));
//Create shape definition and add to body
FixtureDef blockShapeDef = new FixtureDef();
blockShapeDef.shape = blockShape;
blockShapeDef.density = 10.0f;
blockShapeDef.friction = 0.0f;
blockShapeDef.restitution = 0.1f;
blockBody.CreateFixture(blockShapeDef);
}
現在,你應該可以很好地理解上面的代碼了。就像之前我們爲paddle創建一個body類似,這裏,我們每一次也會一個方塊創建一個body。注意,我們把方塊精靈對象的tag設置爲2,這樣將來可以用到。
編譯並運行,你應該可以看到籃球和方塊之間有碰撞了。
銷燬方塊
爲了使breakout遊戲是一個真實的遊戲,當籃球和方塊有交集的時候,我們需要銷燬這些方塊。我們已經添加了一些代碼來追蹤碰撞,因此,我們對tick方法做一改動。
具體改動方式如下:
List<Body> toDestroy = new List<Body>();
foreach (var item in contactListener.contacts)
{
if ((item.fixtureA == bottomFixture && item.fixtureB == ballFixture) ||
(item.fixtureA == ballFixture && item.fixtureB == bottomFixture))
{
GameOverScene pScene = new GameOverScene(false);
CCDirector.sharedDirector().replaceScene(pScene);
}
Body bodyA = item.fixtureA.GetBody();
Body bodyB = item.fixtureB.GetBody();
if (bodyA.GetUserData() != null && bodyB.GetUserData() != null)
{
CCSprite spriteA = (CCSprite)bodyA.GetUserData();
CCSprite spriteB = (CCSprite)bodyB.GetUserData();
//Sprite A = ball, Sprite B = Block
if (spriteA.tag == 1 && spriteB.tag == 2)
{
if (toDestroy.IndexOf(bodyB) == -1)
{
toDestroy.Add(bodyB);
}
}
//Sprite B = block ,Sprite A = ball
else if (spriteA.tag == 2 && spriteB.tag == 1)
{
if (toDestroy.IndexOf(bodyA) == -1)
{
toDestroy.Add(bodyA);
}
}
}
}
foreach (var item in toDestroy)
{
if (item.GetUserData() != null)
{
CCSprite sprite = (CCSprite)item.GetUserData();
this.removeChild(sprite, true);
}
world.DestroyBody(item);
}
好,讓我們解釋一下。我們又一次遍歷所有的碰撞點,但是,這一次在我們測試完籃球和屏幕底部相撞的時候,我們將檢查碰撞點。我們可以通過fixture對象的GetBody方法來找對象。
接着,我們基於精靈的tag,看看到底是哪個在發生碰撞。如果一個精靈與一個body相交的話,我們就把該body添加到待銷燬的對象列表裏面去。
但是也需要注意,只有確定它並不存在於銷燬列表中時才把它添加進去。爲什麼一定要用一個list把需要銷燬的存儲起來而不是直接銷燬。因爲直接銷燬會導致contact listener中留下一些已被刪除指針的垃圾數據。
最後,遍歷我們想要刪除的body列表。
編譯並運行,現在你可以銷燬bricks了!
加入遊戲勝利條件
接下來,我們需要添加一些邏輯,讓用戶能夠取得遊戲勝利。修改你的tick方法的開頭部分,像下面一樣:
bool blockFind = false;
world.Step(dt, 10, 10);
for (Body b = world.GetBodyList(); b != null;b = b.GetNext() )
{
if (b.GetUserData() != null)
{
CCSprite sprite = (CCSprite)b.GetUserData();
if (sprite.tag == 1)
{
int maxSpeed = 10;
Vector2 velocity = b.GetLinearVelocity();
float speed = velocity.Length();
if (speed > maxSpeed)
{
b.SetLinearDamping(0.5f);
}
else if (speed < maxSpeed)
{
b.SetLinearDamping(0.0f);
}
}
else if (sprite.tag == 2)
{
blockFind = true;
}
我們需要做的,僅僅是遍歷一下場景中的所有對象,看看是否還有一個方塊----如果我們確實找到了一個,那麼就把blockFound變量設置爲true,否則就設置爲false.
然後,在這個函數的末尾添加下面的代碼:
if (!blockFind)
{
GameOverScene gameOverScene = new GameOverScene(true);
CCDirector.sharedDirector().replaceScene(gameOverScene);
}
這裏,如果方塊都消失了,我們就會顯示一個遊戲結束的場景。編譯並運行,看看,你的遊戲現在有勝利終止條件了!
完成touch事件
這個遊戲非常酷,但是,毫無疑問,我們需要音樂!你可以下載好聽的blip聲音。(背景音樂自己弄,MP3格式),和之前一樣,在Content工程新建一個resources文件夾,把它添加到你的resources文件夾下。
添加CocosDenshion.dll這個DLL的引用。在tick方法的末尾添加下面的代碼:
if (toDestroy.Count > 0)
{
SimpleAudioEngine.sharedEngine().playEffect(@"resources/blip");
}
終於完成了!你現在擁有一個使用Box2d物理引擎製作的breakout遊戲了
本次工程下載:http://dl.dbank.com/c0a0a5ze3q
何去何從?
很明顯,這是一個非常簡單的beakout遊戲,但是,你還可以在此教程的基礎上實現更多。我可以添加一些邏輯,比如打擊一個白色塊就計一分,或者有些塊需要打中很多下才消失。或者你也可以添加新的不同類型的block,並且讓paddle可以發射出激光等等。你可以充分發揮想象。