使用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可以發射出激光等等。你可以充分發揮想象。

 

 PS:令人傷心的所謂搬家工具。。。搞得顯示亂七八糟的。。。。。。又得手動重來。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM