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

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 […] By .

5 (1) · 1 Review

Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

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 :]