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

本教程基於子龍山人翻譯的cocos2d的IPHONE教程,用cocos2d-x for XNA引擎重寫,加上我一些加工製作。教程中大多數文字圖片都是原作者和翻譯作者子龍山人,還有不少是我自己的理解和加工。感謝原作者的教程和子龍山人的翻譯。本教程僅供學習交流之用,切勿進行商業傳播。

子龍山人翻譯的Iphone教程地址:http://www.cnblogs.com/zilongshanren/archive/2011/05/27/2059460.html

Iphone教程原文地址:http://www.raywenderlich.com/475/how-to-create-a-simple-breakout-game-with-box2d-and-cocos2d-tutorial-part-12

程序截圖:


box2d是一個非常強大的物理引擎庫,同時它與cocos2d-x結合非常適合在window phone上面做遊戲開發。著名的angry birds,tiny wings都是用box2d寫的。你可以用它做好多事情,當然,最好的學習方法就是使用它來創建一個簡單的遊戲。

  在這個教程中,我們將一步一步創建一個簡單的breakout遊戲,完成碰撞檢測,籃球反彈物理效果,通過touch拖動paddle(就是上圖的白色矩形),以及勝利/失敗的場景。

  如果你還不瞭解cocos2d-x和box2d,你可能先要讀一讀《如何使用cocos2d-x製作一個簡單的window phone 7遊戲》以及《在<cocos2d-x for wp7>在cocos2d-x裏面使用BOX2D》這些教程。

  好了,是時候製作breakout了!

一個永遠反彈的球

打開VS2010。新建一個cocos2d-x項目,命名爲cocos2dBox2DBreakOutDemo,並且做好相關的DLL複製和添加。做法可以參照《在<cocos2d-x for wp7>在cocos2d-x裏面使用BOX2D》

然後新建一個類添加到Classes。並且命名爲BreakoutScene.cs。

並且添加以下空間引用。

using cocos2d;
using Box2D.XNA;

現在先在這個文件添加一個類BreakOutLayer,並且使之繼承於CCLayer。


        public static double PTM_RATIO = 32.0;
        World world;
        Body groundBody;   
        Fixture bottomFixture;
        Fixture ballFixture;

在這些聲明裏面第一個聲明定義比率。這個比率我們在上一個教程中已經討論過了,這裏就不再囉嗦了。

然後重載其init方法。並且修改如下:

        public override bool init()
        {
            if (!base.init())
                return false;
            CCSize winSize = CCDirector.sharedDirector().getWinSize();

            CCLabelTTF title = CCLabelTTF.labelWithString("Boxing", "Arial", 24);
            title.position = new CCPoint(winSize.width / 2, winSize.height - 50);
            this.addChild(title);


            //Create the world
            Vector2 gravity = new Vector2(0.0f, 0.0f);
            bool doSleep = true;
            world = new World(gravity, doSleep);

            //Create edges around the entire screen
            BodyDef groundBodyDef = new BodyDef();
            groundBodyDef.position = new Vector2(0, 0);
            groundBody = world.CreateBody(groundBodyDef);
            PolygonShape groundBox = new PolygonShape();
            FixtureDef boxShapeDef = new FixtureDef();
            boxShapeDef.shape = groundBox;
            groundBox.SetAsEdge(new Vector2(0, 0), new Vector2((float)(winSize.width / PTM_RATIO), 0));
            bottomFixture = groundBody.CreateFixture(boxShapeDef);
            groundBox.SetAsEdge(new Vector2(0, 0), new Vector2(0, (float)(winSize.height / PTM_RATIO)));
            groundBody.CreateFixture(boxShapeDef);
            groundBox.SetAsEdge(new Vector2(0, (float)(winSize.height / PTM_RATIO)),
                new Vector2((float)(winSize.width / PTM_RATIO), (float)(winSize.height / PTM_RATIO)));
            groundBody.CreateFixture(boxShapeDef);
            groundBox.SetAsEdge(new Vector2((float)(winSize.width / PTM_RATIO), (float)(winSize.height / PTM_RATIO)),
                new Vector2((float)(winSize.width / PTM_RATIO), 0));
            groundBody.CreateFixture(boxShapeDef);
            return true;
        }

好,這個代碼和我們上一個教程中,爲整個屏幕創建一個盒子邊界差不多。然後,這一次,我們把重力設置爲0,因爲,在我們的breakout遊戲中,我們並不需要重力!注意,我們存儲了底部的fixture的一個指針,以方便後面使用(在後面的教程中,我們將用來追蹤什麼時候籃球與頂部相碰撞了)。

