使用cocos2d-x製作一個太空射擊遊戲

本次教程參考文章:http://www.cnblogs.com/zilongshanren/archive/2011/06/09/2074962.html
  本次教程做一個空戰遊戲,這個遊戲類型相信大家玩小霸王(FC)的時候都玩過,就是一個滾動的畫面,畫面上差不多半屏都是怪物,玩家控制的飛機可以自由移動並且發射麪條槍。當然,最後有一個BOSS,雖然BOSS有時候相當弱智(沒辦法,太難小時候也玩不過)。這次做的示例,用到了一些前面教程沒用過的東西,重力感應控制飛機,視差滾動製作,粒子系統的簡單使用。
  另外,做教程有一段時間了。估計不少朋友也是剛入門,什麼Action這類東西怎麼寫還真是不怎麼懂,推薦去研究官方安裝包自帶的Test工程,那裏有詳細的運用方法。這個沒有在一開始做教程的時候提到,還真是有點失誤。我這裏主要想提供些實例教程,這些怎麼做還不想去詳細講,畢竟有Test工程這個很好的實例了。
程序截圖:




  本次教程假定你已經學習過前面的《用cocos2d-x做一個簡單的windows phone 7遊戲》系列教程,或者你擁有同等的經驗。


  下載這次教程需要的資源包(http://dl.dbank.com/c0h3bugf3a)。這個資源包括所有的圖片,一些音效,並且粒子系統用到的三個plist文件。

    那麼開始吧。


添加一個太空船

  現在新建一個工程,命名爲cocos2dSpaceGameDemo,當然,畢竟是練習項目,所以OpenXLive不用。新建完項目之後,修復DLL是必須的。然後添加一個類SpaceBattleScene到Classes文件夾。並且修改代碼如下:

class SpaceBattleScene:CCScene
    {
        public SpaceBattleScene()
        {
            CCLayerColor colorLayer = CCLayerColor.layerWithColor(new ccColor4B(Color.Black));
            this.addChild(colorLayer, -1);
            this.addChild(SpaceBattleLayer.node());
        }
    }


    class SpaceBattleLayer : CCLayer
    {
        public override bool init()
        {
            if (!base.init())
                return false;
            return true;
        }


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

並且修改AppDelegate類的Launch事件,如下:

            //CCScene pScene = cocos2dSpaceGameDemoScene.scene();
            SpaceBattleScene pScene = new SpaceBattleScene();
            //run
            pDirector.runWithScene(pScene);

上面新建了一個空的工程,並且在戰鬥場景中添加了一個黑色的背景層,畢竟太空還是黑色的。

現在來添加一個spaceship吧。這次用的是CCSpriteBatchNode來添加和管理精靈,而且這個可以優化精靈。具體請看<cocos2d-x for wp7>在cocos2d-x使用spritesheet和用spritesheet創建動畫

那麼把AllSprite.plist文件放置在resource目錄下,AllSprite.png圖片放置在resource/images目錄下。並且修改AllSprite.plist的屬性中的contentProcessor和ContentImporter分別爲TextProcessor和TextImporter。

添加一些聲明到SpaceBattleLayer類中:

        const int shipTag = 1;
        const int attackShipTag = 2;
        const int bulletTag = 3;
        const int bossTag = 4;
        const int lifefoodTag = 5;
    
     CCSpriteBatchNode batchNode;
     
       CCSprite ship;
       CCSize winSize = CCDirector.sharedDirector().getWinSize();

這些是一些TAG的變量。下面要使用到。

添加如下代碼到SpaceBattleLayer的init方法中,

            batchNode = CCSpriteBatchNode.batchNodeWithFile(@"resource/images/Allsprite");
            this.addChild(batchNode);
            CCSpriteFrameCache.sharedSpriteFrameCache().addSpriteFramesWithFile(@"resource/Allsprite");
            ship = CCSprite.spriteWithSpriteFrameName(@"SpaceShip.png");
            ship.position = new CCPoint(winSize.width / 10, winSize.height / 2);
            ship.tag = shipTag;
            batchNode.addChild(ship, 1);

讓我們一句一句地解釋上面的代碼:
  • 使用一張大的圖片創建一個CCSpriteBatchNode對象來批處理所有的對象的描繪操作。
  • 把CCSpriteBatchNode添加到當前層裏面去,這樣就可以繪製它的所有的孩子對象。
  • 加載Sprites.plist文件,它裏面包含了這張大圖裏面的所有的小圖的位置座標信息。這樣,你以後可以非常方便地使用 spriteWithSpriteFrameName來提取一張張小圖片來初使化一些精靈。
  • 使用 SpaceShip.png圖片來創建一個精靈,注意這張圖片是大圖裏的一個子圖。
  • 使用CCDirector來獲得屏幕的大小---我們接下來會用到這個大小。
  • 設置飛船的位置在屏幕寬度的10%,高度的50處。注意,飛船的中心點位置默認是飛船的中心。
  • 把ship當作batchNode的一個孩子添加進去,這樣的話,這些精靈就會被批處理顯示出來。
編譯運行就可以看到spaceship停靠在左邊了。





 添加視差滾動

  我們已經有一個很酷的飛船在屏幕上了,但是,它看起來就好像坐在那裏一樣,毫無生氣!我們可以通過往裏面添加視差滾動背景來解決這個問題。

  但是,等一下,到底什麼是視差滾動了?

  視差滾動,簡單來說,就是“移動背景中的一些圖片比其它圖片慢一點點”,打個比方,一個背景中的物體有遠有近,近的背景移動地快(比如地面),遠的背景移動地慢(比如天空),這樣子就會形成景深不一樣的視差效果出來。

  不過,在XNA現版本的cocos2d-x中,CCParallaxNode這個做視差滾動的類貌似沒完成,Test工程中也沒有相關例子。這樣情況下,我也不打算去使用這個類,因爲我不打算去修改其源代碼。

  那麼,視差滾動怎麼做呢,不過想想,就是一些不一致的移動來引起視差效果,簡單來說就是一些欺騙眼睛的玩意,學過物理的都知道,參照物的問題,如果一個相向移動的物體,以其移動的物體爲參照物,那麼不動的東西我們都感覺其動起來了。

  所以,我們只要添加一些東西其相對移動的效果就好了。

  添加bg_galaxy.png和bg_planetsunrise.png這兩個文件到background文件夾。然後添加一些方法到SpaceBattleLayer中。

        void initBackground()
        {
            CCSprite galaxy = CCSprite.spriteWithFile(@"background/bg_galaxy");
            galaxy.position = new CCPoint(1500, winSize.height / 2);
            galaxy.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(20, new CCPoint(0, winSize.height * 0.3f)),
                CCCallFuncN.actionWithTarget(this, galaxyMoveDone)));
            this.addChild(galaxy, -1);
            CCSprite planetsunrise = CCSprite.spriteWithFile(@"background/bg_planetsunrise");
            planetsunrise.position = new CCPoint(900, 0);
            planetsunrise.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(12, new CCPoint(0, 0)),
                CCCallFuncN.actionWithTarget(this, planetMoveDone)));
            this.addChild(planetsunrise, -1);
        }


        void galaxyMoveDone(object sender)
        {
            CCSprite sprite = sender as CCSprite;
            sprite.position = new CCPoint(1500, winSize.height * 0.3f);
            sprite.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(20, new CCPoint(0, winSize.height * 0.3f)),
                CCCallFuncN.actionWithTarget(this, galaxyMoveDone)));
        }


        void planetMoveDone(object sender)
        {
            CCSprite sprite = sender as CCSprite;
            sprite.position = new CCPoint(900, 0);
            sprite.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(12, new CCPoint(0, 0)),
                CCCallFuncN.actionWithTarget(this, planetMoveDone)));
        }

