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

Pablo Ruiz
Create a Cool Turn-Based Game with Cocos2D!

Create a Cool Turn-Based Game with Cocos2D!

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.

Welcome to the second half of the tutorial series that walks you through a basic turn-based strategy game for the iPhone!

In the first part of the series, we covered how to load the tile map, initialize military units (soldiers, cannons and helicopters), and how to program their movement on the tile map using the A* pathfinding algorithm.

Now in this second and final part of the series, you’ll enable the units to fulfill their mission in life – make war!

You will add buildings to the map and code multiple ways to win the game. You’ll also incorporate the logic for switching turns between players, as well as some nice finishing details like adding music and sound effects.

Best of all, this project is ripe for expansion and customization so you can make your own Turn-Based Strategy game!

This project starts where we left off in the last tutorial, so make sure you have the project where we left it off if you don’t have it already.

Forward march!

Adding a Context Menu

Before turning to the combat portion of the game, you should spend a few minutes enriching the gameplay experience by adding a context menu. After a player has moved a unit, this menu will prompt them to choose whether the unit should stay in the new location without attacking, revert to its previous location, or, if possible, attack nearby enemy units.

First, as we usually have to in order to support new functionality, we have to add some helper methods. And of course, our helper methods need some instance variables in place to work correctly. So, open the project in Xcode, switch to HelloWorldLayer.h and add the following instance variables:

CCMenu *actionsMenu;
CCSprite *contextMenuBck;

In the above, we’ve defined a CCMenu instance for our context menu and have also created a CCSprite instance which will point to the background for our menu.

We need to make actionsMenu and the previously defined selectedUnit into properties since we’ll need to refer to them from the Unit class later on in our code. So add the following:

@property (nonatomic, assign) Unit *selectedUnit;
@property (nonatomic, assign) CCMenu *actionsMenu;

Of course, we also need to synthesise the above, so switch over to HelloWorldLayer.m add the following at the top of the file:

@synthesize selectedUnit;
@synthesize actionsMenu;

Switch back to HelloWorldLayer.h and add method definitions for the two helper methods we’re going to implement:

-(void)showActionsMenu:(Unit *)unit canAttack:(BOOL)canAttack;
-(void)removeActionsMenu;

Next, switch to HelloWorldLayer.m and add the following methods at the end of the file:

-(void)showActionsMenu:(Unit *)unit canAttack:(BOOL)canAttack {
    // 1 - Get the window size
    CGSize wins = [[CCDirector sharedDirector] winSize];
    // 2 - Create the menu background
    contextMenuBck = [CCSprite spriteWithFile:@"popup_bg.png"];
    [self addChild:contextMenuBck z:19];
    // 3 - Create the menu option labels
    CCLabelBMFont * stayLbl = [CCLabelBMFont labelWithString:@"Stay" fntFile:@"Font_dark_size15.fnt"];
    CCMenuItemLabel * stayBtn = [CCMenuItemLabel itemWithLabel:stayLbl target:unit selector:@selector(doStay)];
    CCLabelBMFont * attackLbl = [CCLabelBMFont labelWithString:@"Attack" fntFile:@"Font_dark_size15.fnt"];
    CCMenuItemLabel * attackBtn = [CCMenuItemLabel itemWithLabel:attackLbl target:unit selector:@selector(doAttack)];
    CCLabelBMFont * cancelLbl = [CCLabelBMFont labelWithString:@"Cancel" fntFile:@"Font_dark_size15.fnt"];
    CCMenuItemLabel * cancelBtn = [CCMenuItemLabel itemWithLabel:cancelLbl target:unit selector:@selector(doCancel)];
    // 4 - Create the menu
    actionsMenu = [CCMenu menuWithItems:nil];
    // 5 - Add Stay button
    [actionsMenu addChild:stayBtn];
    // 6 - Add the Attack button only if the current unit can attack
    if (canAttack) {
        [actionsMenu addChild:attackBtn];
    }
    // 7 - Add the Cancel button
    [actionsMenu addChild:cancelBtn];
    // 8 - Add the menu to the layer
    [self addChild:actionsMenu z:19];
    // 9 - Position menu
    [actionsMenu alignItemsVerticallyWithPadding:5];
    if (unit.mySprite.position.x > wins.width/2) {
        [contextMenuBck setPosition:ccp(100,wins.height/2)];
        [actionsMenu setPosition:ccp(100,wins.height/2)];
    } else {
        [contextMenuBck setPosition:ccp(wins.width-100,wins.height/2)];
        [actionsMenu setPosition:ccp(wins.width-100,wins.height/2)];
    }
}
 
-(void)removeActionsMenu {
    // Remove the menu from the layer and clean up
    [contextMenuBck.parent removeChild:contextMenuBck cleanup:YES];
    contextMenuBck = nil;
    [actionsMenu.parent removeChild:actionsMenu cleanup:YES];
    actionsMenu = nil;
}

The two methods in the above code handle the creation of the context menu and the disposal of the menu when it is no longer needed. Right now the menu only lets the player cancel or confirm their movement, but it will eventually provide a third option for attacking an enemy unit. You’ll add that capability soon.

Now switch to Unit.m and find popStepAndAnimate: and replace the first if statement with the following:

// 1 - Check if there remain path steps to go through
if ([movementPath count] == 0) {
    moving = NO;
    [self unMarkPossibleMovement];
    BOOL enemiesAreInRange = NO;
    // You'll determine later if there is a nearby enemy to attack.
    [theGame showActionsMenu:self canAttack:enemiesAreInRange];
    return;
}
// 2 - Get the next step to move toward

We call the showActionsMenu:canAttack: helper method we just implemented to display the context menu. Currently, the enemiesAreInRange value is hardcoded to NO and so the Attack option in the menu will not be shown.

Add the following lines at the top of ccTouchBegan::

// If the action menu is showing, do not handle any touches on unit
if (theGame.actionsMenu)
    return NO;
