本教程基於子龍山人翻譯的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