本教程基于子龙山人翻译的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场景,当然,还有很棒的音效!