“指”上谈兵-如何使用Cocos2D制作一款iPhone回合制策略游戏PART-1

本文由eseedo翻译,泰然授权转载,其他转载请通知原版权方!

感谢原文作者,来自InfinixSoft的联合创始人和COO Pablo Ruiz,同时也是Ray Wenderlich iOS系列教程团队的成员之一。

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

最近大家都因为黄岩岛的事情搞得很不爽,作为攻城湿的屌丝们,未经高富帅大人们的允许不要说投笔从戎,连在自己的地盘上喊几嗓子抗议的机会都没有。想了想,有这破功夫,还不如来“指”上谈兵,制作一款回合制策略游戏过过瘾。至于猴子闹腾的事情,还是交给大宋的高富帅们解决吧。

 

 

在这篇教程中,我们将学习如何使用cocos2d制作一款iPhone回合制策略游戏。这类游戏一向是我的最爱,比如任家的Advance Wars(高级战争)系列。诸如Hero Academy(英雄学院)这样的简化版回合制策略游戏最近也开始流行起来。



在这篇教程中我们将学到以下内容:

1.如何加载具有不同地形的地图。

2.如何创建带有独特属性的不同类型的单位。

3.如何使用一定的人工智能寻路算法让不同的单位在地图中移动和攻击。

 

在学习完本教程中,我们就完成了一个基本但完整的类似高级战争这样的回合制策略游戏,同时可以和朋友一起在设备上玩。

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

在学习这篇教程前,需要一定的cocos2d经验,同时对AI人工智能有一定的了解。此外,我们将使用Tiled创建地图,所以需要对瓦片地图和Tiled工具有一定的了解。

 

这篇教程分为两个部分,在第一部分的内容中,我们将学习如何为两支队伍加载不同的单位,并让这些单位动起来。

 

开始前的准备:游戏概览

 

对于没有玩过高级战争游戏的玩家,可以这样理解这款游戏的核心玩法:双方的主要任务是消灭另一个玩家的所有单位,或是通过将某个战士移动到对方的总部来占领(感觉有点类似中国的军棋)。

 

下面是游戏制作完成后的截图:

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

如你所见,每个玩家都控制着属于自己的单位。玩家1控制着红色的单位(你可以把它想象成大宋的海军陆战队),玩家2控制着蓝色的单位(比如猴子从老鹰那里得到的二手军火)。随着教程的进行,我们将创建四种不同的单位,每一种都有不同的属性,其攻击范围和伤害能力也各不相同。

每个玩家都必须保卫自己的HQ(比如兔子必须保卫一块黄色的岩石,而猴子则必须保卫吕宋)。通过创建自己的HQ总部,玩家可以轻松的拓展游戏,并创建出其它建筑,如工厂,机场等等。

最后,在上面的图形中我们还可以看到不同类型的地形:草地,山脉,河流和森林。而每种地形只允许特定的单位通过。比如,大宋的海军陆战队可以徒步穿越河流(当然运动速度会下降),但坦克还不是两栖的,所以没法穿越。当然,传说中的直9可以随便飞行到任何位置。

在游戏的最终版本中,屏幕的顶部会有一个条用来显示当前回合由哪一方来控制,同时还有一个End Turn按钮来将控制权交给另一方。这些东西会在教程的第二部分添加。

 

开始前的准备:资源就绪

 

