How to Make a Turn-Based Strategy Game – Part 1

This is a post by iOS Tutorial Team Member Pablo Ruiz, an iOS game developer, and co-founder and COO at InfinixSoft. Check out his blog, or follow him on Twitter. In this tutorial, you’ll learn how to create a turn-based strategy game for the iPhone. I’ve always liked this type of game, the Advance Wars […] By .

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Creating the Units

Your game will have four different types of units: Soldier, Tank, Cannon, and Helicopter. Each unit type will have its own movement range, and a set of strengths and weaknesses when combatting other units. Some types will be restricted from moving through certain terrains.

As you saw earlier in Tiled, the map contains two layers for player units, one for each player's units. Each player has six units, represented by gray object tiles. If you check a unit's properties (right-click and select "Object Properties...") you'll be able to see that they all have a "Type" property. This will help you determine which units to load, what kind of units they are and where to position them.

Object properties in Tiled

But before you create the Unit class to represent player units, we need to add a a macro to detect whether the device has a retina display or not, a couple of constants, and a helper enum for detecting touches. Open GameConfig.h and add the following lines of code at the bottom of the file:

#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;

Next, we create the Unit class that will hold most of the information for your different units. Later, you'll create other classes for each unit type that will inherit from the Unit class and define unique properties specific to each type of unit.

Add Unit.h and Unit.m files to your project by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Unit, and make it a subclass of CCNode.

Replace Unit.h with the following:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "HelloWorldLayer.h"
#import "GameConfig.h"
#import "TileData.h"

@interface Unit : CCNode <CCTargetedTouchDelegate> {
    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

The above simply defines the Unit class as a sub-class of CCNode. It contains properties which identify basic attributes of the unit and also has a few additional instance variables which allow the unit to refer back to the main game layer, HelloWorldLayer, or detect whether the unit is moving or whether the unit has a weapon.

But before you can add the code for the Unit class implementation, we need a few helper methods in place which are useful in handling sprite dimensions for retina display and allow you to set the position of units. We will add these helper methods to the HelloWorldLayer class since we need access to some layer properties, such as the size of the layer, that we would not have from within the Unit class.

Add the definitions for the helper methods to HelloWorldLayer.h (above the @end) as follows:

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

While we're at it, let's also add a couple of instance variables to HelloWorldLayer to keep track of the units for each player and the current turn number. Add the following to HelloWorldLayer.h below the other instance variables:

NSMutableArray *p1Units;
NSMutableArray *p2Units;
int playerTurn;

We'll need to access the above instance variables as properties, so add the property definitions as follows:

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

Now switch to HelloWorldLayer.m and synthesise those properties:

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

We will also be referring to the TILE_HEIGHT and TILE_HEIGHT_HD constants that we defined earlier when we implement our helper methods. So import GameConfig.h at the top of HelloWorldLayer.m:

#import "GameConfig.h"

Now add the implementations for the helper methods you defined above to the end of 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<tileMap.mapSize.height)
        [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x,tileCoord.y+1)]];
    if (tileCoord.x+1<tileMap.mapSize.width)
        [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x+1,tileCoord.y)]];
    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;
}

Finally, we can add the code for the Unit class implementation :] Replace the contents of Unit.m with the following:

#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

The Unit class is pretty basic right now. init initializes the class with its max hit points (the same for every unit) and sets the initial state for the unit.

Then you have createSprite which will handle the information received from parsing the tilemap. createSprite receives the unit's position, dimensions, unit type, etc., and creates a sprite to display the unit based on its type. Also, the code adds a label on top of the sprite to display the unit's remaining HP.

The rest of the code mostly deals with detecting touches on the units and is fairly self-explanatory. You'll use this later to be able to select each unit for moving.

Now that you have a Unit class which will act as the base for all player units, you should create four different classes that will inherit from Unit. These four classes will shape your four different unit types. In this tutorial, the code for the four classes is pretty similar, but as you keep working on your game, you may need to add special cases for each unit/scenario.

Let's begin with the Soldier unit. Add Unit_Soldier.h and Unit_Soldier.m files to your project by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Unit_Soldier, and make it a subclass of Unit.

Replace the current code in Unit_Soldier.h with:

#import <Foundation/Foundation.h>
#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

Then, replace Unit_Soldier.m with:

#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 

This class is pretty simple. Its movementRange (the number of tiles it can move per turn) and the attackRange (the distance it has to be from an enemy unit to be able to attack it) are set when the unit is initialized.

You also have a method, canWalkOverTile, which you'll use to determine if the unit can pass through certain terrains. For example, the Tank unit can't move over a water tile and in the Tank class, we'd need to check whether the specified tile is a water tile. In the case of the Solider, it can walk over all terrain and so simply returns YES without any further checks.

In order for your project to work, you must have code in place for all four unit classes. For the time being, create implementations for the other classes by simply copying the code for the Soldier class. Name the new classes Unit_Tank, Unit_Cannon and Unit_Helicopter. Do note that if you copy and pate the code from above, you would need to change all instances of Unit_Soldier in the code to the appropriate value.

Once you have all the unit classes in place, it's time to set up the units in our game! First, import the main Unit class header at the top of HelloWorldLayer.m:

#import "Unit.h"

Next, in order to load our units, we need a new helper method. Add the following method at the end of 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];
    } 
}

The above code retrieves the correct unit layer (either player 1 or player 2 in the tilemap), and for each object found, gets its information and instantiates a Unit object according to the object type. The only complicated bit of code is section #3 where we get the unit type from the object and based on the type, create an instance of the correct Unit sub-class (Unit_Soldier, Unit_Tank, Unit_Cannon, or Unit_Helicopter). Since all of those classes contain the nodeWithTheGame:tileDict:owner: method, the call to that method creates an object from the correct Unit sub-class.

Now that we have all the pieces in place, we can actually load the units into our game. We start off by initializing the arrays for each player's units and loading them from the tilemap in init in HelloWorldLayer.m after the call to createTileMap:

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

And of course, we need to clean up the arrays created above. We do that in dealloc:

[p1Units release];
[p2Units release];

That's it! Now you should be able to compile the project. Go ahead and give it a try! When you load the game you should now see each player's units on screen as they are laid out in the tilemap.