用cocos2d-x做一個簡單的windows phone 7遊戲:更猛的怪獸和更多的關卡(三)

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

子龍山人翻譯的Iphone教程地址:http://www.cnblogs.com/andyque/articles/1997966.html

Iphone教程原文地址:http://www.raywenderlich.com/782/harder-monsters-and-more-levels

上一篇教程我們有一個可以旋轉的炮塔,有怪物可以射殺,還有很棒的音效。

  但是,我們的炮塔覺得這太簡單了。這些怪物只要開一槍就掛了,而且現在只有一個關卡!它還沒有熱身呢!

  在這個教程裏,我將會擴展我們的工程,並增加一些不同種類和難度的怪物,然後實現多個關卡。


爲了好玩,讓我們創建兩種不同類型的怪物:一種不怎麼經打,但是移動速度很快,還有一種很能抗(坦克級別),但是移動速度很慢!爲了使玩家可以區分這兩種不同類型的怪物,下載修改的怪物圖片並把它們添加到工程裏。同時,下載我製作的爆炸音效,也把它們添加到Content工程中去。圖片添加到images文件夾,音效添加到resource文件夾。

好了,讓我們來創建Monster類。這裏有許多方法來爲Monster類建模,但是,我們選擇最簡單的方式,即把Monster類當作CCSprite的一個子類。同時,我們會創建兩個Monster類的子類:一個爲我們的虛弱快速怪創建,另一個爲我們的強悍緩慢怪創建。

添加一個類到Classes文件夾。命名爲Monster.cs。並讓之繼承於CCSprite

接下來,把Monster.cs中的代碼替換成下面的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using cocos2d;
namespace cocos2dSimpleGame.Classes
{
    class Monster:CCSprite
    {
        private int _curHp;
        private int _minMoveDuration;
        private int _maxMoveDuration;
        public int hp { get {
            return _curHp;
        }
            set {
                _curHp = value; 
            }
        }

        public int minMoveDuration { 
            get {
                return _minMoveDuration;
            }
            set {
                _minMoveDuration = value;
            }
        }

        public int maxMoveDuration { 
            get {
                return _maxMoveDuration;
            }
            set {
                _maxMoveDuration = value;
            }
        }
    }

    class WeakAndFastMonster : Monster
    {
        public static WeakAndFastMonster monster()
        {
            WeakAndFastMonster monster = new WeakAndFastMonster();
            if (monster.initWithFile(@"images/Target"))
            {
                monster.hp = 1;
                monster.minMoveDuration = 3;
                monster.maxMoveDuration = 5;
            }
            return monster;
        }
    }

    class StrongAndSlowMonster : Monster
    {
        public static StrongAndSlowMonster monster()
        {
            StrongAndSlowMonster monster = new StrongAndSlowMonster();
            if (monster.initWithFile(@"images/Target2"))
            {
                monster.hp = 3;
                monster.minMoveDuration = 6;
                monster.maxMoveDuration = 12;
            }
            return monster;
        }
    }
}

這裏非常直白:我們從CCSprite派生一個Monster類,然後增加了一些成員變量來記錄monster的狀態。然後,我們又從Monster類派生出兩個不同的monster子類。這裏代碼很簡單的,只有我們爲每個類添加的一個靜態方法,用來返回這個類的實例。然後初使化了默認的HP和移動所需要的時間。

然後,返回到GamePlayLayer裏面,修改addTarget方法來構造我們新創建的類的實例,而不是直接創建精靈(sprite)。替換spriteWithFile那一行,如下所示:

            Monster target = null;
            if (random.Next() % 2 == 0)
                target = WeakAndFastMonster.monster();
            else
                target = StrongAndSlowMonster.monster();

這裏將會有50%的機率來出現不同類型的monster。當然,我們把怪物的speed定義移到了類當中,因此,我們需要修改min/max移動間隔,把它改成下面的樣子:

            float minDuration = target.minMoveDuration;//2.0f;
            float maxDuration = target.maxMoveDuration;//4.0f;

最後,在updates方法裏面做一些修改。首先,在遍歷所有的_targets前,也就是foreach (CCSprite target in _targets)前,添加一個boolean值。