在开始编码之前,首先下载教程的基本项目:猛击这里:(http://d1xzuxjlafny7l.cloudfront.net/downloads/TurnWars.zip),然后解压缩到自己喜欢的位置。这个基本项目中包含了Cocos2D项目的基本内容,以及大多数的美术资源和地图。

 

我是通过Cocos2D的基本模板创建的项目, 当然,我们不会使用HelloWorldLayer

,而是创建自己的游戏界面。

在继续下面的工作前,最好在Xcode中打开项目并且编译运行,确保没有神马问题(不过是一片漆黑的屏幕而已。

在此时的TurnWars文件夹中,我们可以看到:

1.游戏中所用到的所有类

2.libs文件夹中包含了所有和Cocos2D相关的文件

3.Resources文件夹中包含了所有的美术资源

 

你会发现其中提供了一个准备好的瓦片地图。这篇教程不是教你如何使用Tiled创建地图的,如果你还不会,可以参考这篇教程:(xxxxxxxxxx)

 

加载地形

 

现在该是进入代码部分的时候了!第一步就是加载瓦片地图中的地形层。为了更好的理解其工作原理,不妨在Tiled中打开StageMap.tmx文件,如下:

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

 

这时你会看到四种不同类型的地形,(事实上还有一些瓦片,但多出的这些只不过是湖泊地形的不同版本而已,纯粹为了装饰使用。)

邮件单击tileset窗口中的瓦片,并选择”Tile Properties…”(中文版是“图块属性”),会看到两种属性MovementCost和TileType。之所以创建这两个属性,是为了后面在代码中识别每种瓦片的特殊属性。

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

StageMap.tmx这个文件中只包含了5个层:背景层,其中包含了这些用来表示地形的瓦片;两个单位层,用于在地图中放置玩家的单位;两个建筑层,用来放置每个玩家的建筑。

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

现在我们算是大概了解了瓦片地图的构成,接下来看看Xcode中的TurnWars项目。

 

首先我们需要在HelloWorldLayer(当前的主场景层)中加载这个瓦片地图。

 

为此,在Xcode中打开HelloWorldLayer.m,然后使用下面的 代码替代init方法:

-(id) init
{
     if( (self=[super init])) {

        self.isTouchEnabled = YES;
        [self createTileMap];

     }
     return self;
}

以下代码的作用是让当前场景层支持多点触摸,然后调用createTileMap方法。在添加这个方法前,先让我们添加几个实例变量。切换到HelloWorldLayer.h,然后在@interface中添加以下变量:

 CCTMXTiledMap *tileMap;
  CCTMXLayer *bgLayer;
  CCTMXLayer *objectLayer;
  NSMutableArray *tileDataArray;

在以上代码中,我们定义了几个实例变量指向瓦片地图,其中的层,以及一个数组用来保存背景瓦片层中的不同瓦片。

在以上实例变量中,需要将tileDataArray设置为属性,以便后续的调用。添加以下代码:

@property(nonatomic,assign) NSMutableArray *tileDataArray;

 

切换到HelloWorldLayer.m,并合成tileDataArray属性:

@synthesize tileDataArray;

 

此时,我们还需要创建createTileMap方法的说明。在HelloWorldLayer.h中@end的前面添加以下代码:

-(void)createTileMap;

 

现在是不是该实现这个方法了呢?当然,我们可以这样做,不过为了让代码可以顺利通过编译,还需要完成一件事情-添加TileData这个类。这个类的作用是保存项目中要用到的每个瓦片信息,也即刚才在瓦片属性中保存的信息。TileData类需要保存以下信息:

1.Type:瓦片的类型

2.Movement Cost:一个单位要穿越某个瓦片的消耗

3.Position:瓦片的位置,该信息将用于确定单位的移动。

 

在项目中创建一个Objective-C class文件,并命名为TileData,选择其为CCNode的子类。

 

然后使用以下代码替代TileData.h的内容:

#import
#import "cocos2d.h"
#import "HelloWorldLayer.h"

@class HelloWorldLayer;

@interface TileData : CCNode {
    HelloWorldLayer * theGame;
    BOOL selectedForMovement;
    BOOL selectedForAttack;
    int movementCost;
    CGPoint position;
    TileData * parentTile;
    int hScore;
    int gScore;
    int fScore;
    NSString * tileType;
}

@property (nonatomic,readwrite) CGPoint position;
@property (nonatomic,assign) TileData * parentTile;
@property (nonatomic,readwrite) int movementCost;
@property (nonatomic,readwrite) BOOL selectedForAttack;
@property (nonatomic,readwrite) BOOL selectedForMovement;
@property (nonatomic,readwrite) int hScore;
@property (nonatomic,readwrite) int gScore;
@property (nonatomic,assign) NSString * tileType;

+(id)nodeWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType;
-(id)initWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType;
-(int)getGScore;
-(int)getGScoreForAttack;
-(int)fScore;

@end

然后使用以下代码替换TileData.m中的内容:

//
//  TileData.m
//  TurnWars
//
//  Created by Ricky Wang on 5/10/12.
//  Copyright 2012 __MyCompanyName__. All rights reserved.
//

#import "TileData.h"

@implementation TileData

@synthesize parentTile,position,selectedForAttack,selectedForMovement,gScore,hScore,movementCost,tileType;

+(id)nodeWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType {
     return [[[self alloc] initWithTheGame:_game movementCost:_movementCost position:_position tileType:_tileType] autorelease];
}

-(id)initWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType {
     if ((self=[super init])) {
       theGame = _game;
    selectedForMovement = NO;
    movementCost = _movementCost;
    tileType = _tileType;
    position = _position;
    parentTile = nil;
    [theGame addChild:self];
     }
     return self;
}

-(int)getGScore {
  int parentCost = 0;
  if (parentTile) {
    parentCost = [parentTile getGScore];
  }
  return movementCost + parentCost;

}

-(int)getGScoreForAttack {
  int parentCost = 0;
  if(parentTile) {
    parentCost = [parentTile getGScoreForAttack];
  }
  return 1 + parentCost;
}

-(int)fScore {
     return self.gScore + self.hScore;
}

-(NSString *)description {
     return [NSString stringWithFormat:@"%@  pos=[%.0f;%.0f]  g=%d  h=%d  f=%d", [super description], self.position.x, self.position.y, self.gScore, self.hScore, [self fScore]];
}

@end

以上的代码作用很简单,因为它主要就是一个数据容器。如果你对g,h和f值的意思不明白,可以先看看这篇关于http://www.raywenderlich.com/4946/introduction-to-a-pathfinding寻路算法的教程。

 

现在我们已经有了TileData类,接下来需要在HelloWorldLayer中添加导入声明:

 

#import "TileData.h"

@class TileData;

现在可以来添加createTileMap方法了!在HelloWorldLayer.m中,在@end的前面添加以下代码:

-(void)createTileMap {
    // 1 - Create the map
    tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"StageMap.tmx"];
    [self addChild:tileMap];
    // 2 - Get the background layer
    bgLayer = [tileMap layerNamed:@"Background"];
    // 3 - Get information for each tile in background layer
    tileDataArray = [[NSMutableArray alloc] initWithCapacity:5];
    for(int i = 0; i< tileMap.mapSize.height;i++) {
        for(int j = 0; j< tileMap.mapSize.width;j++) {
            int movementCost = 1;
            NSString * tileType = nil;
            int tileGid=[bgLayer tileGIDAt:ccp(j,i)];
            if (tileGid) {
                NSDictionary *properties = [tileMap propertiesForGID:tileGid];
                if (properties) {
                    movementCost = [[properties valueForKey:@"MovementCost"] intValue];
                    tileType = [properties valueForKey:@"TileType"];
                }
            }
            TileData * tData = [TileData nodeWithTheGame:self movementCost:movementCost position:ccp(j,i) tileType:tileType];
            [tileDataArray addObject:tData];
        }
    }
}

在以上代码中:

1.载入项目中所包含的瓦片地图。

2.接下来获取”Background”这个层,其中包含了地图的地形瓦片。

3.接下来创建一个NSMutableArray数组,其中包含了地图中每个瓦片的信息。该数组使用TileData对象创建,其中包含了瓦片层每个瓦片的属性信息。

 

最后,在dealloc方法的前面添加以下代码以释放数组所占用的内存:

[tileDataArray release];

 

此时编译运行项目,会看到屏幕中出现类似下面的画面:

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

创建单位

 

在我们的游戏中包含了四种不同的军事单位:陆战队员,坦克,大炮和直升机。每个单位都有自己的活动范围,也都有各自的优缺点。有些类型的单位只能在特定的地形中穿越。

正如之前在Tiled编辑器中所看到的,地区中包含了两个单位层,每个层对应一个玩家的军事单位。每个玩家都有六个单位,使用灰色的对象瓦片来表示。如果右键单击查看瓦片的属性,会看到它们都有一个“Type”属性。该属性将帮助你判断应该加载哪种单位,单位的类型是什么,以及在哪里放置它们。

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

在创建用于代表玩家单位的Unit类前,我们需要添加几个宏定义,用于判断设备是否支持Retina显示,以及几个常数,同时还有一个枚举变量。在GameConfig.h中添加以下代码:

#define IS_HD ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] == YES && [[UIScreen mainScreen] scale] == 2.0f)

#define TILE_HEIGHT 32
#define TILE_HEIGHT_HD 64

typedef enum tagState {
   kStateGrabbed,
   kStateUngrabbed
} touchState;

接下来,我们将创建Unit类,该类的作用是保存不同军事单位的信息。后面我们还将从Unit类继承出不同的子类,用来代表不同的军事单位。

 

在项目中添加一个新的类Unit,选择其为CCNode的子类。使用以下代码替代Unit.h的内容:

#import
#import "cocos2d.h"
#import "HelloWorldLayer.h"
#import "GameConfig.h"
#import "TileData.h"

@interface Unit : CCNode  {
    HelloWorldLayer * theGame;
    CCSprite * mySprite;
    touchState state;
    int owner;
    BOOL hasRangedWeapon;
    BOOL moving;
    int movementRange;
    int attackRange;
    TileData * tileDataBeforeMovement;
    int hp;
    CCLabelBMFont * hpLabel;
}

@property (nonatomic,assign)CCSprite * mySprite;
@property (nonatomic,readwrite) int owner;
@property (nonatomic,readwrite) BOOL hasRangedWeapon;

+(id)nodeWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner;
-(void)createSprite:(NSMutableDictionary *)tileDict;

@end

以上代码将Unit类定义为CCNode的子类。其中包含的属性用于识别单位的基本特性,以及几个其它的实例变量,从而让单位可以引用到主游戏场景层,以及检测单位是否在移动,单位是否有武器。

在实现Unit.m之前,我们需要添加几个辅助方法,用于处理精灵对象的Retina显示,并设置单位的位置。我们将在HelloWorldLayer类中添加这些辅助方法,因为我们需要访问一些层属性,比如层的大小,而这些信息在Unit类中是没有的。

在HelloWorldLayer.h中添加以下辅助方法的定义:

-(int)spriteScale;
-(int)getTileHeightForRetina;
-(CGPoint)tileCoordForPosition:(CGPoint)position;
-(CGPoint)positionForTileCoord:(CGPoint)position;
-(NSMutableArray *)getTilesNextToTile:(CGPoint)tileCoord;
-(TileData *)getTileData:(CGPoint)tileCoord;

此时,让我们在声明部分添加几个实例变量,以记录每个玩家的单位和当前的回合数:

NSMutableArray *p1Units;
NSMutableArray *p2Units;
int playerTurn;

接下来添加以上变量的属性定义:

@property (nonatomic, assign) NSMutableArray *p1Units;
@property (nonatomic, assign) NSMutableArray *p2Units;
@property (nonatomic, readwrite) int playerTurn;

 

然后切换到HelloWorldLayer.m,并合成这些属性:

@synthesize p1Units;
@synthesize p2Units;
@synthesize playerTurn;

 

同时,我们还需要访问之前在GameConfig.h中定义的几个常数。所以在HelloWorldLayer.m的顶部添加以下代码:

#import "GameConfig.h"

 

然后在HelloWorldLayer.m的底部添加几个辅助方法的实现代码如下:

// Get the scale for a sprite - 1 for normal display, 2 for retina
-(int)spriteScale {
    if (IS_HD)
        return 2;
    else
        return 1;
}

// Get the height for a tile based on the display type (retina or SD)
-(int)getTileHeightForRetina {
    if (IS_HD)
        return TILE_HEIGHT_HD;
     else
         return TILE_HEIGHT;
}

// Return tile coordinates (in rows and columns) for a given position
-(CGPoint)tileCoordForPosition:(CGPoint)position {
    CGSize tileSize = CGSizeMake(tileMap.tileSize.width,tileMap.tileSize.height);
    if (IS_HD) {
        tileSize = CGSizeMake(tileMap.tileSize.width/2,tileMap.tileSize.height/2);
    }
    int x = position.x / tileSize.width;
    int y = ((tileMap.mapSize.height * tileSize.height) - position.y) / tileSize.height;
    return ccp(x, y);
}

// Return the position for a tile based on its row and column
-(CGPoint)positionForTileCoord:(CGPoint)position {
    CGSize tileSize = CGSizeMake(tileMap.tileSize.width,tileMap.tileSize.height);
    if (IS_HD) {
        tileSize = CGSizeMake(tileMap.tileSize.width/2,tileMap.tileSize.height/2);
    }
    int x = position.x * tileSize.width + tileSize.width/2;
    int y = (tileMap.mapSize.height - position.y) * tileSize.height - tileSize.height/2;
    return ccp(x, y);
}

// Get the surrounding tiles (above, below, to the left, and right) of a given tile based on its row and column
-(NSMutableArray *)getTilesNextToTile:(CGPoint)tileCoord {
    NSMutableArray * tiles = [NSMutableArray arrayWithCapacity:4];
    if (tileCoord.y+1=0)
        [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x,tileCoord.y-1)]];
    if (tileCoord.x-1>=0)
        [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x-1,tileCoord.y)]];
    return tiles;
}

