How To Make a Tower Defense Game Tutorial
This Tower Defense Game Tutorial shows you how to make a complete tower defense game with shooting towers and multiple waves of enemies. Now fully updated for Cocos2D 2.X! By .
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Tower Politics: Enemies, Waves and Waypoints
Before creating the enemies, let’s “pave the road” for them. The enemies will follow a series of waypoints, which are simply interconnected points that define a path the enemies use to move around your world. Enemies will appear at the first waypoint, search for the next waypoint in the list, move to that spot, and repeat, until they reach the last waypoint in the list — your base! If the baddies reach your base, you’ll suffer damage.
Create the waypoint list by creating a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Waypoint, and make it a subclass of CCNode.
Replace the contents of Waypoint.h with the following:
#import "cocos2d.h"
#import "HelloWorldLayer.h"
@interface Waypoint: CCNode {
HelloWorldLayer *theGame;
}
@property (nonatomic,readwrite) CGPoint myPosition;
@property (nonatomic,assign) Waypoint *nextWaypoint;
+(id)nodeWithTheGame:(HelloWorldLayer*)_game location:(CGPoint)location;
-(id)initWithTheGame:(HelloWorldLayer *)_game location:(CGPoint)location;
@end
Now, replace Waypoint.m with the following code:
#import "Waypoint.h"
@implementation Waypoint
@synthesize myPosition, nextWaypoint;
+(id)nodeWithTheGame:(HelloWorldLayer*)_game location:(CGPoint)location
{
return [[self alloc] initWithTheGame:_game location:location];
}
-(id)initWithTheGame:(HelloWorldLayer *)_game location:(CGPoint)location
{
if( (self=[super init])) {
theGame = _game;
[self setPosition:CGPointZero];
myPosition = location;
[theGame addChild:self];
}
return self;
}
-(void)draw
{
ccDrawColor4B(0, 255, 2, 255);
ccDrawCircle(myPosition, 6, 360, 30, false);
ccDrawCircle(myPosition, 2, 360, 30, false);
if(nextWaypoint)
ccDrawLine(myPosition, nextWaypoint.myPosition);
[super draw];
}
@end
First, the code initializes a waypoint object by passing in the HelloWorldLayer object by reference and a CGPoint, which is the position of the waypoint.
Each waypoint contains a reference to the next waypoint; this creates a linked list of waypoints (did you pay attention in data structures class?). Each waypoint “knows” the next waypoint in the list. This way, you can guide the enemies to their final destination by following the chain of waypoints. The enemies never retreat in this world; they’re little kamikaze warriors.
Finally, the draw method shows where the waypoint is placed, and draws a line connecting it with the next one, for debug purposes only. A production version of the game wouldn’t draw the enemies’ path – that would make it too easy for the player!
Create the list of waypoints. Open HelloWorldLayer.h and add the following property:
@property (nonatomic,strong) NSMutableArray *waypoints;
Next, add the following code to HelloWorldLayer.m:
//At the top of the file:
#import "Waypoint.h"
// Add synthesise
@synthesize waypoints;
//Add this method
-(void)addWaypoints
{
waypoints = [[NSMutableArray alloc] init];
Waypoint * waypoint1 = [Waypoint nodeWithTheGame:self location:ccp(420,35)];
[waypoints addObject:waypoint1];
Waypoint * waypoint2 = [Waypoint nodeWithTheGame:self location:ccp(35,35)];
[waypoints addObject:waypoint2];
waypoint2.nextWaypoint =waypoint1;
Waypoint * waypoint3 = [Waypoint nodeWithTheGame:self location:ccp(35,130)];
[waypoints addObject:waypoint3];
waypoint3.nextWaypoint =waypoint2;
Waypoint * waypoint4 = [Waypoint nodeWithTheGame:self location:ccp(445,130)];
[waypoints addObject:waypoint4];
waypoint4.nextWaypoint =waypoint3;
Waypoint * waypoint5 = [Waypoint nodeWithTheGame:self location:ccp(445,220)];
[waypoints addObject:waypoint5];
waypoint5.nextWaypoint =waypoint4;
Waypoint * waypoint6 = [Waypoint nodeWithTheGame:self location:ccp(-40,220)];
[waypoints addObject:waypoint6];
waypoint6.nextWaypoint =waypoint5;
}
// At the end of init:
// 4 - Add waypoints
[self addWaypoints];
Compile and run the game. You’ll see the following:
There are six waypoints on the map; this is the path the enemies will follow. Before you let the baddies loose in your game, you need to add a helper method.
First, add the method definition to the header file so that other classes can access this method without compiler warnings.
Open HelloWorldLayer.h and add the following method definitions before the @end:
-(BOOL)circle:(CGPoint)circlePoint withRadius:(float)radius
collisionWithCircle:(CGPoint)circlePointTwo collisionCircleRadius:(float)radiusTwo;
void ccFillPoly(CGPoint *poli, int points, BOOL closePolygon);
-(void) enemyGotKilled;
-(void) getHpDamage;
Next, open HelloWorldLayer.m and add the following method (before the @end):
-(BOOL)circle:(CGPoint) circlePoint withRadius:(float) radius
collisionWithCircle:(CGPoint) circlePointTwo collisionCircleRadius:(float) radiusTwo {
float xdif = circlePoint.x - circlePointTwo.x;
float ydif = circlePoint.y - circlePointTwo.y;
float distance = sqrt(xdif*xdif+ydif*ydif);
if(distance <= radius+radiusTwo)
return YES;
return NO;
}
The collisionWithCircle method will help us determine when two circles collide, or intersect. This will help determine if an enemy reached a waypoint, along with detecting enemies that are within a tower’s attack range.
Time to add the baddies to the mix!
Open HelloWorldLayer.h and add the following code:
// Add these instance variables
int wave;
CCLabelBMFont *ui_wave_lbl;
// Add the following property to the properties section
@property (nonatomic,strong) NSMutableArray *enemies;
Make the following change in HelloWorldLayer.m:
// Synthesize enemies
@synthesize enemies;
Time to create a class that will hold the enemy’s information and manage how they move on screen. Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class Enemy, and make it a subclass of CCNode.
Replace the contents of Enemy.h with the following:
#import "cocos2d.h"
#import "HelloWorldLayer.h"
@class HelloWorldLayer, Waypoint, Tower;
@interface Enemy: CCNode {
CGPoint myPosition;
int maxHp;
int currentHp;
float walkingSpeed;
Waypoint *destinationWaypoint;
BOOL active;
}
@property (nonatomic,assign) HelloWorldLayer *theGame;
@property (nonatomic,assign) CCSprite *mySprite;
+(id)nodeWithTheGame:(HelloWorldLayer*)_game;
-(id)initWithTheGame:(HelloWorldLayer *)_game;
-(void)doActivate;
-(void)getRemoved;
@end
Now, replace Enemy.m with the following code:
#import "Enemy.h"
#import "Tower.h"
#import "Waypoint.h"
#define HEALTH_BAR_WIDTH 20
#define HEALTH_BAR_ORIGIN -10
@implementation Enemy
@synthesize mySprite, theGame;
+(id)nodeWithTheGame:(HelloWorldLayer*)_game {
return [[self alloc] initWithTheGame:_game];
}
-(id)initWithTheGame:(HelloWorldLayer *)_game {
if ((self=[super init])) {
theGame = _game;
maxHp = 40;
currentHp = maxHp;
active = NO;
walkingSpeed = 0.5;
mySprite = [CCSprite spriteWithFile:@"enemy.png"];
[self addChild:mySprite];
Waypoint * waypoint = (Waypoint *)[theGame.waypoints
objectAtIndex:([theGame.waypoints count]-1)];
destinationWaypoint = waypoint.nextWaypoint;
CGPoint pos = waypoint.myPosition;
myPosition = pos;
[mySprite setPosition:pos];
[theGame addChild:self];
[self scheduleUpdate];
}
return self;
}
-(void)doActivate
{
active = YES;
}
-(void)update:(ccTime)dt
{
if(!active)return;
if([theGame circle:myPosition withRadius:1 collisionWithCircle:destinationWaypoint.myPosition
collisionCircleRadius:1])
{
if(destinationWaypoint.nextWaypoint)
{
destinationWaypoint = destinationWaypoint.nextWaypoint;
}else
{
//Reached the end of the road. Damage the player
[theGame getHpDamage];
[self getRemoved];
}
}
CGPoint targetPoint = destinationWaypoint.myPosition;
float movementSpeed = walkingSpeed;
CGPoint normalized = ccpNormalize(ccp(targetPoint.x-myPosition.x,targetPoint.y-myPosition.y));
mySprite.rotation = CC_RADIANS_TO_DEGREES(atan2(normalized.y,-normalized.x));
myPosition = ccp(myPosition.x+normalized.x * movementSpeed,
myPosition.y+normalized.y * movementSpeed);
[mySprite setPosition:myPosition];
}
-(void)getRemoved
{
[self.parent removeChild:self cleanup:YES];
[theGame.enemies removeObject:self];
//Notify the game that we killed an enemy so we can check if we can send another wave
[theGame enemyGotKilled];
}
-(void)draw
{
ccDrawSolidRect(ccp(myPosition.x+HEALTH_BAR_ORIGIN,
myPosition.y+16),
ccp(myPosition.x+HEALTH_BAR_ORIGIN+HEALTH_BAR_WIDTH,
myPosition.y+14),
ccc4f(1.0, 0, 0, 1.0));
ccDrawSolidRect(ccp(myPosition.x+HEALTH_BAR_ORIGIN,
myPosition.y+16),
ccp(myPosition.x+HEALTH_BAR_ORIGIN + (float)(currentHp * HEALTH_BAR_WIDTH)/maxHp,
myPosition.y+14),
ccc4f(0, 1.0, 0, 1.0));
}
@end
That’s a fairly substantial code block - but it breaks down quite nicely. First, the enemy is initialized by passing a reference to the HelloWorldLayer object to it. Inside the init method, a few important variables are set:
- maxHP: Defines how many hits this enemy can take. (Tough guy, eh?)
- walkingSpeed: Defines how fast the enemy moves.
- mySprite: Stores the visual representation of the enemy.
- destinationWaypoint: Stores a reference to the next waypoint.
The update method is where the magic happens. This is called every frame, and it first checks to see if it's reached the destination waypoint by using the collisionWithCircle method you wrote earlier. If so, it advances to the next waypoint - unless the enemy has reached the end, in which case the player is damaged.
Then, it moves the sprite along by moving in a straight line to the destination waypoint, according to the walking speed. It does this by the following algorithm:
- Figure out a vector pointing from the current position to the target position, and then make it length of 1 so it's easy to work with (the normalized variable).
- Multiply the normalized vector by the movement speed to get an amount to move this frame. Add it to the current position to get the new position.
Finally, the draw method has a simple implementation of a health bar above the sprite. It first draws a red background, and then covers it with green according to the current HP of the enemy.
Now that the Enemy class is done, you can show them on the screen!
Switch to HelloWorldLayer.m and make the following changes:
//At the top of the file:
#import "Enemy.h"
//Add the following methods:
-(BOOL)loadWave {
NSString* plistPath = [[NSBundle mainBundle] pathForResource:@"Waves" ofType:@"plist"];
NSArray * waveData = [NSArray arrayWithContentsOfFile:plistPath];
if(wave >= [waveData count])
{
return NO;
}
NSArray * currentWaveData =[NSArray arrayWithArray:[waveData objectAtIndex:wave]];
for(NSDictionary * enemyData in currentWaveData)
{
Enemy * enemy = [Enemy nodeWithTheGame:self];
[enemies addObject:enemy];
[enemy schedule:@selector(doActivate)
interval:[[enemyData objectForKey:@"spawnTime"]floatValue]];
}
wave++;
[ui_wave_lbl setString:[NSString stringWithFormat:@"WAVE: %d",wave]];
return YES;
}
-(void)enemyGotKilled {
if ([enemies count]<=0) //If there are no more enemies.
{
if(![self loadWave])
{
NSLog(@"You win!");
[[CCDirector sharedDirector] replaceScene:[CCTransitionSplitCols
transitionWithDuration:1
scene:[HelloWorldLayer scene]]];
}
}
}
// Add the following to the end of the init method:
// 5 - Add enemies
enemies = [[NSMutableArray alloc] init];
[self loadWave];
// 6 - Create wave label
ui_wave_lbl = [CCLabelBMFont labelWithString:[NSString stringWithFormat:@"WAVE: %d",wave]
fntFile:@"font_red_14.fnt"];
[self addChild:ui_wave_lbl z:10];
[ui_wave_lbl setPosition:ccp(400,winSize.height-12)];
[ui_wave_lbl setAnchorPoint:ccp(0,0.5)];
All those code changes above deserve some explanation. The most important part is in the loadWave method; it reads the data from Waves.plist.
Take a look at the Waves.plist file and you will notice it contains three arrays. Each of these arrays represents a wave, which is simply a group of enemies that arrive together. The first array contains six dictionaries. Each of these dictionaries defines an enemy. In this tutorial, the dictionary stores only the time that the enemy should appear, but this dictionary could also be used to define the type of enemy or any other special property that differentiates your enemies.
The loadWave method checks for the next wave that should appear, creates the corresponding enemies based on the wave information, and schedules them to appear on screen accordingly.
The method enemyGotKilled checks the number of enemies present on screen, and if there are none, sends in the next wave. Later on, this same method will be used to determine if the player has won the game.
Compile and run the game now. Aha! The baddies are marching toward your precious base! (Betcha that old "All your base" meme popped into your head! Don't feel too bad — it popped into our heads, too.)