現在,下載我製作的籃球圖片,並且添加到Content工程的images文件夾中,讓我們往場景裏面添加一個精靈吧。緊接着上面的代碼,加入下面的代碼片段:

            //Create sprite and add it to the layer
            CCSprite ball = CCSprite.spriteWithFile(@"images/Ball");
            ball.position = new CCPoint(100, 100);
            ball.tag = 1;
            this.addChild(ball);

  這裏沒什麼疑問,我們已經做過好多次類似的事情了。注意,我們爲籃球設置了一個tag標識,後面你會看到,這個tag標記有什麼用。

  接下來,爲shape創建一個body:

           //Create ball body 
            BodyDef ballBodyDef = new BodyDef();
            ballBodyDef.type = BodyType.Dynamic;
            ballBodyDef.position = new Vector2((float)(100 / PTM_RATIO), (float)(100 / PTM_RATIO));
            ballBodyDef.userData = ball;
            Body ballBody = world.CreateBody(ballBodyDef);

            //Create circle shape
            CircleShape circle = new CircleShape();
            circle._radius = (float)(26.0 / PTM_RATIO);

            //Create shape definition and add to body
            FixtureDef ballShapeDef = new FixtureDef();
            ballShapeDef.shape = circle;
            ballShapeDef.density = 1.0f;
            ballShapeDef.friction = 0.0f;
            ballShapeDef.restitution = 1.0f;
            ballFixture = ballBody.CreateFixture(ballShapeDef);

  這個看起來和上一篇教程中的也很像。再鞏固一下吧,爲了創建一個body對象,我們先要創建一個body定義結構,然後再創建body,接着是shape,再指定fixture結構,最後是創建fixture對象。

  注意,我們設置這些參數有一點點不一樣了:我們把回覆力(restitution)設置爲1.0,這意味着,我們的球在碰撞的時候,將會是完全彈性碰撞。

  同時,我們也保存了球的fixture,原因和我們爲什麼保存屏幕底部的fixture是一樣的,後面你就會看到了。

  更新:注意,我們也把球的摩擦力設置爲0.這樣可以防止球在碰撞的時候,由於摩擦損失能量,導致來回碰撞的過程中會有一點點偏差。

  好了,是時候做一些完全不同的事了!緊接上面的代碼:

            Vector2 force = new Vector2(10, 10);
            ballBody.ApplyLinearImpulse(force, ballBodyDef.position);

  這裏往球上面施加了一個衝力(impulse),這樣可以讓它初始化的時候朝一個特定的方向運動。

  最後一件事情,就是在init方法中,增加一個tick調度方法:

this.schedule(tick);

下面是tick方法的實現:

        void tick(float dt)
        {
            world.Step(dt, 10, 10);
            for (Body b = world.GetBodyList(); b != null;b = b.GetNext() )
            {
                if (b.GetUserData() != null)
                {
                    CCSprite sprite = (CCSprite)b.GetUserData();
                    sprite.position = new CCPoint((float)(b.GetPosition().X * PTM_RATIO),
                        (float)(b.GetPosition().Y * PTM_RATIO));
                    sprite.rotation = -1 * MathHelper.ToDegrees(b.GetAngle());
                }
            }
        }

當然,這裏也和上一個教程中的一樣,沒有什麼特別的。

現在,在運行測試之前。還得做些工作。

往BreakOutLayer裏面添加一個靜態方法來作爲初始化層工作。

        public static new BreakOutLayer node()
        {
            BreakOutLayer layer = new BreakOutLayer();
            if (layer.init())
            {
                return layer;
            }
            return null;
        }

然後讓原來沒用動過的BreakOutCCScene類繼承於CCScene。並且在其構造函數中添加一個BreakOutLayer。

構造函數添加以下代碼:

            BreakOutLayer layer = BreakOutLayer.node();
            this.addChild(layer);

接着修改AppDelegate這個導演類。找到applicationDidFinishLaunching方法。結尾部分修改如下:

            // create a scene. it's an autorelease object
            //CCScene pScene = cocos2dBox2DBreakOutDemoScene.scene();
            Classes.BreakoutScene pScene = new Classes.BreakoutScene();
            //run
            pDirector.runWithScene(pScene);

            return true;


好了,讓我們試一下吧。編譯並運行工程,你將會看到一個球無限地在屏幕裏面來回彈!----很酷吧!



增加 Paddle:

如果沒有一個paddle的話,那麼就不可能稱其爲一個breakout遊戲。下載http://dl.dbank.com/c0at986ela,並且添加到images文件夾。然後在往BreakOutLayer類中添加下列成員變量:

        Body paddleBody;
        Fixture paddleFixture;

然後,在init方法中構建paddle body:


           //Create paddle and add it to the layer
            CCSprite paddle = CCSprite.spriteWithFile(@"images/Paddle");
            paddle.position = new CCPoint(winSize.width / 2, 50);
            this.addChild(paddle);

            //Create paddle body
            BodyDef paddleBodyDef = new BodyDef();
            paddleBodyDef.type = BodyType.Dynamic;
            paddleBodyDef.position = new Vector2((float)(winSize.width / 2 / PTM_RATIO), (float)(50 / PTM_RATIO));
            paddleBodyDef.userData = paddle;
            paddleBody = world.CreateBody(paddleBodyDef);

            //Create paddle shape
            PolygonShape paddleShape = new PolygonShape();
            paddleShape.SetAsBox((float)(paddle.contentSize.width / PTM_RATIO / 2), (float)(paddle.contentSize.height / PTM_RATIO / 2));

            //Create shape definition and add to body
            FixtureDef paddleShapeDef = new FixtureDef();
            paddleShapeDef.shape = paddleShape;
            paddleShapeDef.density = 10.0f;
            paddleShapeDef.friction = 0.4f;
            paddleShapeDef.restitution = 0.1f;
            paddleFixture = paddleBody.CreateFixture(paddleShapeDef);

  我不想花太多的時間解釋上面的內容了。因爲,和之前的創建籃球的body的過程差不太多。這裏只給出不同的地方:

  • 當你創建CCSprite的時候,你並不需要指定精靈的大小。如果你傳遞一個文件名給它,它會自動計算出大小。
  • 注意,這裏不是使用circle shape了。這一次,我們使用polygon shape。我們使用一個輔助方法來創建shape,當然,其形狀是個盒子。
  • 我們使用了SetAsBox方法來指定shape相對於body的位置,這個方法在構建複雜的對象的時候比較有用。這裏,我們只是讓shape在body中間。
  • 我把paddle的密度設置得比球要大得多,同時調節了一下其它的參數。(這些參數要靠試,按照真實的高中物理知識去計算,可能得不到)
  • 同時,我們存儲paddleBody和paddleFixture的引用,爲了方便後面使用。

  如果你編譯並運行的話,你將會看到屏幕中間有一個paddle,而且球碰到它將會反彈。


  然後,這還不是很有趣,因爲我們還不能移動paddle!

移動Paddle

  移動paddle需要touch事件,所以先在init方法中允許touch事件:

            this.isTouchEnabled = true;

然後,在BreakOutLayer類中添加下面的成員變量:

        MouseJoint mouseJoint = null;

