用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游戏:墓碑机制和收尾工作(完)

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