// Get the TileData for a tile at a given position
-(TileData *)getTileData:(CGPoint)tileCoord {
    for (TileData * td in tileDataArray) {
        if (CGPointEqualToPoint(td.position, tileCoord)) {
            return td;
        }
    }
    return nil;
}

好了,现在可以添加Unit类的实现代码了。使用以下代码替换Unit.m中的内容:

#import "Unit.h"

#define kACTION_MOVEMENT 0
#define kACTION_ATTACK 1

@implementation Unit

@synthesize mySprite,owner,hasRangedWeapon;

+(id)nodeWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner {
    // Dummy method - implemented in sub-classes
    return nil;
}

-(id)init {
    if ((self=[super init])) {
        state = kStateUngrabbed;
        hp = 10;
    }
    return self;
}

// Create the sprite and HP label for each unit
-(void)createSprite:(NSMutableDictionary *)tileDict {
    int x = [[tileDict valueForKey:@"x"] intValue]/[theGame spriteScale];
    int y = [[tileDict valueForKey:@"y"] intValue]/[theGame spriteScale];
    int width = [[tileDict valueForKey:@"width"] intValue]/[theGame spriteScale];
    int height = [[tileDict valueForKey:@"height"] intValue];
    int heightInTiles = height/[theGame getTileHeightForRetina];
    x += width/2;
    y += (heightInTiles * [theGame getTileHeightForRetina]/(2*[theGame spriteScale]));
    mySprite = [CCSprite spriteWithFile:[NSString stringWithFormat:@"%@_P%d.png",[tileDict valueForKey:@"Type"],owner]];
    [self addChild:mySprite];
    mySprite.userData = self;
    mySprite.position = ccp(x,y);
    hpLabel = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d",hp] fntFile:@"Font_dark_size12.fnt"];
    [mySprite addChild:hpLabel];
    [hpLabel setPosition:ccp([mySprite boundingBox].size.width-[hpLabel boundingBox].size.width/2,[hpLabel boundingBox].size.height/2)];
}

