banner



how to design a strategy game

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 series for Nintendo DS being one of my favorites. Simplified versions of turn-based strategy games, such as Hero Academy, have lately become popular.

Some of the things you'll learn include:

  • How to load a map with different types of terrains.
  • How to create different types of units with unique properties.
  • How to make them move and attack using a couple of AI pathfinding methods.

By the end of the tutorial you will have a basic, playable version of an Advance Wars-like game that can be played with a friend on the same device.

To make the most of this tutorial, you'll need some Cocos2D experience and a beginning understanding of AI. You will also use Tiled for your maps, so a good understanding of this program and of tile maps in general is required.

In this first part of the two-part series, you'll set everything up to enable you to load units for both teams and move them around.

Are you ready? Commence mission!

Getting Started: Game Preview

For those who haven't played any of the Advance Wars games, the objective of most of the missions is to either eliminate all of the other player's units, or to capture their headquarters ("HQ") by moving a soldier over it. (It's actually a bit more complicated than that, but we'll simplify it for the sake of this tutorial.)

Here's a screenshot of how the game will look after you're finished:

As you can see in the image, each player controls their own units. Player 1 controls the red units and Player 2 the blue ones. As the tutorial progresses, you'll create four types of units, each with different properties, such as their range of movement and damage capabilities.

Each player will also have an HQ they must protect. You'll create the HQ buildings in such a way that it will be easy to extend the game to create other types of buildings, like factories, airports, etc.

Finally, in the above image you can also see different types of terrain: grass, mountains, water and forest. Each terrain type allows only certain types of movement from the units. For example, soldiers can walk over water (with reduced movement), but tanks cannot. Helicopters can fly anywhere.

While it's not visible in the image, the final version of the game will have a bar at the top of the screen indicating whose turn it is, as well as an End Turn button to pass the turn to the other player. These will be added in the second part of the tutorial.

Getting Started: Assembling the Resources

To get started, download the tutorial starter project and extract it to your preferred location. This will be the starting point containing the basic Cocos2D project and most of the assets, including the free game art pack by Vicki and the tilemap used to create the map you see above.

I made this project by creating a new project from the Cocos2D basic template, which provides you with a working project that has a HelloWorldLayer with a label in the middle of the screen. Of course, we'll not be using the HelloWorldLayer but will be creating our own game UI but this gives us a basic starter Cocos2D project that we can be certain works correctly.

In fact, it might be a good idea for you to open the project in Xcode at this point and compile it and run it to make sure that everything is working. Do note though that we've removed the actual "Hello World" text from the HelloWorldLayer and so, you'd only get a black screen (after the initial Cocos2D splash) if the project runs correctly. But if the projects does compile properly, then you're set!

The project structure is pretty straightforward. Inside the TurnWars folder, you'll find:

  • All the classes used in the game
  • The libs folder containing all the Cocos2D files
  • The Resources folder containing all the graphics

You'll find that the tilemap is already done. Since this isn't a Tiled tutorial, I won't go over the details of how to create it, but I will mention things that are important for the project to work. For a very complete tutorial on how to create tilemaps using Tiled, I recommend this tutorial created by Ray.

Loading the Terrain

Time to start coding! The first step is to load the terrain layer contained within the tilemap. To get a better idea of how this works, open StageMap.tmx, which is inside Resources/Tiles, in Tiled, to see how it looks. (And if you don't have have Tiled, you can download it from here.)

Viewing the Tile Map in Tiled

When you do this, you'll notice the four types of terrain I said you would create. (Actually, there are a few more tiles than that, but the extras are just different versions of the water terrain, created for cosmetic reasons.)

Inspect the tiles (by right-clicking them in the tileset window and selecting "Tile Properties…") and you will see two properties called MovementCost and TileType. I created these properties so that we can identify specific attributes for each tile (such as the type of tile) in our code later.

Tile Properties for Movement Cost

The StageMap.tmx file contains just five layers: the background layer, which contains these terrain tiles; two Units layers, for placing each player's units; and two Buildings layers, for placing each player's buildings.

Layers in Tile Map

Now that you have an understanding of how the tilemap is structured, switch over to the Xcode project for TurnWars.

You need to load the StageMap.tmx tilemap inside your HelloWorldLayer, which will be the main layer of the game.

To do this, open HelloWorldLayer.m and replace the init method with the following:

-(id)init {     if ((self=[super init])) {         self.isTouchEnabled = YES;         [self createTileMap];     }     return self; }        

All we're doing in the above code is to enable touch handling for our layer, HelloWorldLayer, and then calling the createTileMap method. But before adding the new method, we need to set up some instance variables to be used in the method. So add the following to HelloWorldLayer.h (within the @interface section's curly brackets):

          CCTMXTiledMap *tileMap;     CCTMXLayer *bgLayer;     CCTMXLayer *objectLayer;      NSMutableArray * tileDataArray;        