// If the current unit is the selected unit, do not handle any touches
if (theGame.selectedUnit == self) 
    return NO;
// If this unit has moved already, do not handle any touches
if (movedThisTurn) 
    return NO;

Next, we need to add the methods which handle the action selected via the context menu. However, one of the actions will require us to detect whether the current unit is a Soldier unit (you’ll understand why, when we get to that bit of code). So, we need to import the header for the Soldier unit at the top of Unit.m:

#import "Unit_Soldier.h"

Finally, we add the following code to the end of the file to implement the context menu actions:

// Stay on the current tile
-(void)doStay {
    // 1 - Remove the context menu since we've taken an action
    [theGame removeActionsMenu];
    movedThisTurn = YES;
    // 2 - Turn the unit tray to indicate that it has moved
    [mySprite setColor:ccGRAY];
    [theGame unselectUnit];
    // 3 - Check for victory conditions
    if ([self isKindOfClass:[Unit_Soldier class]]) {
        // If this is a Soldier unit and it is standing over an enemy building, the player wins.
        // We'll handle this situation in detail later 
    }
}
 
// Attack another unit
-(void)doAttack {
    // You'll handle attack later
}
 
// Cancel the move for the current unit and go back to previous position
-(void)doCancel {
    // Remove the context menu since we've taken an action
    [theGame removeActionsMenu];
    // Move back to the previous tile
    mySprite.position = [theGame positionForTileCoord:tileDataBeforeMovement.position];
    [theGame unselectUnit];
}

And that’s it!

Build and run the project. You should be able to see this menu and interact with it after moving any unit.

Handling Turns

If you play with the latest build, you may notice that now, once you move a unit, you can’t move it again, ever. You may have also noticed that since you began the project, there hasn’t been any way to switch between players – you can move any player’s units at any time!

While these aspects may make for a surreal and interesting game, the topic of this tutorial is, after all, turn-based gaming. So, to stick with the program, you’ll add a HUD at the top of the screen to indicate which player’s turn it is. The HUD will also provide a button to pass the turn to the other player. And of course, players will only be able to move their own units during their turn.

To get going on these additions, let’s add a few more instance variables to keep track of things. Switch to HelloWorldLayer.h and add the following:

CCMenuItemImage *endTurnBtn;
CCLabelBMFont *turnLabel;

Then, add the following method definitions:

-(void)addMenu;
-(void)doEndTurn;
-(void)setPlayerTurnLabel;
-(void)showEndTurnTransition;
-(void)beginTurn;
-(void)removeLayer:(CCNode *)n;
-(void)activateUnits:(NSMutableArray *)units;

Now switch to HelloWorldLayer.m, add the following lines at the end of init (after loading units):

// Set up turns
playerTurn = 1;
[self addMenu];

Next, add all the new methods you defined above to the end of the file:

// Add the user turn menu
-(void)addMenu {
    // Get window size
    CGSize wins = [[CCDirector sharedDirector] winSize];
    // Set up the menu background and position
    CCSprite * hud = [CCSprite spriteWithFile:@"uiBar.png"];
    [self addChild:hud];
    [hud setPosition:ccp(wins.width/2,wins.height-[hud boundingBox].size.height/2)];
    // Set up the label showing the turn 
    turnLabel = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"Player %d's turn",playerTurn] fntFile:@"Font_dark_size15.fnt"];
    [self addChild:turnLabel];
    [turnLabel setPosition:ccp([turnLabel boundingBox].size.width/2 + 5,wins.height-[hud boundingBox].size.height/2)];
    // Set the turn label to display the current turn
    [self setPlayerTurnLabel];
    // Create End Turn button
    endTurnBtn = [CCMenuItemImage itemFromNormalImage:@"uiBar_button.png" selectedImage:@"uiBar_button.png" target:self selector:@selector(doEndTurn)];
    CCMenu * menu = [CCMenu menuWithItems:endTurnBtn, nil];
    [self addChild:menu];
    [menu setPosition:ccp(0,0)];
    [endTurnBtn setPosition:ccp(wins.width - 3 - [endTurnBtn boundingBox].size.width/2, wins.height - [endTurnBtn boundingBox].size.height/2 - 3)];
}
 
// End the turn, passing control to the other player
-(void)doEndTurn {
    // Do not do anything if a unit is selected
    if (selectedUnit)
        return;
    // Switch players depending on who's currently selected
    if (playerTurn ==1) {
        playerTurn = 2;
    } else if (playerTurn ==2) {
        playerTurn = 1;
    }
    // Do a transition to signify the end of turn
    [self showEndTurnTransition];
    // Set the turn label to display the current turn
    [self setPlayerTurnLabel];
}
 
// Set the turn label to display the current turn
-(void)setPlayerTurnLabel {
    // Set the label value for the current player
    [turnLabel setString:[NSString stringWithFormat:@"Player %d's turn",playerTurn]];
    // Change the label colour based on the player
    if (playerTurn ==1) {
        [turnLabel setColor:ccRED];
    } else if (playerTurn == 2) {
        [turnLabel setColor:ccBLUE];
    }
}
 
// Fancy transition to show turn switch/end
-(void)showEndTurnTransition {
    // Create a black layer
    ccColor4B c = {0,0,0,0};
    CCLayerColor *layer = [CCLayerColor layerWithColor:c];
    [self addChild:layer z:20];
    // Add a label showing the player turn to the black layer
    CCLabelBMFont * turnLbl = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"Player %d's turn",playerTurn] fntFile:@"Font_silver_size17.fnt"];
    [layer addChild:turnLbl];
    [turnLbl setPosition:ccp([CCDirector sharedDirector].winSize.width/2,[CCDirector sharedDirector].winSize.height/2)];
    // Run an action which fades in the black layer, calls the beginTurn method, fades out the black layer, and finally removes it
    [layer runAction:[CCSequence actions:[CCFadeTo actionWithDuration:1 opacity:150],[CCCallFunc actionWithTarget:self selector:@selector(beginTurn)],[CCFadeTo actionWithDuration:1 opacity:0],[CCCallFuncN actionWithTarget:self selector:@selector(removeLayer:)], nil]];
}
 
// Begin the next turn
-(void)beginTurn {
    // Activate the units for the active player
    if (playerTurn ==1) {
        [self activateUnits:p2Units];
    } else if (playerTurn ==2) {
        [self activateUnits:p1Units];
    }
}
 
// Remove the black layer added for the turn change transition
-(void)removeLayer:(CCNode *)n {
    [n.parent removeChild:n cleanup:YES];
}

If you look at the method definitions we added to HelloWorldLayer.h and compare them against the methods we added just now to HelloWorldLayer.m, you’ll notice that we’re missing one method – activateUnits:. There’s a reason for this. The activateUnits: activates all the units belonging to the currently active player. However, we have no method in the Unit class to do this.

So let’s switch to Unit.h and add a method definition for a new helper method:

-(void)startTurn;

And the next step, of course, is to add the method implementation to the end of Unit.m:

// Activate this unit for play
-(void)startTurn {
    // Mark the unit as not having moved for this turn
    movedThisTurn = NO;
    // Mark the unit as not having attacked this turn
    attackedThisTurn = NO;
    // Change the unit overlay colour from gray (inactive) to white (active)
    [mySprite setColor:ccWHITE];
}

Now that we have the necessary helper method, we can switch back to HelloWorldLayer.m and add the activateUnits: implementation to the end of the file:

// Activate all the units in the specified array (called from beginTurn passing the units for the active player)
-(void)activateUnits:(NSMutableArray *)units {
    for (Unit *unit in units) {
        [unit startTurn];
    }
}

As you’ll notice, the method gets passed the unit array for the currently active player and the method loops through each of the units in the array, activating each in turn using the startTurn method that we implemented above.

Are we done? Well, almost :] The code can be built and run at this point and it would mostly work as expected. However, there’s one issue – there is nothing in the code to prevent player 1′s (or 2′s) units being selected when it’s not his/her turn since we don’t set the units not in play to deactivated status.

We can fix this pretty easily. Open Unit.m and add the following lines of code to the top of ccTouchBegan::

// Was a unit belonging to the non-active player touched? If yes, do not handle the touch
if (([theGame.p1Units containsObject:self] && theGame.playerTurn == 2) || ([theGame.p2Units containsObject:self] && theGame.playerTurn == 1)) 
    return NO;

All we do is check if the touched unit belongs to player 1 and if the active player is player 2 (or vice versa) since if the touched unit does not belong to the active player, we should not do anything.

Try building and running the project now. You should see a beautiful button in the top-right corner that allows the current player to pass his/her turn. A player can move each of their units once during their turn, then the unit becomes inactive till the next turn. Soon, they’ll be able to attack as well!

When you end the turn, you should see the transition effect in play.

Attacking Other Units

It’s time to handle combat, our reason for playing! It will work as follows:

  • After moving a unit, if it is in combat range (immediately adjacent to an enemy unit, except for the cannon, which has a bigger range), units it can attack will be marked in red.
  • If the player selects one of the marked units, combat will be begin.
  • The attacker unit will shoot first, and if the defending unit survives the attack, it will retaliate.
  • The damage dealt by each unit will depend upon the types of units involved, pretty similar to a rock-paper-scissors game. (And no, the Lizard-Spock variant will not be allowed :D) Each unit type is strong against some units and weaker against others.

The first thing to be done is to check for enemy units nearby, immediately after a unit completes moving. To do this, switch to Unit.m and replace section #1 of popStepAndAnimate: with the following:

// 1 - Check if the unit is done moving
if ([movementPath count] == 0) {
    // 1.1 - Mark the unit as not moving
    moving = NO;
    [self unMarkPossibleMovement];
	// 1.2 - Mark the tiles that can be attacked
    [self markPossibleAction:kACTION_ATTACK];
    // 1.3 - Check for enemies in range
    BOOL enemiesAreInRange = NO;
    for (TileData *td in theGame.tileDataArray) {
        if (td.selectedForAttack) {
            enemiesAreInRange = YES;
            break;
        }
    }
    // 1.4 - Show the menu and enable the Attack option if there are enemies in range
    [theGame showActionsMenu:self canAttack:enemiesAreInRange];
    return;
}

In the above code, in section 1.3 we check tileDataArray, which is an array of all tiles displayed in the background layer, to see if any tiles are marked as selectedForAttack. But how does a tile get set as selectedForAttack? That actually happens when we call markPossibleAction: in section 1.2 but since we haven’t actually implemented the attack action till now, we have to add this functionality to markPossibleAction:.

But before we make the changes to markPossibleAction:, we have to add some helper methods. Why? Because we have no way to check the board for tiles that contain an enemy unit at the moment. And as usual, when we need helper methods with an overall view of the gameplay area, we add the helper methods to HelloWorldLayer.

First, we add the method definitions to HelloWorldLayer.h:

-(BOOL)checkAttackTile:(TileData *)tData unitOwner:(int)owner;
-(BOOL)paintAttackTile:(TileData *)tData;
-(void)unPaintAttackTiles;
-(void)unPaintAttackTile:(TileData *)tileData;

Next, switch over to HelloWorldLayer.m, and add the method implementations to the end of the file:

// Check the specified tile to see if it can be attacked
-(BOOL)checkAttackTile:(TileData *)tData unitOwner:(int)owner {
    // Is this tile already marked for attack, if so, we don't need to do anything further
    // If not, does the tile contain an enemy unit? If yes, we can attack this tile
    if (!tData.selectedForAttack && [self otherEnemyUnitInTile:tData unitOwner:owner]!= nil) {
        tData.selectedForAttack = YES;
        return NO;
    }
    return YES;
}
 