// Can the unit walk over the given tile?
-(BOOL)canWalkOverTile:(TileData *)td {
    return YES;
}

// Update the HP value display
-(void)updateHpLabel {
    [hpLabel setString:[NSString stringWithFormat:@"%d",hp]];
    [hpLabel setPosition:ccp([mySprite boundingBox].size.width-[hpLabel boundingBox].size.width/2,[hpLabel boundingBox].size.height/2)];
}

-(void)onEnter {
    [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
    [super onEnter];
}

-(void)onExit {
    [[CCTouchDispatcher sharedDispatcher] removeDelegate:self];
    [super onExit];
} 

// Was this unit below the point that was touched?
-(BOOL)containsTouchLocation:(UITouch *)touch {
    if (CGRectContainsPoint([mySprite boundingBox], [self convertTouchToNodeSpaceAR:touch])) {
        return YES;
    }
    return NO;
}

// Handle touches
-(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
    if (state != kStateUngrabbed)
        return NO;
    if (![self containsTouchLocation:touch])
        return NO;
    state = kStateGrabbed;
    return YES;
}

-(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {
    state = kStateUngrabbed;
}

-(void)dealloc {
    [super dealloc];
}

@end

到目前为止,Unit类的内容还很简单。init方法使用军事单位的最大命中次数(对每个单位都是一样的)来初始化该类,并设置了单位的初始状态。

 

接下来使用createSprite方法来处理从瓦片地图中获取的信息。在该方法中会接收单位的位置,大小,类型等信息,并根据其类型来创建一个精灵对象。此外,其中的代码还将在精灵对象的顶部添加一个标签用于显示单位剩余的HP。

剩下的代码主要用于检测单位上的多点触摸事件,其作用是选择每个单位,以便移动它们。

ok,现在我们有了一个Unit类可以作为所有玩家单位的基础,接下来将创建四个不同的子类,这些子类都继承自Unit。这四个类将决定我们的不同单位类型。在这篇教程中,四个子类的代码内容基本上是相似的,不过我们也可以自行添加一些特殊的属性或方法。

 

首先来创建Soldier类,在项目中添加Unit_Soldier.h和Unit_Soldier.m,并设置Unit_Soldier类为Unit类的子类。

 

使用以下代码替代Unit_Soldier.h中的内容:

#import
#import "cocos2d.h"
#import "Unit.h"

@interface Unit_Soldier : Unit {

}

-(id)initWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner;
-(BOOL)canWalkOverTile:(TileData *)td;

@end

然后使用以下代码替代Unit_Soldier.m中的内容:

#import "Unit_Soldier.h"

@implementation Unit_Soldier

+(id)nodeWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner {
    return [[[self alloc] initWithTheGame:_game tileDict:tileDict owner:_owner] autorelease];
}

-(id)initWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner {
    if ((self=[super init])) {
        theGame = _game;
        owner= _owner;
        movementRange = 3;
        attackRange = 1;
        [self createSprite:tileDict];
        [theGame addChild:self z:3];
    }
    return self;
}

-(BOOL)canWalkOverTile:(TileData *)td {
    return YES;
}

@end

以上代码的内容很简单,在初始化方法中会设置一些基本属性,如活动范围(每回合可以移动的瓦片数),以及攻击范围(到可攻击敌人单位的瓦片距离)。canWalkOverTile方法用于判断是否该单位可以通过特定的地形。例如,坦克不能从代表湖泊的瓦片上通过,在Tank类中,我们需要判断某个特定的瓦片是否属于代表湖泊的瓦片。而对于陆战队员来说,它可以穿越所有的地形,因此这里只返回YES。

 

为了确保项目可以运行,我们需要创建所有的四个单位类。为方便起见,目前只需要将Soldier类的实现代码复制到其它类即可。将这些新类分别命名为Unit_Tank,Unit_Cannon和Unit_Helicopter。需要注意的是,在复制代码的时候需要更改变量的值。

 

当所有的单位就绪后,我们就可以在游戏中设置单位了!首先在HelloWorldLayer.m的顶部添加代码导入主要的Unit类:

#import "Unit.h"

 

 

为了加载单位,我们还需要一个新的辅助方法。在HelloWorldLayer.m的底部添加下面的方法:

-(void)loadUnits:(int)player {
    // 1 - Retrieve the layer based on the player number
    CCTMXObjectGroup * unitsObjectGroup = [tileMap objectGroupNamed:[NSString stringWithFormat:@"Units_P%d",player]];
    // 2 - Set the player array
    NSMutableArray * units = nil;
    if (player ==1)
        units = p1Units;
    if (player ==2)
        units = p2Units;
    // 3 - Load units into player array based on the objects on the layer
    for (NSMutableDictionary * unitDict in [unitsObjectGroup objects]) {
        NSMutableDictionary * d = [NSMutableDictionary dictionaryWithDictionary:unitDict];
        NSString * unitType = [d objectForKey:@"Type"];
        NSString *classNameStr = [NSString stringWithFormat:@"Unit_%@",unitType];
        Class theClass = NSClassFromString(classNameStr);
        Unit * unit = [theClass nodeWithTheGame:self tileDict:d owner:player];
        [units addObject:unit];
    }
}

在以上的代码中,我们首先根据玩家编号来获取正确的单位层。对于某个找到的对象,获取其信息并根据对象类型来初始化一个Unit对象。稍微复杂一点的部分在编号为3的注释部分。这里我们从对象中获取单位类型,并根据该类型创建正确的Unit子类(Unit_Soldier,Unit_Tank,Unit_Cannon或Unit_Helicopter)。由于所有这几个类都包含nodeWithTheGame:tileDict:owner这个方法,因此调用该方法将从正确的Unit子类中创建一个对象。

ok,接下来我们可以在游戏中实际加载单位了。首先在HelloWorldLayer.m的init方法中调用createTileMap方法的后面添加代码,以初始化每个玩家的单位数组:

//Load Units
    p1Units = [[NSMutableArray alloc]initWithCapacity:10];
    p2Units = [[NSMutableArray alloc]initWithCapacity:10];
    [self loadUnits:1];
    [self loadUnits:2];

当然,别忘了在dealloc中添加代码释放内存:

 [p1Units release];
  [p2Units release];

好了~现在我们可以编译运行项目,可以看到每个玩家的单位都显示在屏幕中的瓦片地图上。

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

 

选择单位

 

现在我们只是让大宋和菲佣的军事单位出现在屏幕中,但如果双方站在那里大眼瞪小眼(是的,这就是传说中的对峙!),毛用也没有!我们需要让这些军事单位动起来。要实现这一点,可以分为以下步骤来实现:

1.让玩家可以通过触碰来选择某个单位

2.标出该单位可以移动到的瓦片位置

3.让玩家触碰被标出的瓦片,从而将单位移动到那里

4.最后移动单位,当然,需要避开那些无法通过的瓦片。

 

注意:为了实现上面的目的,我们需要用到寻路算法(特别是最后一步)。这篇教程中不会对寻路算法做过多解释,感兴趣的盆友可以看看这两篇文章(http://www.raywenderlich.com/4946/introduction-to-a-pathfinding 

 和http://www.raywenderlich.com/4970/how-to-implement-a-pathfinding-with-cocos2d-tutorial)

 

还是来敲代码吧。首先我们需要设置代码来允许玩家选中某个单位。在Unit.h中添加以下实例变量,以记录不同单位的移动状态:

NSMutableArray *spOpenSteps;
    NSMutableArray *spClosedSteps;
    NSMutableArray * movementPath;
    BOOL movedThisTurn;
    BOOL attackedThisTurn;
    BOOL selectingMovement;
    BOOL selectingAttack;

现在切换到Unit.m,在init方法的最后添加以下代码(在hp = 10的后面):

spOpenSteps = [[NSMutableArray alloc] init];
    spClosedSteps = [[NSMutableArray alloc] init];
    movementPath = [[NSMutableArray alloc] init];

接下来,我们需要在Unit类中添加一些辅助方法来处理单位的移动问题。不过,这些辅助方法需要用到一些只能在HelloWorldLayer中添加的其它辅助方法,因为它们需要访问的信息将会对整个场景层起作用。由于我们在HelloWorldLayer.m已经导入了Unit类的头文件,因此没有必要在HelloWorldLayer.h导入Unit.h头文件(否则会导致循环引用)。因此只需在HelloWorldLayer.h中添加以下代码:

 

@class Unit;

 

 

然后在类声明部分添加以下实例变量:

Unit *selectedUnit;

 

接下来添加以下辅助方法定义:

-(Unit *)otherUnitInTile:(TileData *)tile;
-(Unit *)otherEnemyUnitInTile:(TileData *)tile unitOwner:(int)owner;
-(BOOL)paintMovementTile:(TileData *)tData;
-(void)unPaintMovementTile:(TileData *)tileData;
-(void)selectUnit:(Unit *)unit;
-(void)unselectUnit;

现在切换到HelloWorldLayer.m,并在文件的底部添加以上方法的实现代码:

// Check specified tile to see if there's any other unit (from either player) in it already
-(Unit *)otherUnitInTile:(TileData *)tile {
    for (Unit *u in p1Units) {
        if (CGPointEqualToPoint([self tileCoordForPosition:u.mySprite.position], tile.position))
            return u;
    }
    for (Unit *u in p2Units) {
        if (CGPointEqualToPoint([self tileCoordForPosition:u.mySprite.position], tile.position))
            return u;
    }
    return nil;
}

// Check specified tile to see if there's an enemy unit in it already
-(Unit *)otherEnemyUnitInTile:(TileData *)tile unitOwner:(int)owner {
    if (owner == 1) {
        for (Unit *u in p2Units) {
            if (CGPointEqualToPoint([self tileCoordForPosition:u.mySprite.position], tile.position))
                return u;
        }
    } else if (owner == 2) {
        for (Unit *u in p1Units) {
            if (CGPointEqualToPoint([self tileCoordForPosition:u.mySprite.position], tile.position))
                return u;
        }
    }
    return nil;
}

// Mark the specified tile for movement, if it hasn't been marked already
-(BOOL)paintMovementTile:(TileData *)tData {
    CCSprite *tile = [bgLayer tileAt:tData.position];
    if (!tData.selectedForMovement) {
        [tile setColor:ccBLUE];
        tData.selectedForMovement = YES;
        return NO;
    }
    return YES;
}

// Set the color of a tile back to the default color
-(void)unPaintMovementTile:(TileData *)tileData {
    CCSprite * tile = [bgLayer tileAt:tileData.position];
    [tile setColor:ccWHITE];
}

// Select specified unit
-(void)selectUnit:(Unit *)unit {
    selectedUnit = nil;
    selectedUnit = unit;
}

// Deselect the currently selected unit
-(void)unselectUnit {
    if (selectedUnit) {
        [selectedUnit unselectUnit];
    }
    selectedUnit = nil;
}

ok,现在Unit类所需的辅助方法已经就位,现在可以在Unit类中添加辅助方法了。首先切换到Unit.h,并添加以下代码:

-(void)selectUnit;
-(void)unselectUnit;
-(void)unMarkPossibleMovement;
-(void)markPossibleAction:(int)action;

最后切换到Unit.m,并在文件底部添加以上方法的实现代码:

// Select this unit
-(void)selectUnit {
    [theGame selectUnit:self];
    // Make the selected unit slightly bigger
    mySprite.scale = 1.2;
    // If the unit was not moved this turn, mark it as possible to move
    if (!movedThisTurn) {
        selectingMovement = YES;
        [self markPossibleAction:kACTION_MOVEMENT];
    }
}

// Deselect this unit
-(void)unselectUnit {
    // Reset the sprit back to normal size
    mySprite.scale =1;
    selectingMovement = NO;
    selectingAttack = NO;
    [self unMarkPossibleMovement];
}

// Remove the "possible-to-move" indicator
-(void)unMarkPossibleMovement {
    for (TileData * td in theGame.tileDataArray) {
        [theGame unPaintMovementTile:td];
        td.parentTile = nil;
        td.selectedForMovement = NO;
    }
}

// Carry out specified action for this unit
-(void)markPossibleAction:(int)action {
    // Get the tile where the unit is standing
    TileData *startTileData = [theGame getTileData:[theGame tileCoordForPosition:mySprite.position]];
    [spOpenSteps addObject:startTileData];
    [spClosedSteps addObject:startTileData];
    // If we are selecting movement, paint the tiles
    if (action == kACTION_MOVEMENT) {
        [theGame paintMovementTile:startTileData];
    }
    // else if(action == kACTION_ATTACK)  // You'll handle attacks later
    int i =0;
    // For each tile in the list, beginning with the start tile
    do {
        TileData * _currentTile = ((TileData *)[spOpenSteps objectAtIndex:i]);
        // You get every 4 tiles surrounding the current tile
        NSMutableArray * tiles = [theGame getTilesNextToTile:_currentTile.position];
        for (NSValue * tileValue in tiles) {
            TileData * _neighbourTile = [theGame getTileData:[tileValue CGPointValue]];
            // If you already dealt with it, you ignore it.
            if ([spClosedSteps containsObject:_neighbourTile]) {
                // Ignore it
                continue;
            }
            // If there is an enemy on the tile and you are moving, ignore it. You can't move there.
            if (action == kACTION_MOVEMENT && [theGame otherEnemyUnitInTile:_neighbourTile unitOwner:owner]) {
                // Ignore it
                continue;
            }
            // If you are moving and this unit can't walk over that tile type, ignore it.
            if (action == kACTION_MOVEMENT && ![self canWalkOverTile:_neighbourTile]) {
                // Ignore it
                continue;
            }
            _neighbourTile.parentTile = nil;
            _neighbourTile.parentTile = _currentTile;
            // If you can move over there, paint it.
            if (action == kACTION_MOVEMENT) {
                [theGame paintMovementTile:_neighbourTile];
            }
            // else if(action == kACTION_ATTACK) //You'll handle attacks later
            // Check how much it costs to move to or attack that tile.
            if (action == kACTION_MOVEMENT) {
                if ([_neighbourTile getGScore]> movementRange) {
                    continue;
                }
            } else if(action == kACTION_ATTACK) {
                //You'll handle attacks later
            }
            [spOpenSteps addObject:_neighbourTile];
            [spClosedSteps addObject:_neighbourTile];
        }
        i++;
    } while (i < [spOpenSteps count]);
    [spClosedSteps removeAllObjects];
    [spOpenSteps removeAllObjects];
}

现有所有用于处理单位选择的辅助方法都已经有了,我们只需在检测到屏幕上的触摸事件时实现相关的代码即可。在ccTouchesBegan方法的最后(在return语句前)添加以下代码:

[theGame unselectUnit];
    [self selectUnit];

此时,当程序检测到玩家触摸某个单位时,就会取消选中当前备选的单位(通过HelloWorldLayer,因为其中记录了当前被选中的单位),并将所触摸的单位标记为新的备选单位。这样就会通过markPossibleAction:方法来标记该单位可以移动到的瓦片区域。

编译运行游戏,现在我们可以触摸任何一个单位,然后其周围的特定瓦片区域会使用蓝色标出,表明这些地方是当前单位可以移动到的位置。

 

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

当然,如果你比较细心,很快就会发现有些不太对的地方-比如坦克可以穿越山脉和湖泊,而且所有的单位都可以移动同样的格数。如果你记性好的话,应该还记得我们对所有的单位都用了和陆战队员单位同样的代码,这样所有单位的活动方式都是相同的。

如果我们想让每个单位有不同的活动能力,那么只需更改Unit子类中的movementRange变量值即可。比如,我们可以将直升机的活动范围设置为7,这也比较符合常理。

此外,我们还可以通过修改canWalkOverTile:方法来修正活动限制范围(比如大宋的99式主站坦克和122自行火炮是没法在水中航行的),如下所示:

-(BOOL)canWalkOverTile:(TileData *)td {
    if ([td.tileType isEqualToString:@"Mountain"] || [td.tileType isEqualToString:@"River"]) {
        return NO;
    }
    return YES;
}

再次编译运行游戏,会看到当我们选中坦克或火炮时,代表湖泊和山脉的瓦片是不会显示为可穿越地带的。

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

移动单位

 

选择完单位后,当然是希望它们可以到处移动的,总不能杵在那里一个多月吧!

在前面的步骤中我们已经确定了单位可以移动到的位置,如果为了图简便,当然可以让单位直接穿山越岭跑山涉水抵达目的地。不过在实际游戏中可能会比较奇怪,比如坦克直接在水中游泳抵达了目的地。

为了解决这一问题,我们需要找到一个方法,可以让单位一个瓦片一个瓦片的移动,并找到最短的最合逻辑的路径。这里我们将学到这篇寻路算法教程中的代码http://www.raywenderlich.com/4970/how-to-implement-a-pathfinding-with-cocos2d-tutorial),稍作修改而已。

 

我们将添加一些方法来处理单位的移动问题。不过首先需要在Unit.h中添加方法的定义:

-(void)insertOrderedInOpenSteps:(TileData *)tile;
-(int)computeHScoreFromCoord:(CGPoint)fromCoord toCoord:(CGPoint)toCoord;
-(int)costToMoveFromTile:(TileData *)fromTile toAdjacentTile:(TileData *)toTile;
-(void)constructPathAndStartAnimationFromStep:(TileData *)tile;
-(void)popStepAndAnimate;
-(void)doMarkedMovement:(TileData *)targetTileData;

然后在Unit.m中添加以上方法的是实现代码:

-(void)insertOrderedInOpenSteps:(TileData *)tile {
    // Compute the step's F score
    int tileFScore = [tile fScore];
    int count = [spOpenSteps count];
    // This will be the index at which we will insert the step
    int i = 0;
    for (; i < count; i++) {
        // If the step's F score is lower or equals to the step at index i
    if (tileFScore

编译运行游戏。现在我们可以选中一个单位,看看它可以移动到哪里,然后触摸被标出的瓦片,并让单位移动到那里。你可以不断尝试,看看双方的军事单位在靠近时会发生些什么。

蛤蛤!你猜对了!神马也不会发生,双方依然对峙!

鈥溨糕澤咸副- <wbr>如何使用Cocos2D制作一款iPhone回合制策略游戏1

 

好了,以上就是教程的第一部分!很快会奉上第二部分,希望届时大宋的军队届时给点力,别这样对峙来对峙去没意思!

源代码如下:

http://d1xzuxjlafny7l.cloudfront.net/downloads/TurnWars-Part1.zip

标签: none

?>