In the above code, we've defined several instance variables to point to the tile map, several of its layers, and an array to hold data about individual tiles on the background tile layer.

Of the above instance variable, tileDataArray needs to be set up as a property since we'll need it later on. So add the following property declaration:

@property (nonatomic, assign) NSMutableArray *tileDataArray;        

Quickly switch over to HelloWorldLayer.m and synthesise the tileDataArray property (below the @implementation line):

@synthesize tileDataArray;        

While not strictly necessary, we should also add a method definition for createTileMap at this point so that we follow good coding practices. So switch back to HelloWorldLayer.h and add the following above the @end line:

-(void)createTileMap;        

Now we get to the createTileMap method implementation, right? Well, we can certainly do that, but in order to ensure that our code will compile properly, we have do one more thing first – we have to add the TileData class. What's the TileData class, you ask? TileData holds the information from each tile that you'll need throughout the project – this is the information stored in the tile properties that we looked at earlier. The TileData class needs to keep track of:

  • Type: the tile's type.
  • Movement Cost: how much it costs a unit to move through a tile.
  • Position: the tile's position, which you'll use later to determine a unit's movement options.

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

Replace the contents of TileData.h with the following:

#import <Foundation/Foundation.h> #import "cocos2d.h" #import "HelloWorldLayer.h"  @class HelloWorldLayer;  @interface TileData : CCNode {     HelloWorldLayer * theGame;     BOOL selectedForMovement;     BOOL selectedForAttack;     int movementCost;     CGPoint position;     TileData * parentTile;     int hScore;     int gScore;     int fScore;     NSString * tileType; }  @property (nonatomic,readwrite) CGPoint position; @property (nonatomic,assign) TileData * parentTile; @property (nonatomic,readwrite) int movementCost; @property (nonatomic,readwrite) BOOL selectedForAttack; @property (nonatomic,readwrite) BOOL selectedForMovement; @property (nonatomic,readwrite) int hScore; @property (nonatomic,readwrite) int gScore; @property (nonatomic,assign) NSString * tileType;  +(id)nodeWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType; -(id)initWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType; -(int)getGScore; -(int)getGScoreForAttack; -(int)fScore;  @end        

Then, replace TileData.m with the following:

#import "TileData.h"  @implementation TileData  @synthesize parentTile,position,selectedForAttack,selectedForMovement,gScore,hScore,movementCost,tileType;  +(id)nodeWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType { 	return [[[self alloc] initWithTheGame:_game movementCost:_movementCost position:_position tileType:_tileType] autorelease]; }  -(id)initWithTheGame:(HelloWorldLayer *)_game movementCost:(int)_movementCost position:(CGPoint)_position tileType:(NSString *)_tileType { 	if ((self=[super init])) { 		theGame = _game;         selectedForMovement = NO;         movementCost = _movementCost;         tileType = _tileType;         position = _position;         parentTile = nil;         [theGame addChild:self]; 	} 	return self; }  -(int)getGScore {     int parentCost = 0;     if (parentTile) {         parentCost = [parentTile getGScore];     }     return movementCost + parentCost;      }  -(int)getGScoreForAttack {     int parentCost = 0;     if(parentTile) {         parentCost = [parentTile getGScoreForAttack];     }     return 1 + parentCost; }  -(int)fScore { 	return self.gScore + self.hScore; }  -(NSString *)description { 	return [NSString stringWithFormat:@"%@  pos=[%.0f;%.0f]  g=%d  h=%d  f=%d", [super description], self.position.x, self.position.y, self.gScore, self.hScore, [self fScore]]; }  @end        