上面做了些什麼呢,添加了兩個背景精靈,兩個距離屏幕挺遙遠的,不過它們在移動,移動進入屏幕,出去屏幕後重新設定其爲原來位置重新移動,這樣,這兩個精靈(黑洞和星球)就無限次的從屏幕右邊移動到左邊。

在init方法最下面添加代碼:
 initBackground();

編譯運行,當然,現在看起來飛船感覺已經在動了。





添加星星

  現在我們來添加星星,用到一個我們以前教程從來沒用過的東西---粒子系統。這個系統有什麼優勢呢,可以實現實現一些真實的效果,這些效果(如,火焰,雪花等)都是由大量微粒組合而形成的。cocos2d-x也提供了挺多的封裝,比如火焰,學壞等效果,具體可以看Test工程關於例子系統的部分。在這裏只是簡單使用下。

  另外,粒子系統裏面有太多的參數,這些要理解起來挺費勁的。這裏提供兩張列表來參考理解。


 



  現在,先把Particle文件夾添加到Content工程中,和上面的一樣,那些plist文件要設置其屬性的ContentProcessor和ContentImporter。這些plist文件是幹什麼用的呢,在plist文件裏面,定義了粒子系統所需要的屬性,這些plist文件是用粒子系統編輯器做的,可惜的是,我沒找到在window下用的粒子系統編輯器,如果有朋友有,麻煩提供個。

  這些plist文件原來是子龍山人翻譯的文章原作者ray做的。我沒有編輯器,我就直接打開plist文件硬編碼我需要修改的數據。不過,圖片是我畫的,PS水平在會用的程度就是這個水平了。沒辦法,沒有美工的日子。。。。

  那麼,現在在InitBackground方法添加代碼:

            List<string> starsList = new List<string>() { "Particles/Stars1", "Particles/Stars2", "Particles/Stars3" };
            foreach (string stars in starsList)
            {
                CCParticleSystemQuad starsEffect = CCParticleSystemQuad.particleWithFile(stars);
                this.addChild(starsEffect, -1);
            }

上面的代碼,就是根據plist文件初始化了粒子系統,然後添加到層中。如果你查看plist文件裏面的定義,其實也挺簡單的定義,就是定義了些粒子發射的速度,生命週期等內容。速度這類都是以像素爲單位。剩下的設定可以參照上面列表來理解應該不難了。

現在編譯運行,可以看到滿天的星星了。




滿天的怪物

  曾經玩FC的這類射擊遊戲的時候,不知道大家有沒有思考這麼一個問題,怪物是怎麼每次都在一定的地點出現?是背景的關係?其實細想,背景的滾動,也是時間的流逝,那麼規定在特定的時間怪物出現,和在一定背景怪物出現的設定其實是一樣的。所以,在這個遊戲中,怪物的出現是有時間來決定的。而且,這個遊戲的時長設定爲60秒,60秒後,BOSS出現,打倒BOSS戰鬥就勝利了。
  那麼來添加怪物吧。
  先添加一個聲明:
CCSprite BOSS;

下面添加幾個方法:

        void initMonster1()
        {


            for (int i = 0; i < 5; i++)
            {
                CCSprite monster1 = CCSprite.spriteWithSpriteFrameName(@"monster1.png");
                monster1.tag = attackShipTag;


                monster1.position = new CCPoint(winSize.width + monster1.contentSize.width / 2 + i * (monster1.contentSize.width + 30),
                                    winSize.height * 0.7f);
                float time = (monster1.position.x - 200) / 200;
                monster1.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(time, new CCPoint(winSize.width / 4, winSize.height * 0.7f)),
                    CCDelayTime.actionWithDuration(0.2f),
                    CCMoveTo.actionWithDuration(4, new CCPoint(winSize.width * 0.75f, -monster1.contentSize.height / 2)),
                    CCCallFuncN.actionWithTarget(this, spriteMoveDone)));
                batchNode.addChild(monster1);
                CCSprite monster2 = CCSprite.spriteWithSpriteFrameName(@"monster1.png");
                monster2.tag = attackShipTag;


                monster2.position = new CCPoint(winSize.width + monster2.contentSize.width / 2 + i * (monster2.contentSize.width + 30),
                                    winSize.height * 0.3f);
                monster2.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(time, new CCPoint(winSize.width / 4, winSize.height * 0.3f)),
                    CCDelayTime.actionWithDuration(0.2f),
                    CCMoveTo.actionWithDuration(4, new CCPoint(winSize.width * 0.75f, winSize.height + monster2.contentSize.height / 2)),
                    CCCallFuncN.actionWithTarget(this, spriteMoveDone)));
                batchNode.addChild(monster2);
            }
        }


        void initMonster2()
        {
            Random random = new Random();
            for (int i = 0; i < 5; i++)
            {
                CCSprite monster2 = CCSprite.spriteWithSpriteFrameName(@"monster2.png");
                int randomSize = random.Next() % 10;
                monster2.tag = attackShipTag;
                float firstPosY = (winSize.height * randomSize / 10 + 150 + i * 70) % winSize.height;
                float secondPosY = (firstPosY + i * 80) % winSize.height;
                monster2.position = new CCPoint(winSize.width + monster2.contentSize.width / 2, winSize.height * randomSize / 10);
                monster2.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(4, new CCPoint(100, firstPosY)),
                    CCDelayTime.actionWithDuration(0.2f),
                    CCMoveTo.actionWithDuration(3, new CCPoint(winSize.width + monster2.contentSize.width / 2, secondPosY))));
                batchNode.addChild(monster2);
            }
        }


        void initBOSS()
        {


            BOSS = CCSprite.spriteWithSpriteFrameName(@"EnemySpaceShip.png");
            BOSS.position = new CCPoint(winSize.width + BOSS.contentSize.width / 2, winSize.height / 2);
            BOSS.tag = bossTag;
            float x = winSize.width - BOSS.contentSize.width / 2;
            batchNode.addChild(BOSS);
            var move = CCSequence.actions(
                    CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.7f)),
                    CCCallFunc.actionWithTarget(this, bossShoot),
                    CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, winSize.height - BOSS.contentSize.height / 2)),
                    CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.7f)),
                    CCCallFunc.actionWithTarget(this, bossShoot),
                    CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, winSize.height / 2)),
                    CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.3f)),
                    CCCallFunc.actionWithTarget(this, bossShoot),
                    CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, BOSS.contentSize.height / 2)),
                    CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.3f)),
                    CCCallFunc.actionWithTarget(this, bossShoot),
                    CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, winSize.height / 2))
                    );
            BOSS.runAction(CCSequence.actions(
                CCMoveTo.actionWithDuration(2, new CCPoint(x, winSize.height / 2)),
                CCDelayTime.actionWithDuration(0.5f),
                CCRepeat.actionWithAction(move,1000)
                ));
        }


        void bossShoot()
        {
            CCSprite bossBullet = CCSprite.spriteWithSpriteFrameName(@"BOSSbullet1.png");
            bossBullet.position = new CCPoint(BOSS.position.x, BOSS.position.y);
            bossBullet.tag = attackShipTag;
            bossBullet.runAction(CCSequence.actions(
                CCMoveTo.actionWithDuration(3, new CCPoint(-bossBullet.contentSize.width / 2, bossBullet.position.y)),
                CCCallFuncN.actionWithTarget(this, spriteMoveDone)));


            batchNode.addChild(bossBullet);
        }

在BOSS之前,我們有兩種怪物,其實就是前面用到的兩個簡單怪物。

想想以前的遊戲,怪物當然不只是前進一個動作,往往都有一些其他的動作,所以上面的代碼就做了這麼一些動作。

在initMonster1方法中,定義了兩組怪物,每組5個,動作都是從右邊屏幕進入直線運動到一定位置,然後一定角度折線返回。當然,爲了讓人覺得是從右邊屏幕進入,那麼其位置的X值要大於屏幕的值。而且,爲了其不會重疊並且排隊進入,定義了一樣的速度並且兩個之間間隔一些距離。這樣的兩組怪物,大家可以直接在init方法中添加initMonster1()來測試其效果。

在initMonster2方法中,用到的是和monster1一樣的顏色不一樣的怪物。他做的動作從屏幕右邊隨機一個點進到一定距離的一個隨機點然後折返回到屏幕右邊的一個隨機點,求餘是爲了其高度不至於超過屏幕高度。當然,你可以直接在init()方法中測試其效果。

在initBOSS()方法中,這裏的Action看起來有點複雜,其實做的效果也挺簡單的,BOSS一開始要從屏幕右邊進入到靠在屏幕右邊上,然後做一些上下移動的動作,然後在兩個位置發射紅色的大面條槍。並且重複這些動作。當然,也能夠在init()方法中測試其效果。

  現在,添加一些變量:
        float gameTime = 0;
        int hp = 10;
        int BOSShp = 20;
        List<CCSprite> hpSprites;

gameTime是計算時間,另外hpSprites來保存血條的。

添加一個方法:

       void initShipHP()
        {
            hpSprites = new List<CCSprite>();
            for (int i = 0; i < 10; i++)
            {
                CCSprite hpSpriteBlock = CCSprite.spriteWithSpriteFrameName(@"life.png");
                hpSpriteBlock.position = new CCPoint(hpSpriteBlock.contentSize.width / 2 + i * hpSpriteBlock.contentSize.width,
                    winSize.height - hpSpriteBlock.contentSize.height / 2);
                batchNode.addChild(hpSpriteBlock);
                hpSprites.Add(hpSpriteBlock);
            }
        }

在init方法添加

initShipHP();

這樣,在屏幕的左上角就能看到10個小方塊,代表血條。

接下來添加一個方法:

      void initHpfood()
        {
            CCSprite hpfood = CCSprite.spriteWithSpriteFrameName(@"lifefood.png");
            hpfood.tag = lifefoodTag;
            hpfood.position = new CCPoint(winSize.width + hpfood.contentSize.width / 2, winSize.height * 0.7f);
            hpfood.runAction(CCSequence.actions(
                CCMoveTo.actionWithDuration(10.0f, new CCPoint(0, winSize.height * 0.7f)),
                CCCallFuncN.actionWithTarget(this, spriteMoveDone)));
            batchNode.addChild(hpfood);
        }

這個方法是幹什麼的呢,我們在遊戲中往往有一些東西,可以增加些東西,在這個遊戲中,我們設定了這個圖標代碼血,只要能夠喫到就能夠增加一滴血,這個只是初始化。血塊也是從屏幕右邊往左邊移動。

下面添加一個代碼來管理遊戲流程:

        void gameSchedule(float dt)
        {
            gameTime += dt;
            if ((int)gameTime >= 60)
            {
                initBOSS();
            }
            else if ((int)gameTime % 6 == 0)
            {
                initMonster1();
            }
            else if ((int)gameTime % 10 == 0)
            {
                initMonster2();
            }
            else if ((int)gameTime == 11 || (int)gameTime == 33)
            {
                initHpfood();
            }
        }

這個方法定義每6秒怪物1出現一次,每10秒怪物2出現一次,在11秒和33秒的時候,血塊出現,60秒BOSS出現。

然後在init()添加:         
this.schedule(gameSchedule, 1.0f);

設置爲一秒運行一次。

下面添加一行在initBOSS開始處:
this.unschedule(gameSchedule);

定義在BOSS出現的時候,遊戲進行到了最後,那麼遊戲流程函數就取消週期執行了。

現在運行, 就可以看到我們定義的遊戲流程。大批的怪物,傻逼的BOSS。而且在BOSS出現後,小怪都不會出現了


 飛船射擊

  當然,不能一個勁的捱打(雖然沒打中),我們來添加射擊吧。雖然是可惡的麪條槍(我印象中,麪條槍在這麼多射擊遊戲是最菜的)。
  添加代碼:

        void shoot()
        {
            CCSprite bullet = CCSprite.spriteWithSpriteFrameName(@"weapon.png");
            bullet.tag = bulletTag;
 
            bullet.position = new CCPoint(ship.position.x, ship.position.y);
            bullet.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(3, new CCPoint(winSize.width, ship.position.y)),
                CCCallFuncN.actionWithTarget(this, spriteMoveDone)));
            batchNode.addChild(bullet);
        }
       public override void ccTouchesEnded(List<CCTouch> touches, CCEvent event_)
        {
            shoot();
        } 


    //in the init method
    this.isTouchEnabled = true;

上面的代碼,我們註冊了觸摸事件,並且在觸摸結束的時候,發射麪條,當然,子彈都是從Spaceship位置水平射向右邊。

現在,編譯運行,太空船就能射擊了。


添加重力感應

  單靠射擊並不能完全防衛,打不過還得跑。現在添加重力感應,用重力加速器來移動Spaceship。在下面,進行的都是真機測試,沒有機子的朋友,雖然電腦也能模擬重力感應,但是控制起來相當麻煩。我也沒其他辦法用模擬器來完成這個測試。
  重力加速器,在cocos2d-x這個版本中並沒有封裝,那麼我們只有用基礎的XNA的方法了。
  添加Microsoft.Devices.Sensors的引用。
  添加一個聲明:

Accelerometer gSensor;

然後在init方法添加代碼:

            if (Accelerometer.IsSupported)
            {
                gSensor = new Accelerometer();
                gSensor.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<AccelerometerReading>>(gSensor_CurrentValueChanged);
                gSensor.Start();
            }   

上面我們先判斷加速器是否可用,然後註冊檢測事件,然後讓加速器啓動。
下面添加方法:

       void gSensor_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
        {
            Vector3 vector3 = e.SensorReading.Acceleration;
            CCPoint newPos = new CCPoint(ship.position.x, ship.position.y);
if (vector3.Y > 0.3)
            {
                newPos.x -= 10;
                if (newPos.x <= ship.contentSize.width / 2)
                {
                    newPos.x = ship.contentSize.width / 2;
                }
                ship.DisplayFrame = CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("SpaceShip.png");
                ship.position = newPos;
            }
            else if (vector3.Y >= -0.3 && vector3.Y <= 0.3)
            {
                ship.DisplayFrame = CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("SpaceShip.png");
            }
            else
            {
                newPos.x += 10;
                if (newPos.x >= winSize.width - ship.contentSize.width / 2)
                {
                    newPos.x = winSize.width - ship.contentSize.width / 2;
                }
                ship.DisplayFrame = CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("SpaceShipAccel.png");
                ship.position = newPos;
            }


            if (vector3.X < -0.15)
            {
                newPos.y -= 7;
                if (newPos.y < ship.contentSize.height / 2)
                {
                    newPos.y = ship.contentSize.height / 2;
                }
                ship.position = newPos;
            }
            else if (vector3.X >= -0.15 && vector3.X < 0.15)
            {
            }
            else
            {
                newPos.y += 7;
                if (newPos.y > winSize.height - ship.contentSize.height / 2)
                {
                    newPos.y = winSize.height - ship.contentSize.height / 2;
                }
                ship.position = newPos;
            }
        }

上面,我們做的是在加速器值改變的時候改變Spaceship的位置,上面還有一個特點就是在Spaceship往右邊移動的時候,換了一張圖片。一張顯示船加速的圖片。在不是加速的情況下換回來原來的圖片。

這個,如果有圖片的話,我們所有方向的動作都可以有不同的顯示了。

再添加一個方法:
        public override void onExit()
        {
            if(gSensor != null)
                gSensor.Stop();
            base.onExit();
        }

當離開的時候,把加速器關了,雖然不知道是否是必要的,但是關了還是安全些的。

 現在運行,在真機上面,可以不錯的移動了。並且,可以隨意的射擊了。



 碰撞檢測

  現在,雖然能夠射擊了,並且已經有大量的怪物,但是,是不是少了點什麼,子彈碰到怪物它居然不死亡,並且,怪物碰到Spaceship也不掉血。現在我們就來完成這個吧。碰撞檢測。

  在這裏,我打算用BOX2D做碰撞檢測,畢竟現在我們擁有了大量的怪物,大量的子彈,並且是不定性的參與戰鬥,如果用普通的方法來做碰撞檢測的話,精靈管理起來就相當麻煩,檢測的時候一遍遍的遍歷我也覺得挺麻煩的,容易出錯。不過,用BOX2D就不會發生這個情況。

  如果您對BOX2D不瞭解,或者從來也沒有使用過,可以到我博客看<cocos2d-x for wp7>在cocos2d-x裏面使用BOX2D<cocos2d-x for wp7>使用box2d來做碰撞檢測(且僅用來做碰撞檢測)這類文章。

  現在,先添加BOX2D.XNA.DLL的引用。

添加聲明:

        const double PTM_RATIO = 32.0;
        World world;
在init方法開頭初始化world。

            Vector2 gravity = new Vector2(0, 0);
            bool doSleep = false;
            world = new World(gravity, doSleep);

這些東西在<cocos2d-x for wp7>使用box2d來做碰撞檢測(且僅用來做碰撞檢測)都做過解釋,在這裏就不多說了。創建了一個世界,使之重力爲0;

添加方法:

       void addBoxBodyForSprite(CCSprite sprite)
        {
            BodyDef spriteBodyDef = new BodyDef();
            spriteBodyDef.type = BodyType.Dynamic;
            spriteBodyDef.position = new Vector2((float)(sprite.position.x / PTM_RATIO), (float)(sprite.position.y / PTM_RATIO));
            spriteBodyDef.userData = sprite;
            Body spriteBody = world.CreateBody(spriteBodyDef);


            PolygonShape spriteShape = new PolygonShape();
            spriteShape.SetAsBox((float)(sprite.contentSize.width / PTM_RATIO / 2), (float)(sprite.contentSize.height / PTM_RATIO / 2));
            FixtureDef spriteShapeDef = new FixtureDef();
            spriteShapeDef.shape = spriteShape;
            spriteShapeDef.density = 10.0f;
            spriteShapeDef.isSensor = true;
            spriteBody.CreateFixture(spriteShapeDef);
        }

上面的方法是爲精靈創建body。具體解釋碰撞檢測的文章說過,這個不多說。

現在,要爲添加過的精靈創建body了。我們想想,我們要爲那些添加碰撞呢,Spaceship,monster1,Monster2,BOSS,BOSS的大面條槍,Spaceship的小麪條槍,血塊。應該就是這些吧。
那麼,就在init()創建Spaceship的時候,initMonster1()方法,initMonster2()方法,initBOSS()方法,bossShoot()方法,shoot(),initHpfood()方法的設置tag下面添加這麼一行
this.addBoxBodyForSprite(sprite);注意,這裏的參數是要創建body的精靈引用。另外,initMonster1方法中,那裏可是兩隊怪物哦,不要漏了。

我們的sprites被銷燬的時候,我們需要銷燬Box2d的body。因此,把你的spriteMoveDone方法改寫成下面的形式:

        void spriteMoveDone(object sender)
        {
            CCSprite sprite = sender as CCSprite;
            Body spriteBody = null;
            for (Body b = world.GetBodyList(); b != null; b = b.GetNext())
            {
                if (b.GetUserData() != null)
                {
                    CCSprite curSprite = (CCSprite)b.GetUserData();
                    if (sprite == curSprite)
                    {
                        spriteBody = b;
                    }
                }
            }


            if (spriteBody != null)
            {
                world.DestroyBody(spriteBody);
            }
            sprite.visible = false;
        }

現在,最重要的一部分,我們需要根據sprite的位置改變來更新box2d的body。因此,在init方法裏面添加下面一行代碼:

this.schedule(tick);

添加一個tick方法,我們現在是基於cocos2d的精靈位置來更新box2d的body位置。
 
       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();
                    Vector2 position = new Vector2((float)(sprite.position.x / PTM_RATIO), (float)(sprite.position.y / PTM_RATIO));
                    float angle = -1 * MathHelper.ToRadians(sprite.rotation);
                    b.SetTransform(position, angle);
                }
            }
        }

我們將往world對象裏面註冊一個contact listener對象。寫法和之前的一樣,如果你沒寫過,請直接到<cocos2d-x for wp7>使用cocos2d-x和BOX2D來製作一個BreakOut(打磚塊)遊戲(二)裏面複製代碼即可。把MyContactListener類添加到Classes文件夾。

往SpaceBattleLayer類中添加聲明:
MyContactListener contactListener;
然後,在init方法中的world創建後面加入下面的代碼:

            contactListener = new MyContactListener();
            world.ContactListener = contactListener;

最後,在tick方法的底部加入下面的代碼:

List<Body> toDestroy = new List<Body>();
            //foreach (var item in contactListener.contacts)
            for (int i = 0; i < contactListener.contacts.Count;i++ )
            {
                MyContact item = contactListener.contacts[i];
                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();
                    if ((spriteA.tag == shipTag && spriteB.tag == attackShipTag) || (spriteA.tag == attackShipTag && spriteB.tag == shipTag))
                    {
                        if (spriteB.tag == attackShipTag)
                            toDestroy.Add(bodyB);
                        else
                            toDestroy.Add(bodyB);
                        hp--;
                        hpSprites.LastOrDefault().visible = false;
                        hpSprites.Remove(hpSprites.LastOrDefault());
                        if (hp <= 0)
                        {
                            GameOverScene pScene = new GameOverScene(false);
                            CCDirector.sharedDirector().replaceScene(pScene);
                        }
                    }
                    else if ((spriteA.tag == bulletTag && spriteB.tag == attackShipTag) || (spriteB.tag == bulletTag && spriteA.tag == attackShipTag))
                    {
                        toDestroy.Add(bodyA);
                        toDestroy.Add(bodyB);
                    }
                    else if ((spriteA.tag == bulletTag && spriteB.tag == bossTag) || (spriteB.tag == bulletTag && spriteA.tag == bossTag))
                    {
                        BOSShp--;
                        if (spriteA.tag == bulletTag)
                            toDestroy.Add(bodyA);
                        else
                            toDestroy.Add(bodyB);
                        if (BOSShp <= 0)
                        {
                            GameOverScene pScene = new GameOverScene(true);
                            CCDirector.sharedDirector().replaceScene(pScene);
                        }    
                    }
                    else if ((spriteA.tag == shipTag && spriteB.tag == lifefoodTag) || (spriteA.tag == lifefoodTag && spriteB.tag == shipTag))
                    {
                        if (spriteB.tag == lifefoodTag)
                            toDestroy.Add(bodyB);
                        else
                            toDestroy.Add(bodyB);
                        hp++;
                        CCSprite hpSprite = CCSprite.spriteWithSpriteFrameName(@"life.png");
                        hpSprite.position = new CCPoint(hp * hpSprite.contentSize.width - hpSprite.contentSize.width / 2, 
                            winSize.height - hpSprite.contentSize.height / 2);
                        batchNode.addChild(hpSprite);
                        hpSprites.Add(hpSprite);
                    }


                }
                foreach (var itemToDestroy in toDestroy)
                {
                    if (itemToDestroy.GetUserData() != null)
                    {
                        CCSprite sprite = (CCSprite)itemToDestroy.GetUserData();
                        sprite.visible = false;
                    }
                    world.DestroyBody(itemToDestroy);
                }
            }

上面的tick方法我們做了什麼呢,遍歷所有碰撞點,然後判斷兩個碰撞物體的類型,在這裏,你就知道我們在上面設置的tag的作用了。上面判斷了各種碰撞的結果。我覺得代碼應該是可以看懂的,就不解釋了。

現在添加一個類,GameOverScene這個類。來顯示勝利/失敗界面。


代碼如下:

 class GameOverScene:CCScene
    {
        public GameOverScene(bool isWin)
        {
            string msg;
            if (isWin)
                msg = "YOU WIN!";
            else
                msg = "YOU LOSE";
            CCLabelBMFont label = CCLabelBMFont.labelWithString(msg, @"resource/Arial");
            label.position = new CCPoint(400, 240);
            this.addChild(label);
            this.runAction(CCSequence.actions(CCDelayTime.actionWithDuration(3.0f), CCCallFunc.actionWithTarget(this, gotoPlay)));
        }


        void gotoPlay()
        {
            SpaceBattleScene pScene = new SpaceBattleScene();
            CCDirector.sharedDirector().replaceScene(pScene);
        }
    }

這裏用了一個以前從來沒有使用過的label。這個label可以使用特殊的字體。我們把Arial.fnt放在resource文件夾,Arial.png放在resource/images裏面。另外,設置Arial.fnt的屬性ContentImporter和ContentProcessor分別爲TextImporter和TextProcessor。

這個fnt格式,大家打開看的話,其實就是一堆數據。關於座標x,y,每個字符大小等相關數據,我也沒發現有相關編輯器,裏面的數據我也不是全部懂。覺得大概的就是讀取數據,然後獲取字符的位置,然後用Texture的方式取出顯示出來。如果有誰有編輯器或者資料,可以提供。



現在,我們已經擁有了一個不錯的遊戲,不錯的射擊,滿屏的怪物,運動的場景,傻逼的BOSS。現在來添加一些音效吧。添加CocosDenshion引用。

把laser_ship.wav和laser_enemy.wav添加到music文件夾。

在shoot方法最後添加:

SimpleAudioEngine.sharedEngine().playEffect(@"music/laser_ship");

在bossShoot方法最後添加:

SimpleAudioEngine.sharedEngine().playEffect(@"music/laser_enemy");

好了,到這裏,本次教程就結束了。我們已經擁有了一個不錯的遊戲,不錯的射擊,滿屏的怪物,運動的場景,傻逼的BOSS,WIN/LOSE界面。。。


細心的朋友應該發現了。其實碰撞沒有想象中好,主要原因是MyContactListener類的原因,在EndContact的時候,應該是找到那個contact然後移除並不是直接clear。直接clear的話,就會把有些碰撞點忽略掉了。所以有些碰撞沒有實現,但是現在,我並沒有找到更好的方式來寫這個類,主要原因我在<cocos2d-x for wp7>使用cocos2d-x和BOX2D來製作一個BreakOut(打磚塊)遊戲(二)做過說明。如果有好方法的朋友,麻煩提供個。


示例代碼下載:http://dl.dbank.com/c08fkji48i


何去何從:


  • 把玩家,怪物等信息重構出來,添加多關卡
  • 添加更多的怪物,更多的戰鬥方式
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章