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

Moving Units

Now that you're able to select units, the next step is to actually move them.

Since you've already determined where the unit is able to move in the previous step, you could just have the unit move in a straight line to the selected destination. In this case, the logic would be quite easy, but it would look odd in gameplay since your unit might move over a mountain or water to get to the destination even though it is not able to move over that type of terrain. For example, you might see tanks rolling over water.

To solve this problem, you have to figure out a way to move your units tile by tile, finding the shortest, most logical path. Here you'll use the code presented in this pathfinding tutorial as is, with very few changes.

We'll add several methods to handle the movement code but first, we're going to add the method definitions to Unit.h:

-(void)insertOrderedInOpenSteps:(TileData *)tile;
-(int)computeHScoreFromCoord:(CGPoint)fromCoord toCoord:(CGPoint)toCoord;
-(int)costToMoveFromTile:(TileData *)fromTile toAdjacentTile:(TileData *)toTile;
-(void)constructPathAndStartAnimationFromStep:(TileData *)tile;
-(void)popStepAndAnimate;
-(void)doMarkedMovement:(TileData *)targetTileData;

Next, add the implementations to the end of Unit.m:

-(void)insertOrderedInOpenSteps:(TileData *)tile {
    // Compute the step's F score
    int tileFScore = [tile fScore]; 
    int count = [spOpenSteps count];
    // This will be the index at which we will insert the step
    int i = 0; 
    for (; i < count; i++) {
        // If the step's F score is lower or equals to the step at index i
    if (tileFScore <= [[spOpenSteps objectAtIndex:i] fScore]) {
            // Then you found the index at which you have to insert the new step
            // Basically you want the list sorted by F score
            break;
        }
    }
    // Insert the new step at the determined index to preserve the F score ordering
    [spOpenSteps insertObject:tile atIndex:i];
}

-(int)computeHScoreFromCoord:(CGPoint)fromCoord toCoord:(CGPoint)toCoord {
    // Here you use the Manhattan method, which calculates the total number of steps moved horizontally and vertically to reach the
    // final desired step from the current step, ignoring any obstacles that may be in the way
    return abs(toCoord.x - fromCoord.x) + abs(toCoord.y - fromCoord.y);
}

-(int)costToMoveFromTile:(TileData *)fromTile toAdjacentTile:(TileData *)toTile {
    // Because you can't move diagonally and because terrain is just walkable or unwalkable the cost is always the same.
    // But it has to be different if you can move diagonally and/or if there are swamps, hills, etc...
    return 1;
}

-(void)constructPathAndStartAnimationFromStep:(TileData *)tile {
    [movementPath removeAllObjects];
    // Repeat until there are no more parents
    do {
        // Don't add the last step which is the start position (remember you go backward, so the last one is the origin position ;-)
        if (tile.parentTile != nil) {
            // Always insert at index 0 to reverse the path
            [movementPath insertObject:tile atIndex:0]; 
        }
        // Go backward
        tile = tile.parentTile; 
    } while (tile != nil); 
    [self popStepAndAnimate];
}

-(void)popStepAndAnimate {	
    // Check if there remain path steps to go through
    if ([movementPath count] == 0) {
        moving = NO;
        [self unMarkPossibleMovement];
        return;
    }
    // Get the next step to move toward
    TileData *s = [movementPath objectAtIndex:0];
    // Prepare the action and the callback
    id moveAction = [CCMoveTo actionWithDuration:0.4 position:[theGame positionForTileCoord:s.position]];
    // set the method itself as the callback
    id moveCallback = [CCCallFunc actionWithTarget:self selector:@selector(popStepAndAnimate)]; 
    // Remove the step
    [movementPath removeObjectAtIndex:0];
    // Play actions
    [mySprite runAction:[CCSequence actions:moveAction, moveCallback, nil]];
}


-(void)doMarkedMovement:(TileData *)targetTileData {
    if (moving)
        return;
    moving = YES;
    CGPoint startTile = [theGame tileCoordForPosition:mySprite.position];
    tileDataBeforeMovement = [theGame getTileData:startTile];
    [self insertOrderedInOpenSteps:tileDataBeforeMovement];
    do {
        TileData * _currentTile = ((TileData *)[spOpenSteps objectAtIndex:0]);
        CGPoint _currentTileCoord = _currentTile.position;
        [spClosedSteps addObject:_currentTile];
        [spOpenSteps removeObjectAtIndex:0];
        // If the currentStep is the desired tile coordinate, you are done!
        if (CGPointEqualToPoint(_currentTile.position, targetTileData.position)) {
            [self constructPathAndStartAnimationFromStep:_currentTile];
            // Set to nil to release unused memory
            [spOpenSteps removeAllObjects]; 
            // Set to nil to release unused memory
            [spClosedSteps removeAllObjects]; 
            break;
        }
        NSMutableArray * tiles = [theGame getTilesNextToTile:_currentTileCoord];
        for (NSValue * tileValue in tiles) {
            CGPoint tileCoord = [tileValue CGPointValue];
            TileData * _neighbourTile = [theGame getTileData:tileCoord];
            if ([spClosedSteps containsObject:_neighbourTile]) {
                continue;
            }
            if ([theGame otherEnemyUnitInTile:_neighbourTile unitOwner:owner]) {
                // Ignore it
                continue; 
            }
            if (![self canWalkOverTile:_neighbourTile]) {
                // Ignore it
                continue; 
            }
            int moveCost = [self costToMoveFromTile:_currentTile toAdjacentTile:_neighbourTile];
            NSUInteger index = [spOpenSteps indexOfObject:_neighbourTile];
            if (index == NSNotFound) {
                _neighbourTile.parentTile = nil;
                _neighbourTile.parentTile = _currentTile;
                _neighbourTile.gScore = _currentTile.gScore + moveCost;
                _neighbourTile.hScore = [self computeHScoreFromCoord:_neighbourTile.position toCoord:targetTileData.position];
                [self insertOrderedInOpenSteps:_neighbourTile];
            } else {
                // To retrieve the old one (which has its scores already computed ;-)
                _neighbourTile = [spOpenSteps objectAtIndex:index]; 
                // Check to see if the G score for that step is lower if you use the current step to get there
                if ((_currentTile.gScore + moveCost) < _neighbourTile.gScore) {
                    // The G score is equal to the parent G score + the cost to move from the parent to it
                    _neighbourTile.gScore = _currentTile.gScore + moveCost;
                    // Now you can remove it from the list without being afraid that it can't be released
                    [spOpenSteps removeObjectAtIndex:index];
                    // Re-insert it with the function, which is preserving the list ordered by F score
                    [self insertOrderedInOpenSteps:_neighbourTile];
                }
            }
        }
    } while ([spOpenSteps count]>0);
}

This is similar to what you did before to figure out the tiles the unit can move to, but implemented in a slightly different way. The code checks for the potential tiles the unit can pass through, and if they are on the shortest path and aren't occupied by enemy units, they are added to the final path. Once that path has been constructed, the unit sprite is moved through the tiles in the path.

Now all that remains is for us to make use of the above methods to move a unit when the user touches any part of the screen which does not contain a unit that can be moved. Switch to HelloWorldLayer.m and add the following method to the end of the file:

-(void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
	for (UITouch *touch in touches) {
		// Get the location of the touch
		CGPoint location = [touch locationInView: [touch view]];
		// Convert the touch location to OpenGL coordinates
		location = [[CCDirector sharedDirector] convertToGL: location];
		// Get the tile data for the tile at touched position
		TileData * td = [self getTileData:[self tileCoordForPosition:location]];
		// Move to the tile if we can move there
        if ((td.selectedForMovement && ![self otherUnitInTile:td]) || ([self otherUnitInTile:td] == selectedUnit)) {
            [selectedUnit doMarkedMovement:td];
        }
        // You'll also handle attacks here, later.
	}
}

Build and run the game now. You should be able to select a unit, see where it can move according to the marked tiles, and then touch one of the marked tiles and have the unit move to that place. Do this as many times as you want and see what happens when you're near different tile types or enemy units.