bool monsterHit = false;

然後,在CCRectIntersetsRect裏面,不是馬上把對象添加到targetsToDelete裏面,而是改成下面的:

//targetToDelete.Add(target);
                        monsterHit = true;
                        Monster monster = (Monster)target;
                        monster.hp--;
                        if (monster.hp <= 0)
                        {
                            targetToDelete.Add(target);
                        }
                        break;

  這裏,我們不是馬上殺死怪物,而是減少它的HP,而且只有當它的生命值小於0的時候,才kill它。注意,如果projectile擊中一個怪物的話 我們就跳出循環,這意味着一個飛盤射擊一次只能打一個怪物。

  最後,我們把projectilesToDelete測試的這段代碼:

                if (targetToDelete.Count > 0)
                {
                    projectilesToDelete.Add(projectile);
                }

改成下面所示:

                if (monsterHit)
                {
                    projectilesToDelete.Add(projectile);
                    SimpleAudioEngine.sharedEngine().playEffect("resource/explosion");
                }

編譯並運行代碼,如果一切順利,那麼你將會看到兩種不同類型的怪物在屏幕上飛過---這使得我們的炮塔的生活更加富有挑戰了!


多個關卡

  爲了使遊戲支持多個關卡,首先我們需要重構。這個重構的工作非常簡單,但是在這個項目裏,有許多工作要做。如果把所有的內容都放在這個帖子上,那將會是一篇又長又乏味的帖子。

  相反,我會從一個更高的角度來談談我做了什麼,並且提供一個功能完整的樣例工程。

  抽象出一個Level類。目前,HelloWorldScene類裏面把“level”的概念硬編碼進去了,比如發射哪種類型的monster,發射頻率如何等等。因此,我們的第一步就是要把這些信息提取出來,放到一個Level類裏面。這樣,在HelloWorldScene裏面我們就可以爲不同的關卡重用相同的邏輯。

  重用場景。目前,我們每一次轉換場景(scene)的時候都是重新創建了一個新的場景類。這裏有一個缺點就是效率問題。每一次在場景對象的init方法里加載資源,這會影響遊戲frame。

  因爲我們是一個簡單的遊戲,我們需要做的就是,每一個scene創建一個實例,並且提供一個reset方法來清除任何老的狀態(比如上一關中的飛盤或者怪物)。

  使用應用程序委託來當做跳板。目前,我們並沒有任何全局的狀態,比如:我們在哪一個關卡或者當前關卡的設置是什麼。每一個場景僅僅是硬編碼它需要跳轉的下一個場景是誰。

  我們將會修改這些內容,使用App Delegate來存儲指向一些全局狀態(比如關卡信息)的指針。因爲,所有的場景(scene)都可以很方便地得到delegate對象。我們也會在App Delegate類裏面放置一些方法,用來實現不同場景之間的切換的集中控制。並且減少場景之間的相互依賴。

  好了,上面就是我所做的主要的重構內容,記住,這只是實現功能的方式之一,如果你有其它更好的組織場景和遊戲對象的方法,請在這裏分享出來吧!


上面多個關卡的設計是原作者的話。但是對於入門者來說,講了那麼多的理論還是不會。。。

下面我就不怕帖子又長又乏,來徹底實現下多個關卡吧。雖然設計得可能不是太好,不過還是能用了。。。

現在來看下我們的遊戲邏輯實現。我們如要重構出Level。那麼level類包含什麼元素呢,Monster的hp,speed.還有每個關卡需要完成的打擊數。我們決定用Level類來完成當前關卡的Monster獲取。

那麼修改Monster.cs裏面的代碼,修改如下:

