免責申明(必讀!):本部落格提供的所有教程的翻譯原稿均來自于網際網路,僅供學習交流之用,切勿進行商業傳播。同時,轉載時不要移除本申明。如産生任何糾紛,均與本部落格所有人、發表該翻譯稿之人無任何關系。謝謝合作! 原文連結位址: http://geekanddad.wordpress.com/2010/06/22/enemies-and-combat-how-to-make-a-tile-based-game-with-cocos2d-part-3/ 程式截圖:
這篇教程是Ray Wenderlich的《如何使用cocos2d制作基于tiled地圖的遊戲》系列教程的後續。如果你還沒有看過前面兩部分的教程,可以在我的部落格上找到另外兩篇我翻譯Ray的教程。 在第二部分教程中,Ray教大家如何在地圖中制作可碰撞的區域,如何使用tile屬性,如何制作可以拾取的物品以及如何動态修改地圖、如何使用“Heads up display”來顯示分數。 在這個教程中,我們将加入敵人,這樣的話,你的忍者就可以向它們扔飛镖啦,同時還增加了勝利和失敗的遊戲邏輯。但是,首先,你得下載下傳一些相關的 資源檔案。 這個zip檔案裡面包含以下内容: 1.一個敵人精靈 2.一個忍者飛镖,從Ray的《如何使用cocos2d制作一個簡單的iphone遊戲》中拿過來的。 3.兩張按鈕的圖檔,在教程的後面有使用。 在繼續學習之前,不要忘了把這些資源加入到你的工程中。
增加敵人
到第二部分教程結束的時候,遊戲已經很酷了,但是它還不是一個完整的遊戲。你的忍者可以輕而易舉地四處遊蕩,想吃就吃。但是,什麼時候玩家會勝利或者失敗呢。我們不妨想象一下,有2個敵人在追殺你的忍者,那麼這個遊戲會顯得更加有趣。
敵人出現的位置點
好了,回到Tiled軟體(此教程使用java版),然後打開你的Tile地圖(TileMap.tmx)。 往對象層中加入一個對象,在player附近就行,但是不要太近,否則敵人一出現玩家就Game over了。這個位置将成為敵人出現的位置點,把它命名為“EnemySpawn1”。 對象組(對象層中的所有對象組成一個對象組)中的對象被存儲在一個NSMutableDictionary中,同時使用對象名字作為key。這意味着每一個位置點必須有一個唯一的名字。盡管我們可以周遊所有的key來比較哪個是以“EnemySpawn”開頭,但是這樣做效率很低下。相反,我們采用的做法是,使用一個屬性來表示,每個給定的對象代表一個敵人出現的位置點。
給這個對象一個屬性“Enemy”,同時賦一個值1.如果你想在這個教程的基礎上擴充,并且增加其它的不同類型的敵人,你可以使用這些敵人的屬性值來表示不同類型的敵人。 現在,制作6-10個這種敵人出現位置點對象,相應的它們離player的距離也要有一些不同。為每一個對象定義一個“Enemy”屬性,并且指派為1.儲存這張地圖并且回到Xcode。
開始建立敵人
好了,現在我們将把敵人實際顯示到地圖上來。首先在HelloWorldScene.m中添加如下代碼: // in the HelloWorld class
- ( void )addEnemyAtX:( int )x y:( int )y {
CCSprite * enemy = [CCSprite spriteWithFile: @" enemy1.png " ];
enemy.position = ccp(x, y);
[self addChild:enemy];
}
// in the init method - after creating the player
// iterate through objects, finding all enemy spawn points
// create an enemy for each one
NSMutableDictionary * spawnPoint;
for (spawnPoint in [objects objects]) {
if ([[spawnPoint valueForKey: @" Enemy " ] intValue] == 1 ){
x = [[spawnPoint valueForKey: @" x " ] intValue];
y = [[spawnPoint valueForKey: @" y " ] intValue];
[self addEnemyAtX:x y:y];
}
} 第一個循環周遊對象清單,判斷它是否是一個敵人出現的位置點。如果是,則獲得它的x和y坐标值,然後調用addEnemyAtX:Y方法把它們加入到合适的地方去。 這個addEnemyAtX:Y方法非常直白,它僅僅是在傳入的X,Y坐标值處建立一個敵人精靈。 如果你編譯并運作,你會看到這些敵人出現在你之前在Tiled工具中設定的位置處,很酷吧!
但是,這裡有一個問題,這些敵人很傻瓜,它們并不會追殺你的忍者。
使它們移動
是以,現在我們将添加一些代碼,使這些敵人會追着我們的player跑。因為,player肯定會移動,我們必須動态地改變敵人的運動方向。為了實作這個目的,我們讓敵人每次移動10個像素,然後在下一次移動之前,先調整它們的方向。在HelloWorldScene.m中加入如下代碼: // callback. starts another iteration of enemy movement.
- ( void ) enemyMoveFinished:(id)sender {
CCSprite * enemy = (CCSprite * )sender;
[self animateEnemy: enemy];
}
// a method to move the enemy 10 pixels toward the player
- ( void ) animateEnemy:(CCSprite * )enemy
{
// speed of the enemy
ccTime actualDuration = 0.3 ;
// Create the actions
id actionMove = [CCMoveBy actionWithDuration:actualDuration
position:ccpMult(ccpNormalize(ccpSub(_player.position,enemy.position)), 10 )];
id actionMoveDone = [CCCallFuncN actionWithTarget:self
selector:@selector(enemyMoveFinished:)];
[enemy runAction:
[CCSequence actions:actionMove, actionMoveDone, nil]];
}
// add this at the end of addEnemyAtX:y:
// Use our animation method and
// start the enemy moving toward the player
[self 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
CGPoint diff = ccpSub(_player.position,enemy.position);
float angleRadians = atanf(( float )diff.y / ( float )diff.x);
float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
float cocosAngle = - 1 * angleDegrees;
if (diff.x < 0 ) {
cocosAngle += 180 ;
} enemy.rotation = cocosAngle 這個代碼計算每次玩家相對于敵人的角度,然後旋轉敵人來使之面朝玩家。
忍者飛镖
已經很不錯了,但是玩家是一個忍者啊!他應該要能夠保護他自己! 我們将向遊戲中添加模式(modes)。模式并不是實作這個功能的最好方式,但是,它比其他的方法要簡單,而且這個方法在模拟器下也能運作(因為并不需要多點觸摸)。因為這些優點,是以這個教程裡面,我們使用這種方法。首先将會建立UI,這樣的話玩家可以友善地在“移動模式”和“擲飛镖”模式之間進行切換。我們将增加一個按鈕來使用這個功能的轉換。(即從移動模式轉到擲飛镖模式)。 現在,我們将增加一些屬性,使兩個層之間可以更好的通信。在HelloWorldScene.h裡面增加如下代碼: // at the top of the file add a forward declaration for HelloWorld,
// because our two layers need to reference each other
@class HelloWorld;
// inside the HelloWorldHud class declaration
HelloWorld * _gameLayer;
// After the class declaration
@property (nonatomic, assign) HelloWorld * gameLayer;
// Inside the HelloWorld class declaration
int _mode;
// After the class declaration
@property (nonatomic, assign) int mode; 同時修改HelloWorldScene.m檔案 // At the top of the HelloWorldHud implementation
@synthesize gameLayer = _gameLayer;
// At the top of the HelloWorld implementation
@synthesize mode = _mode;
// in HelloWorld's init method
_mode = 0 ;
// in HelloWorld's scene method
// after layer.hud = hud
hud.gameLayer = layer; 如果想知道在cocos2d裡面如何使用按鈕,可以參照我翻譯的另外一篇教程 《在cocos2d裡面如何制作按鈕:簡單按鈕、單選按鈕和開關按鈕》。 在HelloWorldScene.m中添加下面的代碼,這段代碼定義了一個按鈕。 Add the folowing code, which defines a button, to HelloWorldScene.m:
// in HelloWorldHud's init method
// define the button
CCMenuItem * on;
CCMenuItem * off;
on = [[CCMenuItemImage itemFromNormalImage: @" projectile-button-on.png "
selectedImage: @" projectile-button-on.png " target:nil selector:nil] retain];
off = [[CCMenuItemImage itemFromNormalImage: @" projectile-button-off.png "
selectedImage: @" projectile-button-off.png " target:nil selector:nil] retain];
CCMenuItemToggle * toggleItem = [CCMenuItemToggle itemWithTarget:self
selector:@selector(projectileButtonTapped:) items:off, on, nil];
CCMenu * toggleMenu = [CCMenu menuWithItems:toggleItem, nil];
toggleMenu.position = ccp( 100 , 32 );
[self addChild:toggleMenu];
// in HelloWorldHud
// callback for the button
// mode 0 = moving mode
// mode 1 = ninja star throwing mode
- ( void )projectileButtonTapped:(id)sender
{
if (_gameLayer.mode == 1 ) {
_gameLayer.mode = 0 ;
} else {
_gameLayer.mode = 1 ;
}
} 編譯并運作。這時會在左下角出現一個按鈕,并且你可以打開或者關閉之。但是這并不會對遊戲造成任何影響。我們的下一步就是增加飛镖的發射。
發射飛镖
接下來,我們将添加一些代碼來檢查玩家目前處于哪種模式下面,并且在使用者點選螢幕的時候影響不同的事件。如果是移動模式則移動玩家,如果是射擊模式,則擲飛镖。在ccTouchEnded:withEvent:方法裡面增加下面代碼: if (_mode == 0 ) {
// old contents of ccTouchEnded:withEvent:
} else {
// code to throw ninja stars will go here
} 這樣可以使得移動模式下,玩家隻能移動。下一步就是要添加代碼使忍者能夠發射飛镖。在else部分增加,在增加之前,先在HelloWorld.m中添加一些清理代碼: - ( void ) projectileMoveFinished:(id)sender {
CCSprite * sprite = (CCSprite * )sender;
[self removeChild:sprite cleanup:YES];
} 好了,看到上面的else部分的注釋了嗎:
// code to throw ninja stars will go here
在上面的注釋後面添加下面的代碼: // Find where the touch is
CGPoint touchLocation = [touch locationInView: [touch view]];
touchLocation = [[CCDirector sharedDirector] convertToGL: touchLocation];
touchLocation = [self convertToNodeSpace:touchLocation];
// Create a projectile and put it at the player's location
CCSprite * projectile = [CCSprite spriteWithFile: @" Projectile.png " ];
projectile.position = _player.position;
[self addChild:projectile];
// Determine where we wish to shoot the projectile to
int realX;
// Are we shooting to the left or right?
CGPoint diff = 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 = ( float ) diff.y / ( float ) diff.x;
int realY = ((realX - projectile.position.x) * ratio) + projectile.position.y;
CGPoint realDest = ccp(realX, realY);
// Determine the length of how far we're shooting
int offRealX = realX - projectile.position.x;
int offRealY = realY - projectile.position.y;
float length = sqrtf((offRealX * offRealX) + (offRealY * offRealY));
float velocity = 480 / 1 ; // 480pixels/1sec
float realMoveDuration = length / velocity;
// Move projectile to actual endpoint
id actionMoveDone = [CCCallFuncN actionWithTarget:self
selector:@selector(projectileMoveFinished:)];
[projectile runAction:
[CCSequence actionOne:
[CCMoveTo actionWithDuration: realMoveDuration
position: realDest]
two: actionMoveDone]]; 這段代碼會在使用者點選螢幕的方向發射飛镖。對于這段代碼的完整的細節,可以檢視我翻譯的另一個文章 《如何使用cocos2d來做一個簡單的iphone遊戲教程(第一部分)》。當然,檢視原作者的文章後面的注釋會更加清楚明白一些。 projectileMoveFinished:方法會在飛镖移動到螢幕之外的時候移除。這個方法非常關鍵。一旦我們開始做碰撞檢測的時候,我們将要循環周遊所有的飛镖。如果我們不移除飛出螢幕範圍之外的飛镖的話,這個存儲飛镖的清單将會越來越大,而且遊戲将會越來越慢。編譯并運作工程,現在,你的忍者可以向敵人投擲飛镖了。
碰撞檢測
接下來,就是當飛镖擊中敵人的時候,要把敵人銷毀。在HelloWorldClass類中增加以下變量(在HelloWorldScene.h檔案中): NSMutableArray * _enemies;
NSMutableArray * _projectiles; 然後初使化_projectiles數組: // at the end of the launch projectiles section of ccTouchEnded:withEvent:
[_projectiles addObject:projectile];
// at the end of projectileMoveFinished:
[_projectiles removeObject:sprite]; 然後在addEnemyAtX:y方法的結尾添加如下代碼: [_enemies addObject:enemy]; 接着,在HelloWorld類中添加如下代碼: - ( void )testCollisions:(ccTime)dt {
NSMutableArray * projectilesToDelete = [[NSMutableArray alloc] init];
// iterate through projectiles
for (CCSprite * projectile in _projectiles) {
CGRect projectileRect = CGRectMake(
projectile.position.x - (projectile.contentSize.width / 2 ),
projectile.position.y - (projectile.contentSize.height / 2 ),
projectile.contentSize.width,
projectile.contentSize.height);
NSMutableArray * targetsToDelete = [[NSMutableArray alloc] init];
// iterate through enemies, see if any intersect with current projectile
for (CCSprite * target in _enemies) {
CGRect targetRect = CGRectMake(
target.position.x - (target.contentSize.width / 2 ),
target.position.y - (target.contentSize.height / 2 ),
target.contentSize.width,
target.contentSize.height);
if (CGRectIntersectsRect(projectileRect, targetRect)) {
[targetsToDelete addObject:target];
}
}
// delete all hit enemies
for (CCSprite * target in targetsToDelete) {
[_enemies removeObject:target];
[self removeChild:target cleanup:YES];
}
if (targetsToDelete.count > 0 ) {
// add the projectile to the list of ones to remove
[projectilesToDelete addObject:projectile];
}
[targetsToDelete release];
}
// remove all the projectiles that hit.
for (CCSprite * projectile in projectilesToDelete) {
[_projectiles removeObject:projectile];
[self removeChild:projectile cleanup:YES];
}
[projectilesToDelete release];
} 最後,初始化敵人來飛镖數組,并且排程testCollisions:方法,把這些代碼加在HelloWorld類的init方法中。 // you need to put these initializations before you add the enemies,
// because addEnemyAtX:y: uses these arrays.
_enemies = [[NSMutableArray alloc] init];
_projectiles = [[NSMutableArray alloc] init];
[self schedule:@selector(testCollisions:)]; 上面的所有的代碼,關于具體是如何工作的,可以在我的部落格上查找 《如何使用COCOS2D制作一個簡單的iphone遊戲》教程。當然,原作者的文章注釋部分的讨論更加清晰,是以我翻譯的教程,也希望大家多讨論啊。代碼盡量自己用手敲進去,不要為了省事,alt+c,alt+v,這樣不好,真的! 好了,現在可以用飛镖打敵人,而且打中之後它們會消失。現在讓我們添加一些邏輯,使得遊戲可以勝利或者失敗吧!
勝利和失敗
The Game Over Scene
好了,讓我們建立一個新的場景,來作為我們的“You Win”或者“You Lose”訓示器吧。在Xcode中,選擇Classes檔案夾,然後點選File\New File,再選擇Objective-c類,確定NSObject是基類被選中。點選下一步,然後輸入檔案名GameOverScene,并且確定“Also create GameOverScene.h”複選中。 然後用下面的代碼替換掉模闆生成代碼: #import " cocos2d.h "
@interface GameOverLayer : CCColorLayer {
CCLabel * _label;
}
@property (nonatomic, retain) CCLabel * label;
@end
@interface GameOverScene : CCScene {
GameOverLayer * _layer;
}
@property (nonatomic, retain) GameOverLayer * layer;
@end 相應地修改GameOverScene.m檔案: #import " GameOverScene.h "
#import " HelloWorldScene.h "
@implementation GameOverScene
@synthesize layer = _layer;
- (id)init {
if ((self = [super init])) {
self.layer = [GameOverLayer node];
[self addChild:_layer];
}
return self;
}
- ( void )dealloc {
[_layer release];
_layer = nil;
[super dealloc];
}
@end
@implementation GameOverLayer
@synthesize label = _label;
- (id) init
{
if ( (self = [super initWithColor:ccc4( 255 , 255 , 255 , 255 )] )) {
CGSize winSize = [[CCDirector sharedDirector] winSize];
self.label = [CCLabel labelWithString: @"" fontName: @" Arial " fontSize: 32 ];
_label.color = ccc3( 0 , 0 , 0 );
_label.position = ccp(winSize.width / 2 , winSize.height / 2 );
[self addChild:_label];
[self runAction:[CCSequence actions:
[CCDelayTime actionWithDuration: 3 ],
[CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)],
nil]];
}
return self;
}
- ( void )gameOverDone {
[[CCDirector sharedDirector] replaceScene:[HelloWorld scene]];
}
- ( void )dealloc {
[_label release];
_label = nil;
[super dealloc];
}
@end GameOverLayer僅僅隻是在螢幕中間旋轉一個label,然後排程一個transition隔3秒後回到HelloWorld場景中。
勝利場景
現在,讓我們添加一些代碼,使得玩家吃完所有的西瓜的時候,遊戲會結束。在HelloWorld類的setPlayerPositoin:方法中添加以下代碼,(位于HelloWorldScene.m中,就是update代碼後面:) // put the number of melons on your map in place of the '2'
if (_numCollected == 2 ) {
[self win];
} 然後,在HelloWorld類中建立win方法: - ( void ) win {
GameOverScene * gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString: @" You Win! " ];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
} 不要忘了包含頭檔案: #import " GameOverScene.h " 編譯并運作,當你吃完所有的西瓜後,就會出現如下畫面:
失敗場景
就這個教程而言,我們的玩家隻要有一個敵人碰到他,遊戲是結束了。在HelloWorld類的testCollision方法中添加以列循環: for (CCSprite * target in _enemies) {
CGRect targetRect = CGRectMake(
target.position.x - (target.contentSize.width / 2 ),
target.position.y - (target.contentSize.height / 2 ),
target.contentSize.width,
target.contentSize.height );
if (CGRectContainsPoint(targetRect, _player.position)) {
[self lose];
}
} 這個循環周遊所有的敵人,隻要有一個敵人精靈的圖檔所在的矩形和玩家接觸到了,那麼遊戲就失敗了。接下,再建立lose方法: - ( void ) lose {
GameOverScene * gameOverScene = [GameOverScene node];
[gameOverScene.layer.label setString: @" You Lose! " ];
[[CCDirector sharedDirector] replaceScene:gameOverScene];
} 編譯并運作,一旦有一個敵人碰到你,你就會看到下面的場景:
完整源代碼
這裡有這個教程的 完整源代碼。謝謝你們有耐心看到這裡。
接下來怎麼做?
建議:
- 增加多個關卡
- 增加不同類型的敵人
- 在Hud層中顯示血條和玩家生命
- 制作更多的道具,比如加血的,武器等等
- 一個菜單系統,可以選擇關卡,關閉音效,等等
- 使用更好的使用者界面,來使遊戲畫面更加精美,投擲飛镖更加潇灑。
著作權聲明:本文由 http://www.cnblogs.com/andyque教程,歡迎轉載分享。請尊重作者勞動,轉載時保留該聲明和作者部落格連結,謝謝!