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 4 of 5 of this article. Click here to view the first page.

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

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.