The above code is fairly straightforward since the TileData class, as the name implies, is mostly a data receptacle. You initialize it with various values for the class properties and have a couple of methods which calculate various scores based on the properties for the current tile and its parent.

Now that we have a TileData class, we need to add an import statement to use it in HelloWorldLayer. In HelloWorldLayer.h add the following lines at the top of the file (below the existing #import line):

#import "TileData.h"  @class TileData;        

At last we get to the createTileMap method itself! Add it to the end of HelloWorldLayer.m (before the @end):

-(void)createTileMap {     // 1 - Create the map     tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"StageMap.tmx"];             [self addChild:tileMap];     // 2 - Get the background layer     bgLayer = [tileMap layerNamed:@"Background"];     // 3 - Get information for each tile in background layer     tileDataArray = [[NSMutableArray alloc] initWithCapacity:5];     for(int i = 0; i< tileMap.mapSize.height;i++) {         for(int j = 0; j< tileMap.mapSize.width;j++) {             int movementCost = 1;             NSString * tileType = nil;             int tileGid=[bgLayer tileGIDAt:ccp(j,i)];             if (tileGid) {                 NSDictionary *properties = [tileMap propertiesForGID:tileGid];                 if (properties) {                     movementCost = [[properties valueForKey:@"MovementCost"] intValue];                     tileType = [properties valueForKey:@"TileType"];                 }             }             TileData * tData = [TileData nodeWithTheGame:self movementCost:movementCost position:ccp(j,i) tileType:tileType];             [tileDataArray addObject:tData];         }      } }        

In the code above:

  1. You first load the tilemap included in the project.
  2. Then, you retrieve the layer named "Background", which contains the terrain tiles for the map.
  3. Next, you create an NSMutableArray to contain the information of every tile in the map. That array is populated with TileData objects created with properties gathered from each tile in the tile layer, the same properties you looked at in Tiled earlier.

Finally, add the following line to the beginning of dealloc to release the NSMutableArray you create in the code above:

[tileDataArray release];        

Go ahead and run the project now. You should see the tiles you saw before in Tiled appear on the screen.

Creating the Units

Your game will have four different types of units: Soldier, Tank, Cannon, and Helicopter. Each unit type will have its own movement range, and a set of strengths and weaknesses when combatting other units. Some types will be restricted from moving through certain terrains.

As you saw earlier in Tiled, the map contains two layers for player units, one for each player's units. Each player has six units, represented by gray object tiles. If you check a unit's properties (right-click and select "Object Properties...") you'll be able to see that they all have a "Type" property. This will help you determine which units to load, what kind of units they are and where to position them.

Object properties in Tiled

But before you create the Unit class to represent player units, we need to add a a macro to detect whether the device has a retina display or not, a couple of constants, and a helper enum for detecting touches. Open GameConfig.h and add the following lines of code at the bottom of the file:

#define IS_HD ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] == YES && [[UIScreen mainScreen] scale] == 2.0f)  #define TILE_HEIGHT 32 #define TILE_HEIGHT_HD 64  typedef enum tagState { 	kStateGrabbed, 	kStateUngrabbed } touchState;        

Next, we create the Unit class that will hold most of the information for your different units. Later, you'll create other classes for each unit type that will inherit from the Unit class and define unique properties specific to each type of unit.

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

Replace Unit.h with the following:

#import <Foundation/Foundation.h> #import "cocos2d.h" #import "HelloWorldLayer.h" #import "GameConfig.h" #import "TileData.h"  @interface Unit : CCNode <CCTargetedTouchDelegate> {     HelloWorldLayer * theGame;     CCSprite * mySprite;     touchState state;     int owner;     BOOL hasRangedWeapon;     BOOL moving;     int movementRange;     int attackRange;     TileData * tileDataBeforeMovement;     int hp;     CCLabelBMFont * hpLabel; }  @property (nonatomic,assign)CCSprite * mySprite; @property (nonatomic,readwrite) int owner; @property (nonatomic,readwrite) BOOL hasRangedWeapon;  +(id)nodeWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner; -(void)createSprite:(NSMutableDictionary *)tileDict;  @end        

The above simply defines the Unit class as a sub-class of CCNode. It contains properties which identify basic attributes of the unit and also has a few additional instance variables which allow the unit to refer back to the main game layer, HelloWorldLayer, or detect whether the unit is moving or whether the unit has a weapon.

But before you can add the code for the Unit class implementation, we need a few helper methods in place which are useful in handling sprite dimensions for retina display and allow you to set the position of units. We will add these helper methods to the HelloWorldLayer class since we need access to some layer properties, such as the size of the layer, that we would not have from within the Unit class.

Add the definitions for the helper methods to HelloWorldLayer.h (above the @end) as follows:

-(int)spriteScale; -(int)getTileHeightForRetina; -(CGPoint)tileCoordForPosition:(CGPoint)position; -(CGPoint)positionForTileCoord:(CGPoint)position; -(NSMutableArray *)getTilesNextToTile:(CGPoint)tileCoord; -(TileData *)getTileData:(CGPoint)tileCoord;        

While we're at it, let's also add a couple of instance variables to HelloWorldLayer to keep track of the units for each player and the current turn number. Add the following to HelloWorldLayer.h below the other instance variables:

NSMutableArray *p1Units; NSMutableArray *p2Units; int playerTurn;        

We'll need to access the above instance variables as properties, so add the property definitions as follows:

@property (nonatomic, assign) NSMutableArray *p1Units; @property (nonatomic, assign) NSMutableArray *p2Units; @property (nonatomic, readwrite) int playerTurn;        

Now switch to HelloWorldLayer.m and synthesise those properties:

@synthesize p1Units; @synthesize p2Units; @synthesize playerTurn;        

We will also be referring to the TILE_HEIGHT and TILE_HEIGHT_HD constants that we defined earlier when we implement our helper methods. So import GameConfig.h at the top of HelloWorldLayer.m:

#import "GameConfig.h"        

Now add the implementations for the helper methods you defined above to the end of HelloWorldLayer.m:

// Get the scale for a sprite - 1 for normal display, 2 for retina -(int)spriteScale {     if (IS_HD)         return 2;     else         return 1; }  // Get the height for a tile based on the display type (retina or SD) -(int)getTileHeightForRetina {     if (IS_HD)         return TILE_HEIGHT_HD;      else          return TILE_HEIGHT; }  // Return tile coordinates (in rows and columns) for a given position -(CGPoint)tileCoordForPosition:(CGPoint)position {     CGSize tileSize = CGSizeMake(tileMap.tileSize.width,tileMap.tileSize.height);     if (IS_HD) {         tileSize = CGSizeMake(tileMap.tileSize.width/2,tileMap.tileSize.height/2);     }     int x = position.x / tileSize.width;     int y = ((tileMap.mapSize.height * tileSize.height) - position.y) / tileSize.height;     return ccp(x, y); }  // Return the position for a tile based on its row and column -(CGPoint)positionForTileCoord:(CGPoint)position {     CGSize tileSize = CGSizeMake(tileMap.tileSize.width,tileMap.tileSize.height);     if (IS_HD) {         tileSize = CGSizeMake(tileMap.tileSize.width/2,tileMap.tileSize.height/2);     }     int x = position.x * tileSize.width + tileSize.width/2;     int y = (tileMap.mapSize.height - position.y) * tileSize.height - tileSize.height/2;     return ccp(x, y); }  // Get the surrounding tiles (above, below, to the left, and right) of a given tile based on its row and column -(NSMutableArray *)getTilesNextToTile:(CGPoint)tileCoord {     NSMutableArray * tiles = [NSMutableArray arrayWithCapacity:4];      if (tileCoord.y+1<tileMap.mapSize.height)         [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x,tileCoord.y+1)]];     if (tileCoord.x+1<tileMap.mapSize.width)         [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x+1,tileCoord.y)]];     if (tileCoord.y-1>=0)         [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x,tileCoord.y-1)]];     if (tileCoord.x-1>=0)         [tiles addObject:[NSValue valueWithCGPoint:ccp(tileCoord.x-1,tileCoord.y)]];     return tiles; }  // Get the TileData for a tile at a given position -(TileData *)getTileData:(CGPoint)tileCoord {     for (TileData * td in tileDataArray) {         if (CGPointEqualToPoint(td.position, tileCoord)) {             return td;         }     }     return nil; }        

Finally, we can add the code for the Unit class implementation :] Replace the contents of Unit.m with the following:

#import "Unit.h"  #define kACTION_MOVEMENT 0 #define kACTION_ATTACK 1  @implementation Unit  @synthesize mySprite,owner,hasRangedWeapon;  +(id)nodeWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner {     // Dummy method - implemented in sub-classes     return nil; }  -(id)init {     if ((self=[super init])) {         state = kStateUngrabbed;         hp = 10;     }     return self; }  // Create the sprite and HP label for each unit -(void)createSprite:(NSMutableDictionary *)tileDict {     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];     int heightInTiles = height/[theGame getTileHeightForRetina];     x += width/2;     y += (heightInTiles * [theGame getTileHeightForRetina]/(2*[theGame spriteScale]));     mySprite = [CCSprite spriteWithFile:[NSString stringWithFormat:@"%@_P%d.png",[tileDict valueForKey:@"Type"],owner]];     [self addChild:mySprite];     mySprite.userData = self;     mySprite.position = ccp(x,y);     hpLabel = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"%d",hp] fntFile:@"Font_dark_size12.fnt"];     [mySprite addChild:hpLabel];     [hpLabel setPosition:ccp([mySprite boundingBox].size.width-[hpLabel boundingBox].size.width/2,[hpLabel boundingBox].size.height/2)]; }  // Can the unit walk over the given tile? -(BOOL)canWalkOverTile:(TileData *)td {     return YES; }  // Update the HP value display -(void)updateHpLabel {     [hpLabel setString:[NSString stringWithFormat:@"%d",hp]];     [hpLabel setPosition:ccp([mySprite boundingBox].size.width-[hpLabel boundingBox].size.width/2,[hpLabel boundingBox].size.height/2)]; }  -(void)onEnter {     [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];     [super onEnter]; }  -(void)onExit {	     [[CCTouchDispatcher sharedDispatcher] removeDelegate:self];     [super onExit]; }	  // Was this unit below the point that was touched? -(BOOL)containsTouchLocation:(UITouch *)touch {     if (CGRectContainsPoint([mySprite boundingBox], [self convertTouchToNodeSpaceAR:touch])) {         return YES;     }     return NO; }  // Handle touches -(BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {     if (state != kStateUngrabbed)          return NO;     if (![self containsTouchLocation:touch])          return NO;     state = kStateGrabbed;     return YES; }  -(void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event {     state = kStateUngrabbed; }  -(void)dealloc {     [super dealloc]; }  @end        

The Unit class is pretty basic right now. init initializes the class with its max hit points (the same for every unit) and sets the initial state for the unit.

Then you have createSprite which will handle the information received from parsing the tilemap. createSprite receives the unit's position, dimensions, unit type, etc., and creates a sprite to display the unit based on its type. Also, the code adds a label on top of the sprite to display the unit's remaining HP.

The rest of the code mostly deals with detecting touches on the units and is fairly self-explanatory. You'll use this later to be able to select each unit for moving.

Now that you have a Unit class which will act as the base for all player units, you should create four different classes that will inherit from Unit. These four classes will shape your four different unit types. In this tutorial, the code for the four classes is pretty similar, but as you keep working on your game, you may need to add special cases for each unit/scenario.

Let's begin with the Soldier unit. Add Unit_Soldier.h and Unit_Soldier.m files to your project by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Unit_Soldier, and make it a subclass of Unit.

Replace the current code in Unit_Soldier.h with:

#import <Foundation/Foundation.h> #import "cocos2d.h" #import "Unit.h"  @interface Unit_Soldier : Unit {      }  -(id)initWithTheGame:(HelloWorldLayer *)_game tileDict:(NSMutableDictionary *)tileDict owner:(int)_owner; -(BOOL)canWalkOverTile:(TileData *)td;  @end        

Then, replace Unit_Soldier.m with:

#import "Unit_Soldier.h"  @implementation Unit_Soldier  +(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;         movementRange = 3;         attackRange = 1;         [self createSprite:tileDict];         [theGame addChild:self z:3];     }     return self; }  -(BOOL)canWalkOverTile:(TileData *)td {     return YES; }  @end        

This class is pretty simple. Its movementRange (the number of tiles it can move per turn) and the attackRange (the distance it has to be from an enemy unit to be able to attack it) are set when the unit is initialized.

You also have a method, canWalkOverTile, which you'll use to determine if the unit can pass through certain terrains. For example, the Tank unit can't move over a water tile and in the Tank class, we'd need to check whether the specified tile is a water tile. In the case of the Solider, it can walk over all terrain and so simply returns YES without any further checks.

In order for your project to work, you must have code in place for all four unit classes. For the time being, create implementations for the other classes by simply copying the code for the Soldier class. Name the new classes Unit_Tank, Unit_Cannon and Unit_Helicopter. Do note that if you copy and pate the code from above, you would need to change all instances of Unit_Soldier in the code to the appropriate value.

Once you have all the unit classes in place, it's time to set up the units in our game! First, import the main Unit class header at the top of HelloWorldLayer.m:

#import "Unit.h"        

Next, in order to load our units, we need a new helper method. Add the following method at the end of HelloWorldLayer.m:

-(void)loadUnits:(int)player {     // 1 - Retrieve the layer based on the player number     CCTMXObjectGroup * unitsObjectGroup = [tileMap objectGroupNamed:[NSString stringWithFormat:@"Units_P%d",player]];     // 2 - Set the player array     NSMutableArray * units = nil;     if (player ==1)         units = p1Units;     if (player ==2)         units = p2Units;     // 3 - Load units into player array based on the objects on the layer     for (NSMutableDictionary * unitDict in [unitsObjectGroup objects]) {         NSMutableDictionary * d = [NSMutableDictionary dictionaryWithDictionary:unitDict];         NSString * unitType = [d objectForKey:@"Type"];         NSString *classNameStr = [NSString stringWithFormat:@"Unit_%@",unitType];         Class theClass = NSClassFromString(classNameStr);         Unit * unit = [theClass nodeWithTheGame:self tileDict:d owner:player];         [units addObject:unit];     }  }        

The above code retrieves the correct unit layer (either player 1 or player 2 in the tilemap), and for each object found, gets its information and instantiates a Unit object according to the object type. The only complicated bit of code is section #3 where we get the unit type from the object and based on the type, create an instance of the correct Unit sub-class (Unit_Soldier, Unit_Tank, Unit_Cannon, or Unit_Helicopter). Since all of those classes contain the nodeWithTheGame:tileDict:owner: method, the call to that method creates an object from the correct Unit sub-class.

Now that we have all the pieces in place, we can actually load the units into our game. We start off by initializing the arrays for each player's units and loading them from the tilemap in init in HelloWorldLayer.m after the call to createTileMap:

// Load units p1Units = [[NSMutableArray alloc] initWithCapacity:10]; p2Units = [[NSMutableArray alloc] initWithCapacity:10]; [self loadUnits:1]; [self loadUnits:2];        

And of course, we need to clean up the arrays created above. We do that in dealloc:

[p1Units release]; [p2Units release];        

That's it! Now you should be able to compile the project. Go ahead and give it a try! When you load the game you should now see each player's units on screen as they are laid out in the tilemap.

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.

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.

Where to Go From Here?

That's it for Part 1 of this tutorial! Now that you have the units in place and can move them around the battlefield, you're ready for Part 2, where you'll learn how to make the units attack… and more!

An example project with all of the code from this part of the tutorial can be downloaded here.

Any questions or feedback so far? Let me know in the forums.


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.

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!

how to design a strategy game

Source: https://www.raywenderlich.com/2925-how-to-make-a-turn-based-strategy-game-part-1

Posted by: perrylitsee.blogspot.com

Related Posts

0 Response to "how to design a strategy game"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel