本教程基於子龍山人翻譯的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遊戲:墓碑機制和收尾工作(完)