// Paint the given tile as one that can be attacked
-(BOOL)paintAttackTile:(TileData *)tData {
    CCSprite * tile = [bgLayer tileAt:tData.position];
    [tile setColor:ccRED];
    return YES;
}
 
// Remove the attack marking from all tiles
-(void)unPaintAttackTiles {
    for (TileData * td in tileDataArray) {
        [self unPaintAttackTile:td];
    }
}
 
// Remove the attack marking from a specific tile
-(void)unPaintAttackTile:(TileData *)tileData {
    CCSprite * tile = [bgLayer tileAt:tileData.position];
    [tile setColor:ccWHITE];
}

Now that we have the helper methods in place, we can call checkAttackTile:unitOwner: from Unit objects to check each tile adjacent to the moved unit.

We have to do that in several places in markPossibleAction: in Unit.m. First, replace the commented out else if condition about 9 lines down from the top (the one handling the kACTION_ATTACK possibility) with the following:

else if (action == kACTION_ATTACK) {
    [theGame checkAttackTile:startTileData unitOwner:owner];
}

Then, replace the commented out else if condition for kACTION_ATTACK inside the for loop:

else if (action == kACTION_ATTACK) {
    [theGame checkAttackTile:_neighbourTile unitOwner:owner];
}

Finally, there’s a second else if condition for kACTION_ATTACK almost immediately following the above (but not commented out this time) – replace it with the following:

else if (action == kACTION_ATTACK) {
    // Is the tile not in attack range?
    if ([_neighbourTile getGScoreForAttack]> attackRange) {
        // Ignore it
        continue;
    }
}

We also have to implement the doAttack: method, which is called when the Attack option is selected from the context menu. Currently, we have a placeholder method in Unit.m for it. Replace it with the following:

// Attack another unit
-(void)doAttack {
    // 1 - Remove the context menu since we've taken an action
    [theGame removeActionsMenu];
    // 2 - Check if any tile has been selected for attack
    for (TileData *td in theGame.tileDataArray) {
        if (td.selectedForAttack) {
            // 3 - Mark the selected tile as attackable
            [theGame paintAttackTile:td];
        }
    }
    selectingAttack = YES;
}

Once a unit has completed its turn, we have to unmark any tiles marked for attack. Let’s add a method to handle that. And as usual, we add the method definition to Unit.h first:

-(void)unMarkPossibleAttack;

Now add the method to Unit.m (at the end of the file):

// Remove attack selection marking from all tiles
-(void)unMarkPossibleAttack {
    for (TileData *td in theGame.tileDataArray) {
        [theGame unPaintAttackTile:td];
        td.parentTile = nil;
        td.selectedForAttack = NO;
    }
}

Finally, add the following to the end of unselectUnit to call the above method to unmark the tiles marked for attack:

[self unMarkPossibleAttack];

Build and run the program. If you move a unit near an enemy unit and select “attack” from the context menu, you should see the enemy unit get marked in red.

Right now, if you tap on the unit you want to attack, nothing happens. Plus, you might also notice that once a unit is selected to attack, you can’t end your turn either. Get ready to dive into the code that will handle the actual exchange of fire, damage calculation, and destruction of units (and of course, fix other issues like the one mentioned above).

To do this, we need to add a new helper method to HelloWorldLayer which helps you determine the damage from an attack. So add the following method definition to HelloWorldLayer.h:

-(int)calculateDamageFrom:(Unit *)attacker onDefender:(Unit *)defender;

Next, we need to add the method implementation to HelloWorldLayer.m but since our method will need to know about each specific Unit type in order to figure out the damage, we’re going to need to add imports for all the Unit types to the top of HelloWorldLayer.m:

#import "Unit_Soldier.h"
#import "Unit_Tank.h"
#import "Unit_Cannon.h"
#import "Unit_Helicopter.h"

Now add the method implementation to the end of the file:

// Calculate the damage inflicted when one unit attacks another based on the unit type
-(int)calculateDamageFrom:(Unit *)attacker onDefender:(Unit *)defender {
    if ([attacker isKindOfClass:[Unit_Soldier class]]) {
        if ([defender isKindOfClass:[Unit_Soldier class]]) {
            return 5;
        } else if ([defender isKindOfClass:[Unit_Helicopter class]]) {
            return 1;
        } else if ([defender isKindOfClass:[Unit_Tank class]]) {
            return 2;
        } else if ([defender isKindOfClass:[Unit_Cannon class]]) {
            return 4;
        }
    } else if ([attacker isKindOfClass:[Unit_Tank class]]) {
        if ([defender isKindOfClass:[Unit_Soldier class]]) {
            return 6;
        } else if ([defender isKindOfClass:[Unit_Helicopter class]]) {
            return 3;
        } else if ([defender isKindOfClass:[Unit_Tank class]]) {
            return 5;
        } else if ([defender isKindOfClass:[Unit_Cannon class]]) {
            return 8;
        }
    } else if ([attacker isKindOfClass:[Unit_Helicopter class]]) {
        if ([defender isKindOfClass:[Unit_Soldier class]]) {
            return 7;
        } else if ([defender isKindOfClass:[Unit_Helicopter class]]) {
            return 4;
        } else if ([defender isKindOfClass:[Unit_Tank class]]) {
            return 7;
        } else if ([defender isKindOfClass:[Unit_Cannon class]]) {
            return 3;
        }
    } else if ([attacker isKindOfClass:[Unit_Cannon class]]) {
        if ([defender isKindOfClass:[Unit_Soldier class]]) {
            return 6;
        } else if ([defender isKindOfClass:[Unit_Helicopter class]]) {
            return 0;
        } else if ([defender isKindOfClass:[Unit_Tank class]]) {
            return 8;
        } else if ([defender isKindOfClass:[Unit_Cannon class]]) {
            return 8;
        }
    }
    return 0;
}

Next, we have to add methods to the Unit class to carry out an attack and to handle an attack. So let’s add the method definitions for these helper methods to Unit.h:

-(void)doMarkedAttack:(TileData *)targetTileData;
-(void)attackedBy:(Unit *)attacker firstAttack:(BOOL)firstAttack;
-(void)dealDamage:(NSMutableDictionary *)damageData;
-(void)removeExplosion:(CCSprite *)e;

Then, switch to Unit.m and add the method implementations to the end of the file:

// Attack the specified tile
-(void)doMarkedAttack:(TileData *)targetTileData {
    // Mark the unit as having attacked this turn
    attackedThisTurn = YES;
    // Get the attacked unit
    Unit *attackedUnit = [theGame otherEnemyUnitInTile:targetTileData unitOwner:owner];
    // Let the attacked unit handle the attack
    [attackedUnit attackedBy:self firstAttack:YES];
    // Keep this unit in the curren location
    [self doStay];
}
 
// Handle the attack from another unit
-(void)attackedBy:(Unit *)attacker firstAttack:(BOOL)firstAttack {
    // Create the damage data since we need to pass this information on to another method
    NSMutableDictionary *damageData = [NSMutableDictionary dictionaryWithCapacity:2];
    [damageData setObject:attacker forKey:@"attacker"];
    [damageData setObject:[NSNumber numberWithBool:firstAttack] forKey:@"firstAttack"];
    // Create explosion sprite
    CCSprite *explosion = [CCSprite spriteWithFile:@"explosion_1.png"];
    [self addChild:explosion z:10];
    [explosion setPosition:mySprite.position];
    // Create explosion animation
    CCAnimation *animation = [CCAnimation animation];
    for (int i=1;i<=7;i++) {
        [animation addFrameWithFilename: [NSString stringWithFormat:@"explosion_%d.png", i]];
    }
    id action = [CCAnimate actionWithDuration:0.5 animation:animation restoreOriginalFrame:NO];
    // Run the explosion animation, call method to remove explosion once it's done and finally calculate damage from attack    
    [explosion runAction: [CCSequence actions: action,
        [CCCallFuncN actionWithTarget:self selector:@selector(removeExplosion:)], 
        [CCCallFuncO actionWithTarget:self selector:@selector(dealDamage:) object:damageData],
        nil]];
}
 
// Calculate damage from attack
-(void)dealDamage:(NSMutableDictionary *)damageData {
    // 1 - Get the attacker from the passed in data dictionary
    Unit *attacker = [damageData objectForKey:@"attacker"];
    // 2 - Calculate damage
    hp -= [theGame calculateDamageFrom:attacker onDefender:self];
    // 3 - Is the unit dead?
    if (hp<=0) {
        // 4 - Unit is dead - remove it from game
        [self.parent removeChild:self cleanup:YES];
        if ([theGame.p1Units containsObject:self]) {
            [theGame.p1Units removeObject:self];
        } else if ([theGame.p2Units containsObject:self]) {
            [theGame.p2Units removeObject:self];
        }        
    } else {
        // 5 - Update HP for unit
        [self updateHpLabel];
        // 6 - Call attackedBy: on the attacker so that damage can be calculated for the attacker
        if ([[damageData objectForKey:@"firstAttack"] boolValue] && !attacker.hasRangedWeapon && !self.hasRangedWeapon) {
            [attacker attackedBy:self firstAttack:NO];
        }
    }
}
 
// Clean up after explosion
-(void)removeExplosion:(CCSprite *)e {
    // Remove the explosion sprite
    [e.parent removeChild:e cleanup:YES];
}

Since we have all the pieces (or methods, if you will) in place for units attacking each other, we can handle an attack in the game. But first, we need to be able to determine whether we are selecting to move or attack using the currently selected unit. We already have instance variables in the Unit class for this but they weren’t originally set up as properties and so we can’t access them from HelloWorldLayer. So let’s create the properties.

Switch to Unit.h and add the property declarations:

@property (nonatomic,readwrite) BOOL selectingMovement;
@property (nonatomic,readwrite) BOOL selectingAttack;

Then, jump over to Unit.m and synthesise the properties:

@property (nonatomic,readwrite) BOOL selectingMovement;
@property (nonatomic,readwrite) BOOL selectingAttack;

Finally, we handle an attack by adding a new “else” condition to the “if” statement in ccTouchesBegan in HelloWorldLayer.m:

else if(td.selectedForAttack) {
    // Attack the specified tile
    [selectedUnit doMarkedAttack:td];
    // Deselect the unit
    [self unselectUnit];
} else {
    // Tapped a non-marked tile. What do we do?
    if (selectedUnit.selectingAttack) {
        // Was in the process of attacking - cancel attack and show menu
        selectedUnit.selectingAttack = NO;
        [self unPaintAttackTiles];
        [self showActionsMenu:selectedUnit canAttack:YES];
    } else if (selectedUnit.selectingMovement) {
        // Was in the process of moving - just remove marked tiles and await further action
        selectedUnit.selectingMovement = NO;
        [selectedUnit unMarkPossibleMovement];
        [self unselectUnit];
    }
}

And that’s it! We’re set for attacking and destroying enemy units.

Build, run and enjoy some enemy combat :]

Finishing Touches to the Game

The final step in working out gameplay is to make something happen when there are no units left. If you remember from Part 1, this is one of the scenarios for winning.

How do we check this win scenario? It’s pretty simple. After each kill, the program will check if the destroyed unit was that team’s last unit. If it was, the program will display a congratulatory screen and restart the game.

And of course, this means adding a few more helper methods – to check if there are more enemy units and to display the congratulatory message. So switch to HelloWorldLayer.h and add the following method definitions:

-(void)checkForMoreUnits;
-(void)showEndGameMessageWithWinner:(int)winningPlayer;
-(void)restartGame;

And we follow that up with the method implementations themselves in HelloWorldLayer.m (added to the end of the file):