class WeakAndFastMonster : Monster
    {
        public static WeakAndFastMonster monster(int _hp,int _minMoveDuration,int _maxMoveDuration)
        {
            WeakAndFastMonster monster = new WeakAndFastMonster();
            if (monster.initWithFile(@"images/Target"))
            {
                monster.hp = _hp;
                monster.minMoveDuration = _minMoveDuration;//3;
                monster.maxMoveDuration = _maxMoveDuration;//5;
            }
            return monster;
        }
    }

    class StrongAndSlowMonster : Monster
    {
        public static StrongAndSlowMonster monster(int _hp, int _minMoveDuration, int _maxMoveDuration)
        {
            StrongAndSlowMonster monster = new StrongAndSlowMonster();
            if (monster.initWithFile(@"images/Target2"))
            {
                monster.hp = _hp;//3;
                monster.minMoveDuration = _minMoveDuration;//6;
                monster.maxMoveDuration = _maxMoveDuration;//12;
            }
            return monster;
        }
    }

我們把速度和hp作爲參數了。

那麼我們新建一個類添加到Classes。命名爲Level.cs。Level類的代碼如下:

class Level
    {
        int _level;
        int _levelCount;

        public int levelCount { get { return _levelCount; } }
        public int level { get { return _level; } }
        public Level()
        { }

        /// <summary>
        /// 默認有7個關卡
        /// </summary>
        /// <param name="l"></param>
        public Level(int l)
        {
            if (l <= 0 || l > 7)
                _level = 1;
            else
                _level = l;
            _levelCount = GetLevelCount(_level);
        }

        /// <summary>
        /// 獲取每個關卡要完成的打擊數
        /// </summary>
        /// <param name="level"></param>
        /// <returns></returns>
        private int GetLevelCount(int level)
        {
            switch (level)
            {
                case 1:
                    return 10;
                case 2:
                    return 10;
                case 3:
                    return 35;
                case 4: return 50;
                case 5: return 55;
                case 6: return 60;
                case 7: return 65;
                default:
                    return 30;
            }
        }

        /// <summary>
        /// 跳轉到下一關
        /// </summary>
        public void NextLevel()
        {
            _level++;
            if (_level > 7)
            {
                _level = 1;
            }
            _levelCount = GetLevelCount(_level);
        }

        /// <summary>
        /// 有Level來生成怪獸。每個關卡的怪獸都不一樣。
        /// </summary>
        /// <returns></returns>
        public Monster GetMonster()
        {
            Monster monster;
            Random random = new Random();
            switch (level)
            {
                case 1: monster = WeakAndFastMonster.monster(1, 5, 8); break;
                case 2: monster = WeakAndFastMonster.monster(1, 4, 7); break;
                case 3: monster = WeakAndFastMonster.monster(1, 3, 5); break;
                case 4:
                    {
                        if (random.Next() % 7 == 0)
                            monster = StrongAndSlowMonster.monster(3, 6, 12);
                        else
                            monster = WeakAndFastMonster.monster(1, 3, 6);
                        break;
                    }
                case 5:
                    {
                        if (random.Next() % 5 == 0)
                            monster = StrongAndSlowMonster.monster(3, 6, 12);
                        else
                            monster = WeakAndFastMonster.monster(1, 3, 6);
                        break; 
                    }
                case 6:
                    {
                        if (random.Next() % 4 == 0)
                            monster = StrongAndSlowMonster.monster(3, 6, 12);
                        else
                            monster = WeakAndFastMonster.monster(1, 2, 6);
                        break;
                    }
                case 7:
                    {
                        if (random.Next() % 3 == 0)
                            monster = StrongAndSlowMonster.monster(3, 6, 12);
                        else
                            monster = WeakAndFastMonster.monster(1, 3, 6);
                        break;
                    }
                default:
                    monster = WeakAndFastMonster.monster(1, 3, 7);break;
            }
            return monster;
        }
    }

接下來要修改GamePlayLayer類。在類中添加兩個聲明:

        Level level = new Level(1);
        int life = 40;

如果下載了我前兩個教程的工程代碼,就發現我在GamePlayLayer裏面添加了一個Label作爲信息顯示,如果您現在的工程沒有添加。那麼在類再添加一個聲明:

CCLabelTTF label;

下面在init裏面添加label的初始化:

            string msg = String.Format("Count:{0},life:{1},Level:{2}", projectilesDestroyed, life, level.level);
            label = CCLabelTTF.labelWithString(msg, "Arial", 24);
            label.position = new CCPoint(label.contentSize.width / 2, screenHeight - label.contentSize.height / 2);
            addChild(label);

這裏用這個label來顯示殺敵數,大炮剩餘的生命值,和當前關卡。

然後,修改addTarget方法來構造我們新創建的類的實例,用level來創建Monster。

            //CCSprite target = CCSprite.spriteWithFile(@"images/Target");
            Monster target = null;
            //if (random.Next() % 2 == 0)
            //    target = WeakAndFastMonster.monster();
            //else
            //    target = StrongAndSlowMonster.monster();
            target = level.GetMonster();

接着修改spriteMoveFinished方法。

if (sprite.tag == 1)//target
            {
                _targets.Remove(sprite);
                life--;
                string msg = String.Format("Count:{0},life:{1},Level:{2}", projectilesDestroyed, life, level.level);
                label.setString(msg);
                if (life <= 0)
                {
                    GameOverScene pScene = new GameOverScene(false);
                    CCDirector.sharedDirector().replaceScene(pScene);
                }
            }

上面修改了個判斷,當生命值爲0的時候,跳轉到GamOverScene。

下面修改勝利判斷。找到updates方法中foreach (CCSprite target in targetToDelete)這裏。修改如下:

                foreach (CCSprite target in targetToDelete)
                {
                    _targets.Remove(target);
                    projectilesDestroyed++;
                    string msg = String.Format("Count:{0},life:{1},Level:{2}", projectilesDestroyed, life, level.level);
                    label.setString(msg);
                    if (projectilesDestroyed >= level.levelCount)
                    {
                        GameOverScene pScene = new GameOverScene(true);
                        CCDirector.sharedDirector().replaceScene(pScene);
                    }
                    this.removeChild(target, true);
                }

上面勝利判斷的做法很明顯了。就不多說了。

到這裏,邏輯修改好了,Level的重構就算是完了。

大家應該發現了,上面GameOverScene的調用改了。一會再說怎麼修改。

接下來要做的場景重用,場景重用,就要保留原來的場景,但是在WP7的程序裏面,全局變量怎麼保存呢,我們用PhoneApplicationService來保存。

首先添加兩個引用。Microsoft.Phone.dll和System.Windows.dll這兩個引用。

場景重用要去掉場景中原來的需要去掉的精靈等元素,我們添加一個方法到GamePlayLayer來完成。

        /// <summary>
        /// 清除任何老的狀態
        /// </summary>
        /// <param name="replay">是否重玩當前關卡</param>
        public void Reset(bool replay)
        {
            foreach (var item in _targets)
            {
                this.removeChild(item,true);
            }
            foreach (var item in _projectiles)
            {
                this.removeChild(item, true);
            }
            _targets.Clear();
            _projectiles.Clear();
            projectilesDestroyed = 0;
            nextProjectile = null;
            if (replay)
                life = 40;
            else
                level.NextLevel();
            this.schedule(gameLogic, 1.0f);
            this.schedule(updates);
            string msg = String.Format("Count:{0},life:{1},Level:{2}", projectilesDestroyed, life, level.level);
            label.setString(msg);
        }

注意,場景一旦跳轉,重回場景後那些schedule事件都無效了。所以要重置。這裏設置的邏輯是不重玩就是下一關。在這裏,我們主要清除的狀態也就是_targets和_projectiles裏面的精靈,remove後,把這兩個List清空。

那麼,我們要保存這個GamePlayScene。

GamePlayScene這個類修改如下:

    class GamePlayScene:CCScene
    {
        public GamePlayScene()
        {
            CCLayerColor colorLayer = CCLayerColor.layerWithColor(new ccColor4B(255, 255, 255, 255));
            this.addChild(colorLayer);
            GamePlayLayer pLayer = (GamePlayLayer)GamePlayLayer.node();
            pLayer.tag = 3;
            this.addChild(pLayer);
            PhoneApplicationService.Current.State["PlayScene"] = this;
        }
    }

爲了獲取到遊戲層,我們爲其添加了一個tag元素。並且在構造函數中,把這個類保存到了PhoneApplicationService裏面。

接下來修改的是GameOverScene。我們要使這個GameOverScene這個場景作爲一個跳板。來實現關卡的跳轉。

