使用cocos2d-x和BOX2D來製作一個BreakOut(打磚塊)遊戲(二)

本教程基於子龍山人翻譯的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"!


添加Game Over場景

新建一個類添加到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可以發射出激光等等。你可以充分發揮想象。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章