// Check if each player has run out of units
-(void)checkForMoreUnits {
    if ([p1Units count]== 0) {
        [self showEndGameMessageWithWinner:2];
    } else if([p2Units count]== 0) {
        [self showEndGameMessageWithWinner:1];
    }
}
 
// Show winning message for specified player
-(void)showEndGameMessageWithWinner:(int)winningPlayer {
    // Create black layer
    ccColor4B c = {0,0,0,0};
    CCLayerColor * layer = [CCLayerColor layerWithColor:c];
    [self addChild:layer z:20];
    // Add background image to new layer
    CCSprite * bck = [CCSprite spriteWithFile:@"victory_bck.png"];
    [layer addChild:bck];
    [bck setPosition:ccp([CCDirector sharedDirector].winSize.width/2,[CCDirector sharedDirector].winSize.height/2)];
    // Create winning message
    CCLabelBMFont * turnLbl = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"Player %d wins!",winningPlayer]  fntFile:@"Font_dark_size15.fnt"];
    [layer addChild:turnLbl];
    [turnLbl setPosition:ccp([CCDirector sharedDirector].winSize.width/2,[CCDirector sharedDirector].winSize.height/2-30)];
    // Fade in new layer, show it for 2 seconds, call method to remove layer, and finally, restart game
    [layer runAction:[CCSequence actions:[CCFadeTo actionWithDuration:1 opacity:150],[CCDelayTime actionWithDuration:2],[CCCallFuncN actionWithTarget:self selector:@selector(removeLayer:)],[CCCallFunc actionWithTarget:self selector:@selector(restartGame)], nil]];
}
 
// Restart game
-(void)restartGame {
    [[CCDirector sharedDirector] replaceScene:[CCTransitionJumpZoom transitionWithDuration:1 scene:[HelloWorldLayer scene]]];
}

In order to use the methods we defined above, we switch to Unit.m and add a call to checkForMoreUnits at the end of section #4 in dealDamage::

[theGame checkForMoreUnits];

If you’re wondering why we add the call to checkForMoreUnits to section #4, and are confused because section #6 calls attackedBy: on the attacker, do note that when attackedBy: is called on the attacker, that in turn again calls dealDamage: for the attacking unit. So in this secondary run you’ll be calling checkForMoreUnits again.

Basically, when one unit attacks another, checkForMoreUnits

Now, build and run the game and eliminate all the units from one of the teams. After the last one is killed, you'll see a congratulatory screen similar to the one below for the winning team, and then, after a moment, the game will restart.

Adding Buildings

I mentioned at the beginning of the tutorial that there would be two ways to win a game: by eliminating all units from one of the teams, or by having a soldier unit capture the other player’s HQ. It’s time to implement the latter scenario.

First, add a new class, Building, which will be the base for other buildings. In this tutorial you’ll create a simple HQ, but you can use the same technique to create other types of buildings to make your gameplay more interesting.

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

Replace the content of Building.h with the following:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "HelloWorldLayer.h"
#import "GameConfig.h"
 
@class HelloWorldLayer;
 
@interface Building : CCNode {
    HelloWorldLayer *theGame;
    CCSprite *mySprite;
    int owner;
}
 
@property (nonatomic,assign)CCSprite *mySprite;
@property (nonatomic,readwrite) int owner;
 
-(void)createSprite:(NSMutableDictionary *)tileDict;
 
@end

Similarly, replace Building.m with:

#import "Building.h"
 
@implementation Building
 
@synthesize mySprite,owner;
 
-(id)init {
    if ((self=[super init])) {
 
    }
    return self;
}
 
-(void)createSprite:(NSMutableDictionary *)tileDict {
    // Get the sprite position and dimension from tile data
    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];
    // Get the height of the building in tiles
    int heightInTiles = height/[theGame getTileHeightForRetina];
    // Calculate x and y values
    x += width/2;
    y += (heightInTiles * [theGame getTileHeightForRetina]/(2*[theGame spriteScale]));
    // Create building sprite and position it
    mySprite = [CCSprite spriteWithFile:[NSString stringWithFormat:@"%@_P%d.png",[tileDict valueForKey:@"Type"],owner]];
    [self addChild:mySprite];
    mySprite.userData = self;
    mySprite.position = ccp(x,y);
}
 
@end

As you can see, the code is pretty simple and not that different from how you started the Unit class.

Now, you need to create the Building_HQ class that will be a subclass of Building.

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

Replace the contents of Building_HQ.h with the following:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "Building.h"
 
@interface Building_HQ : Building {
 
}
 
+(id)nodeWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner;
-(id)initWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner;
 
@end

Then, replace Building_HQ.m with:

#import "Building_HQ.h"
 
@implementation Building_HQ
 
+(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;
        [self createSprite:tileDict];
        [theGame addChild:self z:1];
    }
    return self;
}
 
@end

The HQ class too is really simple. It doesn’t do much more than just standing there.

Now that we have the building classes in place, we need to add building for both players.

Open HelloWorldLayer.h and add the following instance variables for our buildings:

NSMutableArray *p1Buildings;
NSMutableArray *p2Buildings;

Import the Building header at the top of HelloWorldLayer.h and predefine the class:

#import "Building.h"
 
@class Building;

We’re also going to need several helper methods to load the buildings and to identify the specific building on a given tile. So let’s add the method definitions as follows:

-(void)loadBuildings:(int)player;
-(Building *)buildingInTile:(TileData *)tile;

Now switch to HelloWorldLayer.m and add the following lines just before the [self addMenu] line in init:

// Create building arrays
p1Buildings = [[NSMutableArray alloc] initWithCapacity:10];
p2Buildings = [[NSMutableArray alloc] initWithCapacity:10];
// Load buildings
[self loadBuildings:1];
[self loadBuildings:2];

We need to release memory for the arrays created above when the layer is deallocated. So add the following to dealloc:

[p1Buildings release];
[p2Buildings release];

And of course, we mustn’t forget to implement the helper methods we defined previously. So add the following to the end of the file:

// Load buildings for layer
-(void)loadBuildings:(int)player {
    // Get building object group from tilemap
    CCTMXObjectGroup *buildingsObjectGroup = [tileMap objectGroupNamed:[NSString stringWithFormat:@"Buildings_P%d",player]];
    // Get the correct building array based on the current player
    NSMutableArray *buildings = nil;
    if (player == 1)
        buildings = p1Buildings;
    if (player == 2)
        buildings = p2Buildings;
    // Iterate over the buildings in the array, adding them to the game
    for (NSMutableDictionary *buildingDict in [buildingsObjectGroup objects]) {
        // Get the building type
        NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:buildingDict];
        NSString *buildingType = [d objectForKey:@"Type"];
        // Get the right building class based on type
        NSString *classNameStr = [NSString stringWithFormat:@"Building_%@",buildingType];
        Class theClass = NSClassFromString(classNameStr);
        // Create the building
        Building *building = [theClass nodeWithTheGame:self tileDict:d owner:player];
        [buildings addObject:building];
    }
}
 
// Return the first matching building (if any) on the given tile
-(Building *)buildingInTile:(TileData *)tile {
    // Check player 1's buildings
    for (Building *u in p1Buildings) {
        if (CGPointEqualToPoint([self tileCoordForPosition:u.mySprite.position], tile.position))
            return u;
    }
    // Check player 2's buildings
    for (Building *u in p2Buildings) {
        if (CGPointEqualToPoint([self tileCoordForPosition:u.mySprite.position], tile.position))
            return u;
    }
    return nil;
}

Do note that the logic for buildingInTile: always returns the first building that it finds on a given tile. This could be an issue if two buildings existed on the same tile but since that’s not how our game works, this is not something we have to worry about.

The final step is to make sure the game ends when a soldier unit moves over an enemy HQ. Just open Unit.m and add the following code inside the “if” statement for section #3 in doStay:

// Get the building on the current tile
Building *buildingBelow = [theGame buildingInTile:[theGame getTileData:[theGame tileCoordForPosition:mySprite.position]]];
// Is there a building?
if (buildingBelow) {
    // Is the building owned by the other player?
    if (buildingBelow.owner != self.owner) {
        NSLog(@"Building captured!!!");
        // Show end game message
        [theGame showEndGameMessageWithWinner:self.owner];
    }
}

That’s it! Compile and run the project, and you should see the HQs appear on both sides of the screen. Try moving a soldier unit onto the enemy’s HQ. The game should end at that point.

Music and Sounds

You’re almost done with the project. Let’s add some music and sound effects to jazz things up and you’re set! In case you’re curious, I got the music from Incompetech.com and made the sound effects with CXFR.

First, import SimpleAudioEngine at the top of HelloWorldLayer.m:

#import "SimpleAudioEngine.h"

Now we can use SimpleAudioEngine to play the background music. Add the following line at the end of init (after [self addMenu]):

// Play background music
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"Five Armies.mp3" loop:YES];

Finally, let’s add some sound effects for various events such as units being attacked or buttons getting pressed.

The first event we want to handle is the button click when you select “End Turn”. Add the following line inside doEndTurn, after the first if statement:

// Play sound
[[SimpleAudioEngine sharedEngine] playEffect:@"btn.wav"];

Now, let’s switch over to Unit.m and add several sound effects. But first, we need to import SimpleAudioEngine at the top of the file:

#import "SimpleAudioEngine.h"

Then, add a move sound effect for every tile a unit moves through by adding the following line to popStepAndAnimate just before section #2:

// Play move sound
[[SimpleAudioEngine sharedEngine] playEffect:@"move.wav"];

Next, add a sound effect for damage sustained by adding the following code to attackedBy:firstAttack: before the last line where you play the explosion animation:

// Play damage sound
[[SimpleAudioEngine sharedEngine] playEffect:@"hurt.wav"];

Add a sound effect for a unit being destroyed by adding the following to dealDamage:, as the first line in section #4:

// Unit destroyed sound
[[SimpleAudioEngine sharedEngine] playEffect:@"explosion.wav"];

Finally, add sound effects for a menu selection by adding the following line at the top of doStay, doAttack, and doCancel:

// Play menu selection sound
[[SimpleAudioEngine sharedEngine] playEffect:@"btn.wav"];

And that’s it – we’re done! Build and run the game now, and you should be able to hear all of the music and sound effects.

Where to Go From Here?

Mission Accomplished! Looks like you now have a working turn-based strategy game. Here is the final sample project with everything this tutorial has covered.

But, it’s not time to rest on your glory. Even if you laid the foundations for the game, there are a lot of things you can add to make it even better. Here are a few suggestions:

  • How about adding new, more interesting units? For example, ships that can only move in water, units that can carry soldiers, and medics/mechanics that can heal other units?
  • You could add other types of buildings, like a factory that can build new tanks using some currency, or a hospital.
  • Why have just one map? How about creating new maps players can choose from with other terrain types, or with things like traps, destroyable obstacles, etc.?
  • You could completely change the theme. How about going medieval? Or using a sci-fi theme with alien units?

I’d like to hear about any other ideas you have to improve or modify the game, and your experiences working with this tutorial, so head over to the forums to leave your comments. And happy coding!


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.

Pablo Ruiz is a full-time mobile software developer and entrepreneur. He helps companies increase their profit by generating ideas, advising them and developing mobile apps and games for them through his company, InfinixSoft. Pablo is also the author of Cocos2d for iPhone: Beginner’s guide. To learn more, check out his LinkedIn profile. Pablo loves to talk about technology and games. Follow him on Twitter!

User Comments

13 Comments

  • Great tutorial! Thanks for sharing it!

    An interesting extension for this article (part three perhaps) would be using GameCenter (or some other web based service) for transmitting game state to another player's device.
    ChiSoxKeith
  • aeberbach
  • Any suggestion to change the game to Human vs CPU mode?
    kuboy
  • @kuboy: Giving the turn to the CPU is easy. When you touch the "End turn" button you should check which player's turn it is, if it is P2's, you should prevent the user from interacting with the game.

    Now, the hard part is actually simulating the AI. In AW, (in maps that don't have unit producing buildings) the CPU would always move their troops forward without getting into other unit's fire range unless they had another of their troops in rage to counter attack later.

    You should start by making soldier units move towards the player HQ and maybe making other units move towards attacking the units they are powerful against at.

    Making the AI for such a game is not a simple task (That is why I left it out of the tutorial) and as you add new troops with other features, it starts to get even more difficult.

    The best thing you can do to be able to come up with a nice AI is to actually play any skirmish AW map. The CPU makes the same decisions almost all the time, although it also depend on the map size.
    Pabloruiz55
  • Hi,

    Love your book and your work. Just wanted to point this little correction in the second part.

    Switch to Unit.h and add the property declarations:
    Code: Select all
    @property (nonatomic,readwrite) BOOL selectingMovement;
    @property (nonatomic,readwrite) BOOL selectingAttack;

    Then, jump over to Unit.m and synthesise the properties:
    Code: Select all
    @property (nonatomic,readwrite) BOOL selectingMovement;
    @property (nonatomic,readwrite) BOOL selectingAttack;

    It should be:
    Code: Select all
    @synthesize selectingMovement, selectingAttack;


    Once again, thank you so much for sharing. Hopefully I may have something to shown off for ya :)

    JN
    jujutouch
  • Great timing for this tutorial as I started about 3 weeks ago on my own TBS game. Awesome job on it! Very detailed and well written. Being a noob, the code for my game probably wasn't the most efficient, but I did get a decent amount of a game working with a little AI. Seeing this though has made me want to redo my code structure.

    Playing around with your project, my first thought was to make a bigger map. So I did just that, which brought on the need for additional enhancements. These would be 1) panning the view and 2) making the HUD a separate layer while linking back to the main game layer (otherwise the HUD will be painted on a specific part of the map and not stay at the top of the screen). I had done something similar in my other game, so I utilized that to implement here. If anyone wants to see these I can post some changes, but essentially it is the following:

    -for panning, add code for ccTouchesMoved to change self.position by the difference between the previous location and the current location and limit by the bounds of the map.

    -for the HUD, I added another implementation of a layer within HelloWorldLayer (called HelloWorldHUD) and moved all of the previous menu code to that layer. The addMenu function actually was put into that layer's init. You also would need to create references to the game layer to reference and change playerTurn. The only thing that wouldn't be easy to move is beginTurn as this references the unit arrays, so I just made a function call the game layer function.

    If there are better ideas for these additions I would like to hear them. As I said, I'm a noob and there might be better ways to approach these ideas.
    baranowito
  • hey baranowito you said you "did get a decent amount of a game working with a little AI." do you think i could see your code or at least some of it to see what your doing with AI because this has always been a game style i have wanted to make and think i have a pretty good idea to expand on this tutorial but i dont know how im going to work out the AI. If you could help me out, or anyone else could that would be great.

    Thanks
    AustinF
  • found a small bug with the actions menu. If you move a unit and touch the same unit again after the end actions menu has shown it will call the menu again. Then you have a actionsmenu that wont go away. I added a extra check in the ccTouchesBegan in HelloWorldLayer

    Code: Select all
    if (!actionsMenu ){
         [selectedUnit doMarkedMovement:td];
    }


    I just wrapped the doMarkedMovement call in a check to see if there already is an actions menu.
    Seems to fix it.

    Oh great tutorial by the way! It came at a perfect time. I was looking for examples on turned based games. This was exactly what I needed. Thank you!
    JakeB
  • Thanks a lot for this great Tutorial.

    I just want to point out two "bugs" or more likely points which could be added to the "Where to go from here" list.

    1) the movement cost is always applied to all units - even the helicopter which makes no sense.
    2) a Defending unit can retaliate against cannons even though their attack range is limited.

    Anyway it's a great starting point for a TBS game and a good example for pathfinding.
    falc410
  • Hi, I really enjoyed working through this app and I thought it was excellent, Please keep up the good work and continue to share your knowledge with the rest of us!
    bagellad
  • Hi guys,

    I really enjoyed this tutorial.
    I'm new to Xcode/Cocoa (although have experience of Java/C# etc) and was able to follow and understand the concepts put forward by Pablo.

    I'm trying to take this further by adding other elements to the game and I'm struggling with panning the map (so we can add larger maps).

    I've successfully managed to setup a handlepanFrom recognizer that takes the translation of the drag and adjusts the
    Code: Select all
    self.position
    of the map, it works pretty well with a much larger map, I can pan around and as a baranowito states above you need to change the active player and end turn button into a HUD.

    However I'm running into problems moving the units round once panned, if we move the screen the equivalent of 1 tile down and select a unit, the program correctly paints the correct tiles that this unit can move to, but the check for
    Code: Select all
    td.selectedForMovement
    seems to be absolute? it returns false unless we select 1 tile up of the moveable tile.
    I appreciate that I'm not doing a good job of explaining this so I've attached an image -



    the blue circle is where the unit should be able to move to, but its the tile under the red circle that the program will take as a viable move, not sure how best to address this?

    Thanks,
    Columbo
  • Please update this. :(
    DKL
  • Not to revive an old topic, but I would love love love to see this further developed to show game center (GameKit) real time multiplayer added on. I know a tutorial to do a racing game exists, but it would be great to see how to do a real-time, turn-based, multiplayer game.
    iAtoria

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in July: Facebook Pop Tech Talk!

Sign Up - July

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

... 50 total!

Update Team

  • Riccardo D'Antoni

Editorial Team

  • Matt Galloway

... 23 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Vitaliy Zarubin

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!