現在,讓我們實現touch方法!首先是ccTouchesBegan:

        public override void ccTouchesBegan(List<CCTouch> touches, CCEvent event_)
        {
            if (mouseJoint != null)
                return;
            CCTouch myTouch = touches.FirstOrDefault();
            CCPoint location = myTouch.locationInView(myTouch.view());
            location = CCDirector.sharedDirector().convertToGL(location);
            Vector2 locationWorld = new Vector2((float)(location.x / PTM_RATIO), (float)(location.y / PTM_RATIO));

            if (paddleFixture.TestPoint(locationWorld))
            {
                MouseJointDef md = new MouseJointDef();
                md.bodyA = groundBody;
                md.bodyB = paddleBody;
                md.target = locationWorld;
                md.collideConnected = true;
                md.maxForce = 1000.0f * paddleBody.GetMass();

                mouseJoint = (MouseJoint)world.CreateJoint(md);
                paddleBody.SetAwake(true);
            }
        }

 呃,好多新知識!讓我們一點一點來討論。

  首先,我們把touch座標轉換成coocs2d座標(convertToGL)然後,再轉換成Box2d座標(locationWorld)。

  然後,我們使用paddle fixture的一個方法來測試這個touch點是否在fixture內部。

  如果是的話,我們就創建一個所謂的”鼠標關節“。在Box2d裏面,一個鼠標關節用來讓一個body朝着一個指定的點移動---在這裏個例子中,就是用戶點的方向。

  當你創建一個mouse joint後,你賦值給它兩個body。第一個沒有被使用,通常都是設置成ground body。第二個,就是你想讓它移動的body,在這個例子中就是paddle。

  接下來,你指定移動的終點---這個例子中就是用戶點擊的位置。

  然後,你告訴box2d,但bodyA和bodyB碰撞的時候,把它當成是碰撞,而不是忽略它。這個很重要!因爲,我之前沒有設置它爲ture,結果不行!因此,當我們用鼠標拖動這個paddle的時候,它並不會與屏幕的邊界相碰撞,而且有時候,我的paddle直接就飛出屏幕之外了。這個非常非常奇怪,不過我現在知道是爲什麼了。因爲沒有設置bodyA和bodyB是可碰撞的。

  你然後指定移動body的最大的力是多少。如果你減少這個數值的話,paddle body響應鼠標移動時就會慢一些。但是,我們想讓paddle快速地響應鼠標的變化。

  最後,我們把這個關節加入到world中,同時,保存這個指針,因爲後面有用。同時,我們還要把body設置成甦醒的(awake)。之所以要這麼做,是因爲如果body在睡覺的話,那麼它就不會響應鼠標的移動!

  好了,接下來,讓我們添加ccTouchesMoved方法:

        public override void ccTouchesMoved(List<CCTouch> touches, CCEvent event_)
        {
            if (mouseJoint == null)
                return;
            CCTouch myTouch = touches.FirstOrDefault();
            CCPoint location = myTouch.locationInView(myTouch.view());
            location = CCDirector.sharedDirector().convertToGL(location);
            Vector2 locationWorld = new Vector2((float)(location.x / PTM_RATIO), (float)(location.y / PTM_RATIO));
            mouseJoint.SetTarget(locationWorld);
        }

  這個方法的開頭部分和ccTouchesBegan差不多---我們把touch座標轉換成Box2d座標。唯一的區別就是,我們更新了鼠標關節的目標位置(也就是我們想讓paddle移動的位置的)。

  接下來,我們添加ccTouchesCacelled和ccTouchesEnded方法:

        public override void ccTouchesCancelled(List<CCTouch> touches, CCEvent event_)
        {
            if (mouseJoint != null)
            {
                world.DestroyJoint(mouseJoint);
                mouseJoint = null;
            }    
        }

        public override void ccTouchesEnded(List<CCTouch> touches, CCEvent event_)
        {
            if (mouseJoint != null)
            {
                world.DestroyJoint(mouseJoint);
                mouseJoint = null;
            }
        }

  我們在這些方法中做的只有一件事,就是在我們移動完paddle或者取消移動之後銷燬mouse joint。

  編譯並運行,你現在可以用鼠標移動paddle了,同時可以讓它與籃球相互碰撞了!

 很好。。。不過,等一下,這還不是一個breakout!我們不可以把paddle移動到任何位置,我們只能在屏幕底部左右來回移動它!

限制Paddle的移動

  我們可以很容易地限制paddle的移動,只需要添加另外一個關節,叫做prismatic joint。這個關節會限制一個body的移動沿着一根指定的軸。

  因此,我們可以使用這種方法來限制paddle相對於地面移動,也就是說只能沿着x軸移動。

  讓我們看看相關代碼。往init方法中加入下列代碼:

            //Restrict paddle along the x axis
            PrismaticJointDef jointDef = new PrismaticJointDef();
            Vector2 worldAxis = new Vector2(1.0f, 0.0f);
            jointDef.collideConnected = true;
            jointDef.Initialize(paddleBody, groundBody, paddleBody.GetWorldCenter(), worldAxis);
            world.CreateJoint(jointDef);

  第一件事情就是指定一個沿着x軸的向量。然後,我們需要指定collideConnected爲true,因此,我們的球才能夠正確的反彈,而不是飛到屏幕之外去。

  然後,初始化關節,指定paddle和ground兩個body,再使用world對象來創建關節!

  編譯並運行,你現在只能沿關x軸方向移動paddle了,這正是我們想要的,不是吧?

完成touch事件

  現在,你玩一下,可能你會發現,有時候球反彈地特別快,有時候又比較慢。這取決於你是如何控制paddle與球相碰撞的。

  原作者理論:我第一次嘗試去修正這個bug的時候,我通過直接調整球的速度,使用SetLinearVelocity方法。然後,Steve Oldmeadow也指出,這非常不好!它會破壞物理仿真,最好的方法是通過調用SetLinearDamping方法,間接影響速度。因此,現在這個教程就是這個做的。(damping就是阻尼的意思)

  接下來,在tick方法中添加下列代碼,具體位置是在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);
                        }
                    }

  這裏,我們判斷sprite的tag,看是否是球的tag。如果是的話,我們就檢查它的速度,如果太大的話,就設置它的阻尼爲0.5,這樣可以讓它慢下來。如果對速度啥的還不滿意,可以調節maxSpeed,Damping值

  如果你編譯並運行的話,你將會看到一個球以非常適中的速度在屏幕四周來回反彈。


本次工程下載:http://dl.dbank.com/c0x042rumn,這只是一部分,第二部分的教程會包含一個完整的breakout的源碼。

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