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

Selecting Units

Now that you have units appearing on screen, you want to allow moving those units around. This can be broken down into several steps:

  • Enable the user to select a unit by tapping it
  • Mark the tiles that the unit can move to.
  • Enable the user to tap one of the marked tiles to move the unit there.
  • Finally, move the unit while avoiding impassable tiles.

Note: In order to accomplish the above, you'll need to use pathfinding algorithms, especially for the last step. This tutorial will not go over the details of how these algorithms work, but you can find a great explanation in this tutorial and in this one.

First, we need to set up code to allow selecting a unit. Add the following instance variables to Unit.h to keep track of the various unit movement states:

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

Now switch to Unit.m and add the following lines to the end of init (right below hp=10;):

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

The above initialises the various arrays that we use to keep track of different movement elements. But we also need to release those arrays when we're done. So do that at the top of dealloc:

    [movementPath release]; 
    movementPath = nil;
    [spOpenSteps release]; 
    spOpenSteps = nil;
    [spClosedSteps release]; 
    spClosedSteps = nil;

Next, we need to add a bunch of helper methods to the Unit class to handle movement. However, those helper methods need other helper methods that can only be added at the HelloWorldLayer level since those method need access to information dealing with the full layer. And of course, those helper methods will require some supporting instance variables. But one of those instance variables is a pointer to an instance of the Unit class. And since we already import the Unit header in HelloWorldLayer.m and since importing the Unit.h file in HelloWorldLayer.h can result in circular references, we will simply predefine the Unit class in HelloWorldLayer.h. Add the following line below the existing @class TileData line:

@class Unit;

Next, add the following instance variables:

Unit *selectedUnit;
int playerTurn;

Then, add the new helper method definitions:

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

Now, switch to HelloWorldLayer.m and add the actual method implementations to the end of the file:

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

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

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

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

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

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

Now that all the helper methods needed by our Unit class helper methods are in place, we can add the helper methods to the Unit class. But again following good coding practices, we should add the method definitions to the header file first. So switch to Unit.h and add the following definitions:

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

Finally, switch to Unit.m and add the following helper methods to the end of the file:

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

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

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

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

Now that we have all the helper methods in place to handle unit selection, we'll implement the actual selection code when a touch is detected on the screen. Add the following to the end of ccTouchesBegan: (right before the return statement):

    [theGame unselectUnit];
    [self selectUnit];

Basically, when a touch is detected on a unit, we deselec the currently selected unit (via the HelloWorldLayer which always keeps track of the currently selected unit) and then ask that the touched unit be marked as the new selected unit. This in turn initiates the code to mark the squares available to this unit to move to via the markPossibleAction: method.

Now build and run the project. You should be able to touch any unit and see the surrounding tiles get painted in blue, marking the possible locations that unit can move to.

Of course, when you play around for a little while, you'll notice certain movement anomalies - like the fact that tanks can move over mountains and water, or the fact that all units move the same number of squares. If you recall, we currently have the same code (that for Soldier units) for all the units. Hence, all units show the same movement behaviour.

If you want to implement unique movement capabilities for different unit types, play with the movementRange variable inside each Unit subclass we created. For example, you could set the Helicopter's movement range to 7, to make it capable of moving greater distances in one turn.

Additionally, you can implement movement restrictions (for instance, you probably don't want tanks and cannons moving into water or onto mountains) by modifying the canWalkOverTile: method for the relevant class, similar to the following:

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

Now, if you build and run the game again, you should see that when you select tank or cannon units, the water and mountain tiles don't show up as available for moving to.