子龍山人翻譯的Iphone教程地址:http://www.cnblogs.com/andyque/archive/2011/05/07/2039481.html
Iphone教程原文地址:http://geekanddad.wordpress.com/2010/06/22/enemies-and-combat-how-to-make-a-tile-based-game-with-cocos2d-part-3/
程序截圖:
這篇教程是《使用cocos2d-x製作基於Tile地圖的遊戲》系列教程的後續。如果你還沒有看過前面兩部分的教程,可以在我的博客上找到另外兩篇教程。
在第二部分教程中,Ray教大家如何在地圖中製作可碰撞的區域,如何使用tile屬性,如何製作可以拾取的物品以及如何動態修改地圖、如何使用“Heads up display”來顯示分數。
在這個教程中,我們將加入敵人,這樣的話,你的忍者就可以向它們扔飛鏢啦,同時還增加了勝利和失敗的遊戲邏輯。但是,首先,你得下載一些相關的資源文件。
增加敵人
到第二部分教程結束的時候,遊戲已經很酷了,但是它還不是一個完整的遊戲。你的忍者可以輕而易舉地四處遊蕩,想喫就喫。但是,什麼時候玩家會勝利或者失敗呢。我們不妨想象一下,有2個敵人在追殺你的忍者,那麼這個遊戲會顯得更加有趣。
敵人出現的位置點
好了,回到Tiled軟件,然後打開你的Tile地圖(TileMap.tmx)。
往對象層中加入一個對象,在player附近就行,但是不要太近,否則敵人一出現玩家就Game over了。這個位置將成爲敵人出現的位置點,把它命名爲“EnemySpawn1”。
對象組(對象層中的所有對象組成一個對象組)中的對象被存儲在一個List<Dictionary>中,同時使用對象名字作爲value。這意味着每一個位置點必須有一個唯一的名字。儘管我們可以遍歷所有的Dictionary是否有value是以“EnemySpawn”開頭,但是這樣做效率很低下。相反,我們採用的做法是,使用一個屬性來表示,每個給定的對象代表一個敵人出現的位置點。
給這個對象一個屬性“Enemy”,同時賦一個值1.如果你想在這個教程的基礎上擴展,並且增加其它的不同類型的敵人,你可以使用這些敵人的屬性值來表示不同類型的敵人。
現在,製作6-10個這種敵人出現位置點對象,相應的它們離player的距離也要有一些不同。爲每一個對象定義一個“Enemy”屬性,並且賦值爲1.保存這張地圖並且更新到工程中。
開始創建敵人
好了,現在我們將把敵人實際顯示到地圖上來。首先在TileMapLayer類中添加如下方法:
void addEnemyAtXY(int x, int y)
{
CCSprite enemy = CCSprite.spriteWithFile("images/enemy1");
enemy.position = new CCPoint(x, y);
this.addChild(enemy);
}
並且在init裏面的添加player的if語句後面添加: if (item.ContainsValue("EnemySpawn1"))
{
int ex = Convert.ToInt32(item["x"]);
int ey = Convert.ToInt32(item["y"]);
this.addEnemyAtXY(ex, ey);
}
循環遍歷對象列表,判斷是否是敵人對象,如果是,則獲得它的x和y座標值,然後調用addEnemyAtXY方法把它們加入到合適的地方去。這個addEnemyAtXY方法非常直白,它僅僅是在傳入的X,Y座標值處創建一個敵人精靈。
如果你編譯並運行,你會看到這些敵人出現在你之前在Tiled工具中設定的位置處,很酷吧!
但是,這裏有一個問題,這些敵人很傻瓜,它們並不會追殺你的忍者。
使它們移動
因此,現在我們將添加一些代碼,使這些敵人會追着我們的player跑。因爲,player肯定會移動,我們必須動態地改變敵人的運動方向。爲了實現這個目的,我們讓敵人每次移動10個像素,然後在下一次移動之前,先調整它們的方向。在TileMapLayer類中加入如下代碼:
/// <summary>
/// callback, starts another iteration of enemy movement
/// </summary>
/// <param name="sender"></param>
void enemyMoveFinished(object sender)
{
CCSprite enemy = sender as CCSprite;
this.animateEnemy(enemy);
}
/// <summary>
/// a method to move enemy 10 pixls toward the player
/// </summary>
/// <param name="enemy"></param>
void animateEnemy(CCSprite enemy)
{
//speed of the enemy
float acturalDuration = 0.3f;
var actionMove = CCMoveBy.actionWithDuration(acturalDuration, CCPointExtension.ccpMult(
CCPointExtension.ccpNormalize(
CCPointExtension.ccpSub(player.position, enemy.position)), 10));
var actionMoveDone = CCCallFuncN.actionWithTarget(this, enemyMoveFinished);
enemy.runAction(CCSequence.actions(actionMove, actionMoveDone));
}
並且在addEnemyAtXY方法最後添加:this.animateEnemy(enemy);
animateEnemy:方法創建兩個action。第一個action使之朝敵人移動10個像素,時間爲0.3秒。你可以改變這個時間使之移動得更快或者更慢。第二個action將會調用enemyMoveFinished:方法。我們使用CCSequence action來把它們組合起來,這樣的話,當敵人停止移動的時候就立馬可以執行enemyMoveFinished:方法就可以被調用了。在addEnemyAtX:Y:方法裏面,我們調用animateEnemy:方法來使敵人朝着玩家(player)移動。(其實這裏是個遞歸的調用,每次移動10個像素,然後又調用enemyMoveFinished:方法)
很簡潔!但是,但是,如果敵人每次移動的時候面部都對着player那樣是不是更逼真呢?只需要在animateEnemy:方法中加入下列語句即可:
//immediately before creating the actions in animateEnemy
//rotate to face the player
CCPoint diff = CCPointExtension.ccpSub(player.position, enemy.position);
double angleRadians = Math.Atan((double)(diff.y / diff.x));
float angleDegrees = MathHelper.ToDegrees((float)angleRadians);
float cocosAngle = -1 * angleDegrees;
if (diff.x < 0)
{
cocosAngle += 180;
}
enemy.rotation = cocosAngle;
這個代碼計算每次玩家相對於敵人的角度,然後旋轉敵人來使之面朝玩家。
滿屏的怪物,似乎怪物設置得有點多了。。
忍者飛鏢
已經很不錯了,但是玩家是一個忍者啊!他應該要能夠保護他自己!
我們將向遊戲中添加模式(modes)。模式並不是實現這個功能的最好方式,但是,它比其他的方法要簡單,而且這個方法在模擬器下也能運行(因爲並不需要多點觸摸)。因爲這些優點,所以這個教程裏面,我們使用這種方法。首先將會建立UI,這樣的話玩家可以方便地在“移動模式”和“擲飛鏢”模式之間進行切換。我們將增加一個按鈕來使用這個功能的轉換。(即從移動模式轉到擲飛鏢模式)。
現在,我們將增加一些屬性,使兩個層之間可以更好的通信。在TileMapHud類裏面增加如下代碼:
int _mode = 0;
public int mode {
get {
return _mode;
}
}
/// <summary>
/// callback for the button
/// mode 0 = moving mode
/// mode 1 = ninja star throwing mode
/// </summary>
/// <param name="sender"></param>
void projectileButtonTapped(object sender)
{
if (_mode == 1)
{
_mode = 0;
}
else
_mode = 1;
}
//in the init method
CCMenuItem on = CCMenuItemImage.itemFromNormalImage(@"images/projectile-button-on", @"images/projectile-button-on");
CCMenuItem off = CCMenuItemImage.itemFromNormalImage(@"images/projectile-button-off", @"images/projectile-button-off");
CCMenuItemToggle toggleItem = CCMenuItemToggle.itemWithTarget(this, projectileButtonTapped, on, off);
CCMenu toggleMenu = CCMenu.menuWithItems(toggleItem);
toggleMenu.position = new CCPoint(100, 32);
this.addChild(toggleMenu);
上面我們做了什麼呢,就是添加了一個變量mode作爲判斷所用。另外添加一個button。編譯並運行。這時會在左下角出現一個按鈕,並且你可以打開或者關閉之。但是這並不會對遊戲造成任何影響。我們的下一步就是增加飛鏢的發射。
發射飛鏢
接下來,我們將添加一些代碼來檢查玩家當前處於哪種模式下面,並且在用戶點擊屏幕的時候影響不同的事件。如果是移動模式則移動玩家,如果是射擊模式,則擲飛鏢。在ccTouchEnded方法裏面增加下面代碼:
if (hud.mode ==0) {
// old contents of ccTouchEnded:withEvent:
} else {
// code to throw ninja stars will go here
}
這樣可以使得移動模式下,玩家只能移動。下一步就是要添加代碼使忍者能夠發射飛鏢。在else部分增加,在增加之前,先在TileMapLayer類中添加一些清理代碼: void projectileMoveFinished(object sender)
{
CCSprite sprite = sender as CCSprite;
this.removeChild(sprite, true);
}
好了,看到上面的else部分的註釋了嗎:// code to throw ninja stars will go here
在上面的註釋後面添加下面的代碼:
//code to throw ninja stars will go here
//Find where the touch is
CCPoint touchLocation = touch.locationInView(touch.view());
touchLocation = CCDirector.sharedDirector().convertToGL(touchLocation);
touchLocation = this.convertToNodeSpace(touchLocation);
//Create a projectile and put it at the player's location
CCSprite projectile = CCSprite.spriteWithFile(@"images/Projectile");
projectile.position = new CCPoint(player.position.x, player.position.y);
this.addChild(projectile);
//Determine where we wish to shoot the projectile to
float realX;
//Are we shooting to left or right?
CCPoint diff = CCPointExtension.ccpSub(touchLocation, player.position);
if (diff.x > 0)
{
realX = tileMap.MapSize.width * tileMap.TileSize.width + projectile.contentSize.width / 2;
}
else
realX = -tileMap.MapSize.width * tileMap.TileSize.width - projectile.contentSize.width / 2;
float ratio = diff.y / diff.x;
float realY = (realX - projectile.position.x) * ratio + projectile.position.y;
CCPoint realDest = new CCPoint(realX, realY);
//Determine the length of how far we're shooting
float offRealX = realX - projectile.position.x;
float offRealY = realY - projectile.position.y;
double length = Math.Sqrt((double)(offRealX * offRealX + offRealY * offRealY));
float velocity = 480 / 1;//480pixls/1sec
float realMoveDuration = (float)length / velocity;
//Move projectile to actual endpoint
var actionMoveDone = CCCallFuncN.actionWithTarget(this, projectileMoveFinished);
projectile.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(realMoveDuration,realDest),actionMoveDone));
這段代碼會在用戶點擊屏幕的方向發射飛鏢。對於這段代碼的完整的細節,可以查看我的另一個文章《用cocos2d-x做一個簡單的windows phone 7遊戲(一)》。當然,查看原作者的文章後面的註釋會更加清楚明白一些。projectileMoveFinished:方法會在飛鏢移動到屏幕之外的時候移除。這個方法非常關鍵。一旦我們開始做碰撞檢測的時候,我們將要循環遍歷所有的飛鏢。如果我們不移除飛出屏幕範圍之外的飛鏢的話,這個存儲飛鏢的列表將會越來越大,而且遊戲將會越來越慢。編譯並運行工程,現在,你的忍者可以向敵人投擲飛鏢了。
接下來,就是當飛鏢擊中敵人的時候,要把敵人銷燬。
那麼,我們應該怎麼做呢。我倒想用BOX2D來做碰撞檢測,不過常規的碰撞檢測也是可以的。這個就提供兩個方案吧。
先添加一個類到classes文件夾。命名爲GameOverScene,並且使之繼承於CCScene。
修改代碼如下;
public GameOverScene(bool isWin)
{
string winMsg;
if (isWin)
{
winMsg = "YOU WIN!";
}
else
winMsg = "YOU LOSE!";
CCLabelTTF label = CCLabelTTF.labelWithString(winMsg, "Arial", 32);
label.position = new CCPoint(400, 300);
this.addChild(label);
this.runAction(CCSequence.actions(CCDelayTime.actionWithDuration(3.0f), CCCallFunc.actionWithTarget(this, gameOverDone)));
}
void gameOverDone()
{
TileMapScene pScene = new TileMapScene();
CCDirector.sharedDirector().replaceScene(pScene);
}
}
這樣,就完成了GameOverScene了。上面的代碼很清楚了。就是添加一個label,並且在延時3秒後跳轉到遊戲界面。PART1:BOX2D檢測
這個部分,我們用BOX2D來做碰撞檢測,如果你對BOX2D不瞭解,可以看我博客裏面的<cocos2d-x for wp7>在cocos2d-x裏面使用BOX2D和<cocos2d-x for wp7>使用box2d來做碰撞檢測(且僅用來做碰撞檢測)。
具體添加代碼方法看<cocos2d-x for wp7>使用box2d來做碰撞檢測(且僅用來做碰撞檢測)。其實是差不多的。我在這裏大概說下我做了什麼,後面也會提供一個示例代碼。
首先,我添加了和碰撞檢測文章中一樣的聲明,世界的創建等操作,爲player,projectile,enemy都添加了tag以便碰撞檢測,並且也添加了一樣的addBoxBodyForSprite方法。tick方法。在碰撞檢測上面做了一樣的操作。當player和enemy碰撞的時候跳轉到GameOverScene顯示YOU LOSE。在setPlayerPosition裏面蒐集食物處添加檢測,如果蒐集數爲8,就跳轉顯示YOU WIN。(我數了下我地圖上的個數爲8)。
這樣,就完成了。BOX2D做檢測還真是方便。示例代碼下載:http://dl.dbank.com/c0rr5l1b7p
PART2:普通方式檢測
普通方式就是指用精靈的position和contentSize生成一個矩形做檢測。
那麼,先往TileMapLayer類裏面添加變量聲明:
List<CCSprite> enemies;
List<CCSprite> projectiles;
然後在init裏面初使化list: //init the list
enemies = new List<CCSprite>();
projectiles = new List<CCSprite>();
然後在addEnemyAtXY方法的結尾添加如下代碼:enemies.Add(enemy);
接着,在TileMapLayer類中添加如下代碼: void testCollision(float dt)
{
List<CCSprite> projectilesToDelete = new List<CCSprite>();
List<CCSprite> targetToDelete = new List<CCSprite>();
//iterate through projectiles
foreach (var projectile in projectiles)
{
CCRect projectileRect = new CCRect(
projectile.position.x - projectile.contentSize.width / 2,
projectile.position.y - projectile.contentSize.height / 2,
projectile.contentSize.width,
projectile.contentSize.height);
//iterate through enemies,see if any intersect with current projectile
foreach (var target in enemies)
{
CCRect targetRect = new CCRect(
target.position.x - target.contentSize.width / 2,
target.position.y - target.contentSize.width / 2,
target.contentSize.width,
target.contentSize.height);
if (CCRect.CCRectIntersetsRect(projectileRect,targetRect))
{
targetToDelete.Add(target);
}
}
//delete all hit enemies
foreach (var target in targetToDelete)
{
enemies.Remove(target);
this.removeChild(target,true);
}
if (targetToDelete.Count > 0)
{
//add the projectile to the list of ones to remove
projectilesToDelete.Add(projectile);
}
targetToDelete.Clear();
}
//remove all the projectiles that hit
foreach (var projectile in projectilesToDelete)
{
projectiles.Remove(projectile);
this.removeChild(projectile, true);
}
projectilesToDelete.Clear();
}
然後添加以下代碼:
// at the end of the launch projectiles section of ccTouchEnded:withEvent:
projectiles.Add(projectile);
// at the end of projectileMoveFinished:
projectiles.Remove(sprite);
//in the init
this.schedule(testCollision);
上面的所有的代碼,關於具體是如何工作的,可以在我的博客上查找《用cocos2d-x做一個簡單的windows phone 7遊戲(一)》教程。當然,原作者的文章註釋部分的討論更加清晰,所以我翻譯的教程,也希望大家多討論啊。代碼儘量自己用手敲進去,不要爲了省事,alt+c,alt+v,這樣不好,真的!
好了,現在可以用飛鏢打敵人,而且打中之後它們會消失。現在讓我們添加一些邏輯,使得遊戲可以勝利或者失敗吧!
添加一個方法:
void gameOver(bool isWin)
{
GameOverScene gameOverScene = new GameOverScene(isWin);
CCDirector.sharedDirector().replaceScene(gameOverScene);
}
然後在setPlayerPosition裏面蒐集食物處添加檢測,如果蒐集數爲8,就跳轉顯示YOU WIN。(我數了下我地圖上的個數爲8)。
if (numCollected == 2)
{
gameOver(true);
}
就這個教程而言,我們的玩家只要有一個敵人碰到他,遊戲是結束了。在類的testCollision方法中添加以列循環: foreach (var target in enemies)
{
CCRect targetRect = new CCRect(
target.position.x - target.contentSize.width / 2,
target.position.y - target.contentSize.width / 2,
target.contentSize.width,
target.contentSize.height);
if (CCRect.CCRectContainsPoint(targetRect, player.position))
{
gameOver(false);
}
}
編譯並運行,得到預期效果。示例代碼:http://dl.dbank.com/c0xy82fc3w
接下來怎麼做?
建議:
增加多個關卡
增加不同類型的敵人
在Hud層中顯示血條和玩家生命
製作更多的道具,比如加血的,武器等等
一個菜單系統,可以選擇關卡,關閉音效,等等
使用更好的用戶界面,來使遊戲畫面更加精美,投擲飛鏢更加瀟灑。
另外,繼續:<cocos2d-x for wp7>使用cocos2d-x製作基於Tile地圖的遊戲:不一樣的戰鬥(回合制戰鬥)(四)。