下面是勝利的界面:

擁有三個選項,重玩,回到菜單,下一關。那麼我們就需要一些圖片。可以到這裏下載:http://dl.dbank.com/c0g3z4wmma,並且將圖片添加到Content工程的images目錄下。

PS;圖片有些大,懶得整了,將就着用吧。

GameOverScene類修改如下:

    class GameOverScene:CCScene
    {
        public CCLabelTTF label;
        public GameOverScene()
        {        }
        public GameOverScene(bool isWin)
        {
            CCLayerColor colorLayer = CCLayerColor.layerWithColor(new ccColor4B(255, 255, 255, 255));
            this.addChild(colorLayer);
            CCSize winSize = CCDirector.sharedDirector().getWinSize();
            string msg;
            if (isWin)
                msg = "YOU WIN";
            else
                msg = "YOU LOSE";
            label = CCLabelTTF.labelWithString(msg, "Arial", 32);
            label.Color = new ccColor3B(0, 0, 0);
            label.position = new CCPoint(winSize.width / 2, winSize.height / 2 + 100);
            this.addChild(label);
            //this.runAction(CCSequence.actions(CCDelayTime.actionWithDuration(3), CCCallFunc.actionWithTarget(this, gameOverDone)));
            var itemReplay = CCMenuItemImage.itemFromNormalImage(@"images/reload", @"images/reload", this, replay);
            var itemMainMenu = CCMenuItemImage.itemFromNormalImage(@"images/mainmenu", @"images/mainmenu", this, mainMenu);
            var itemNextLevel = CCMenuItemImage.itemFromNormalImage(@"images/nextlevel", @"images/nextlevel", this, nextLevel);
            if (!isWin)
                itemNextLevel.visible = false;
            var menu = CCMenu.menuWithItems(itemReplay, itemMainMenu, itemNextLevel);
            menu.alignItemsHorizontally();
            menu.position = new CCPoint(winSize.width / 2, winSize.height / 2 - 100);
            this.addChild(menu);
        }

        void nextLevel(object sender)
        {
            GamePlayScene pScene;
            if (PhoneApplicationService.Current.State.ContainsKey("PlayScene"))
            {
                pScene = (GamePlayScene)PhoneApplicationService.Current.State["PlayScene"];
                GamePlayLayer pLayer = (GamePlayLayer)pScene.getChildByTag(3);
                pLayer.Reset(false);
            }
            else
                pScene = new GamePlayScene();
            CCDirector.sharedDirector().replaceScene(pScene);
        }

        void mainMenu(object sender)
        {
            CCScene pScene = CCScene.node();
            pScene.addChild(cocos2dSimpleGame.Classes.MainMenu.node());
            CCDirector.sharedDirector().replaceScene(pScene);
        }

        void replay(object sender)
        {
            GamePlayScene pScene;
            if (PhoneApplicationService.Current.State.ContainsKey("PlayScene"))
            {
                pScene = (GamePlayScene)PhoneApplicationService.Current.State["PlayScene"];
                GamePlayLayer pLayer = (GamePlayLayer)pScene.getChildByTag(3);
                pLayer.Reset(true);
            }
            else
                pScene = new GamePlayScene();
            CCDirector.sharedDirector().replaceScene(pScene);
        }

        void gameOverDone()
        {
            CCScene pScene = CCScene.node();
            pScene.addChild(cocos2dSimpleGame.Classes.MainMenu.node());
            CCDirector.sharedDirector().replaceScene(pScene);
        }
    }

上面基本的邏輯估計都能看懂了。就是添加了三個菜單選項。在重玩和下一關中,先取到那個場景,然後取到遊戲層,調用Reset,完成重玩或者下一關的設置。然後場景跳轉。

  到這裏,不管怎麼說,我們有一個非常不錯的遊戲了----一個旋轉的炮塔,成千上萬的不同類型的敵人,多個關卡,win/lose場景,當然,還有很棒的音效!


本次工程下載:http://dl.dbank.com/c0c1vbow72


繼續學習:用cocos2d-x做一個簡單的windows phone 7遊戲:墓碑機制和收尾工作(完)

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