Welcome back to the Tower Defense Tutorial Series! Once again I am assuming that you have read the previous 5 parts of this tutorial and are happy with the content covered in them.
In this Tutorial, we will be incorporating a new tower type into our game, this will give you the knowledge to then continue on and make/implement your own towers types. The tower type which we will be incorporating is a freeze tower, this tower will shoot Ice projectiles at a creep which will freeze it in place for a period of time, before the creep then continues on its journey.
Source code after the break!
The first thing to do is to download the new Turret and Projectile images that I created in about 2 minutes. They can be found here: Freeze Turret and Ice Projectile. Alternatively you can use your own images (however i recommend re-naming them in the same way to avoid confusion later). Then add them to your resources folder in Xcode. Alternatively if you want to just see everything in action, here is the source code: Tower Defense Part 6.
Now we have our images in our project, we want to add our new turret image to our HUD so that the player can select and place it within the level. Open GameHUD.m and look at the init section.
NSArray *images = [NSArray arrayWithObjects:@"MachineGunTurret.png", @"FreezeTurret.png", @"MachineGunTurret.png", @"MachineGunTurret.png", nil];
for(int i = 0; i < images.count; ++i) {
NSString *image = [images objectAtIndex:i];
CCSprite *sprite = [CCSprite spriteWithFile:image];
float offsetFraction = ((float)(i+1))/(images.count+1);
sprite.position = ccp(winSize.width*offsetFraction, 35);
sprite.tag = i+1;//individual tag for each Turret.
[self addChild:sprite];
[movableSprites addObject:sprite];
Within the "NSArray *images" we have changed the second "MachineGunTurret.png" to "FreezeTurret.png". This will load the freeze turret image as the second from the left image. Then in the "for loop" we added an individual tag to each tower type, "sprite.tag = i+1;" I chose to +1 so that the first Turret type(MachineGunTurret) starts at 1 rather than 0. And the New FreezeTurret will have a tag of 2. For now, turret types 3 + 4 are still machine gun turrets, but once you have completed this tutorial, feel free to add your own turrets types in here.
While we are in GameHUD.m go down to ccTouchesBegan and add the following piece of code directly above the line "[self addChild:newSprite];"
selSprite.tag = sprite.tag;
then go down to ccTouchedEnded and adjust the if statement:
if (!CGRectContainsPoint(backgroundRect, touchLocation))
to say:
if (!CGRectContainsPoint(backgroundRect, touchLocation)) {
CGPoint touchLocationInGameLayer = [m._gameLayer convertTouchToNodeSpace:touch];
[m._gameLayer addTower: touchLocationInGameLayer: selSprite.tag];
}
What do these changes do? In effect they take the tag of the currently selected turret type, and pass it to the addTower function in TutorialScene.m, when the player lets go of the tower placing it within the level. To get rid of the pesky error messages which are now occurring we need to adjust the addTower function.
Go into TutorialScene.h replace the addTower function definition with the following:
- (void)addTower: (CGPoint)pos: (int)towerTag;
then in TutorialScene.m replace the addTower function header (not the contents of the function) with the following:
-(void)addTower: (CGPoint)pos: (int)towerTag{
//Keep the same function content!
}
At this moment in time, if we run the game, we will see the FreezeTurret in the HUD and be able to select and move it, however when we place it on the map, it will transform into a MachineGunTurret.This is partly because we have not created the FreezeTower class, we shall do this now. Open Tower.m and copy and paste the following class at the very end of the file (after @end).
@implementation FreezeTower
+ (id)tower {
FreezeTower *tower = nil;
if ((tower = [[[super alloc] initWithFile:@"FreezeTurret.png"] autorelease])) {
tower.range = 200;
tower.target = nil;
[tower schedule:@selector(towerLogic:) interval:2];
}
return tower;
}
-(id) init
{
if ((self=[super init]) ) {
//[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
return self;
}
-(void)setClosestTarget:(Creep *)closestTarget {
self.target = closestTarget;
}
-(void)towerLogic:(ccTime)dt {
self.target = [self getClosestTarget];
if (self.target != nil) {
//rotate the tower to face the nearest creep
CGPoint shootVector = ccpSub(self.target.position, self.position);
CGFloat shootAngle = ccpToAngle(shootVector);
CGFloat cocosAngle = CC_RADIANS_TO_DEGREES(-1 * shootAngle);
float rotateSpeed = 0.5 / M_PI; // 1/2 second to roate 180 degrees
float rotateDuration = fabs(shootAngle * rotateSpeed);
[self runAction:[CCSequence actions: [CCRotateTo actionWithDuration:rotateDuration angle:cocosAngle], [CCCallFunc actionWithTarget:self selector:@selector(finishFiring)], nil]];
}
}
-(void)creepMoveFinished:(id)sender {
DataModel * m = [DataModel getModel];
CCSprite *sprite = (CCSprite *)sender; [self.parent removeChild:sprite cleanup:YES];
[m._projectiles removeObject:sprite];
}
- (void)finishFiring {
DataModel *m = [DataModel getModel];
self.nextProjectile = [Projectile projectile];
self.nextProjectile.position = self.position;
[self.parent addChild:self.nextProjectile z:1];
[m._projectiles addObject:self.nextProjectile];
ccTime delta = 0.5;
CGPoint shootVector = ccpSub(self.target.position, self.position);
CGPoint normalizedShootVector = ccpNormalize(shootVector);
CGPoint overshotVector = ccpMult(normalizedShootVector, 320);
CGPoint offscreenPoint = ccpAdd(self.position, overshotVector);
[self.nextProjectile runAction:[CCSequence actions: [CCMoveTo actionWithDuration:delta position:offscreenPoint], [CCCallFuncN actionWithTarget:self selector:@selector(creepMoveFinished:)], nil]];
self.nextProjectile.tag = 2;!!
self.nextProjectile = nil;
}
@end
then in Tower.H: copy and paste the following class definitions at the very end of the file (after @end)
@interface FreezeTower : Tower {
}
+ (id)tower;
- (void)setClosestTarget:(Creep *)closestTarget;
- (void)towerLogic:(ccTime)dt;
- (void)creepMoveFinished:(id)sender;
- (void)finishFiring;
@end
So what have we done here? Well if you look through the code you will see that we have done very little apart from copy the MachineGunTurret class and change a few parameters. Such as: the image, the fire rate of the turret (towerLogic schedule), the speed of the projectile (just made it a bit faster). So now we have our FreezeTurret class, we should go back into addTower and allow it to create a FreezeTurret if a player selects a freeze turret. Now open TutorialScene.m, and go to the addTower function. Under the check to see if the tile is build able add in the following:
NSLog(@"Buildable: %@", type);
if([type isEqualToString: @"1"]) {
printf("tower tag %i", towerTag);
switch (towerTag) {
case 1:
target = [MachineGunTower tower];
break;
case 2:
target = [FreezeTower tower];
break;
case 3:
target = [MachineGunTower tower];
break;
case 4:
target = [MachineGunTower tower];
break;
default:
break;
}
Now our towerTag, passed from GameHUD, comes in handy as we check to see which tower the player has selected. When the player lets go we add the same tower type to the level. So now we should be able to select and add two different classes of Tower to our game world.
Run, Check, Debug.
Ok, so you might have noticed, at the moment the FreezeTurret behaves more like a slow MachineGunTurret than a FreezeTurret. In this next section we shall improve on this by adding the freeze logic to the update function, and then allowing the creeps to resume its path. The first thing that we should do is to go into Tower.M and make sure that the projectiles coming from MachineGunTurret have a tag of 1. Whilst the projectiles coming from FreezeTurret have a tag of 2. Next, open TutorialScene.m and go down to our Update function, and find the section where we check for collisions between projectiles and creeps and replace the entire if statement with the following.
if (CGRectIntersectsRect(projectileRect, targetRect)) {
[projectilesToDelete addObject:projectile];
Creep *creep = (Creep *)target;
if (projectile.tag ==1) { //MachineGun Projectile
creep.hp--;
if (creep.hp <= 0) {
[targetsToDelete addObject:target];
}
break;
}
else if (projectile.tag ==2) { //Freeze projectile
id actionFreeze = [CCMoveTo actionWithDuration:1 position:creep.position];
id actionMoveResume = [CCCallFuncN actionWithTarget:self selector:@selector(ResumePath:)];
[creep stopAllActions];
[creep runAction:[CCSequence actions:actionFreeze, actionMoveResume, nil]];
break;
}
break;
}
When a collision between a projectile and a creep is detected, we then check to see what the projectile tag is. If the projectile.tag == 1 then we know this projectile come from a MachineGunTurret and we perform the same actions as we have done in the previous tutorials. However if the projectile.tag == 2 then we know this is a freeze projectile, in which case we run a series of actions. First we Freeze the creep for 1 second at its current location. Then we call a new function ResumePath:, in which we will allow the creep to continue along its path.
in tutorialscene.m add in the following function
-(void)ResumePath:(id)sender {
Creep *creep = (Creep *)sender;
WayPoint * cWaypoint = [creep getCurrentWaypoint];//destination
WayPoint * lWaypoint = [creep getLastWaypoint];//startpoint
float waypointDist = fabsf(cWaypoint.position.x -lWaypoint.position.x);
float playerDist = fabsf(creep.position.x - cWaypoint.position.x);
float distFraction = playerDist / waypointDist;
float moveDuration = creep.moveDuration * (distFraction); //Time it takes to go from one way point to another * the fraction of how far is left to go (meaning it will move at the correct speed)
id actionMove = [CCMoveTo actionWithDuration:moveDuration position:cWaypoint.position];
id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(FollowPath:)];
[creep stopAllActions];
[creep runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
}
then in creep.m add in the following function
- (WayPoint *)getLastWaypoint{
DataModel *m = [DataModel getModel];
//int lastWaypoint = m._waypoints.count;
self.lastWaypoint = self.curWaypoint -1;
WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.lastWaypoint];
return waypoint;
}
[Side Note: REMEMBER TO ADD IN THE HEADERS FOR THESE FUNCTIONS.]
What do we do in these functions? Well in essence we find the distance between the previous waypoint and the current waypoint. Then we find the distance between the creep and the current waypoint. We then represent the distance between the creep and the current waypoint as a fraction of the distance between the last waypoint and the current waypoint. Then we use this fraction to affect the creep.moveDuration so that when the creep continues towards the current waypoint, it moves at a speed relative to its fellow creeps. After it hits the current waypoint it will then continue along its path normally. One thing that I would like to do here is to correct a slight error which occurs within the creep.m code. Replace the getNextWaypoint function with the following:
[Update: found a small issue in the getNextWaypoint function that would cause the health bar to act weird - It's been fixed now in the below code]
- (WayPoint *)getNextWaypoint{
DataModel *m = [DataModel getModel];
self.curWaypoint++;
if (self.curWaypoint >= m._waypoints.count){
self.curWaypoint--;
gameHUD = [GameHUD sharedHUD];
if (gameHUD.baseHpPercentage > 0) {
[gameHUD updateBaseHp:-10];
}
Creep *target = (Creep *) self;
NSMutableArray *endtargetsToDelete = [[NSMutableArray alloc] init];
[endtargetsToDelete addObject:target];
for (CCSprite *target in endtargetsToDelete) {
[m._targets removeObject:target];
[self.parent removeChild:target cleanup:YES];
}
return NULL;
}
WayPoint *waypoint = (WayPoint *) [m._waypoints objectAtIndex:self.curWaypoint];
return waypoint;
}
If you have noticed an intermittent crash, it is because when the creeps gets to the last waypoint this function was not working correctly. However this update will recognize when a creep is at the last waypoint and delete the creep. So, run this and we should see that the FreezeTowers, freeze the creeps, and then the creeps (after one second) will continue along their path at a normal speed.
One final thing we can implement is to add in an IceProjectile that the FreezeTower fires, rather than the usual machine gun projectile. Once again this is fairly simple, open Projectile.m and add the following class at the end of the file (after @end):
@implementation IceProjectile
+ (id)projectile {
IceProjectile *projectile = nil;
if ((projectile = [[[super alloc] initWithFile:@"IceProjectile.png"] autorelease])) {
}
return projectile;
}
- (void) dealloc
{
[super dealloc];
}
@end
then in Projectile.h add the following at the end of the file:
@interface IceProjectile : CCSprite {
}
+ (id)projectile;
@end
Simply this creates another class of projectile, the IceProjectile. Now go into Tower.h and go down to the FreezeTower class, and find the function finishedFiring. Adjust the call to [Projectile projectile]; to say the following:
self.nextProjectile = [IceProjectile projectile];
And there we have it! FreezeTowers, IceProjectiles, and FreezeCreep functionality. As always, comments / suggestions / found errors? All very welcome! Thanks for Reading!
Yours,
Aiden Fry
iPhone Game Tutorials Contributor
Aiden Fry is a recent graduate, working to get into the games industry. He is a games programmer with a special interest in audio programming. Please check out his website aidenfry.tk to see some of his work.
sending...
Great tutorial !
I have some ideas/wishes
- Splash towers (affect an area with multiple enemies)
- Upgrade towers to give more powerful/speed.
Thanks
Thanks demosc.
.
Upgrades are coming in part 8
I was thinking about splash damage… but as i’v shown you the basic principals of adding towers i thought it would be good “homework” for you to try adding your own tower types.
But feel free to get in contact if you get stuck.
I have some problems with this kind of tower (problem to get the affected area).
Please can you add a code to build this kind of tower?
Thanks.
Ok, so add your new tower, and add your new projectile with tag = 3.
This is exactly the same as we did in the above tutorial for freezetower.
However, the change comes in the TutorialScene.m > update method
When a collision between a projectile and a creep is detected add in the following code, when we are checking the projectile.tag.
else if (projectile.tag ==3){//Splash projectile
CGRect splashprojectileRect = CGRectMake(projectile.position.x – (100),
projectile.position.y – (100),
200,
200);
for (CCSprite *target in m._targets) {
CGRect thistargetRect = CGRectMake(target.position.x – (target.contentSize.width/2),
target.position.y – (target.contentSize.height/2),
target.contentSize.width,
target.contentSize.height);
if (CGRectIntersectsRect(splashprojectileRect, thistargetRect)) {
Creep *thisCreep = (Creep *) target;
thisCreep.hp -= 5;
if (thisCreep.hp <= 0) {
[targetsToDelete addObject:thisCreep];
}
}
else
break;
}
[projectilesToDelete addObject:projectile];
}
Explanation: we find that the projectile is of tag 3 therefor we want splash damage.
We create a new rect splashRect (this is your splash damage square) you could hold the distance values (here it is 100 and 100/2) in the projectile/tower classes (for upgrades later) however for ease of explanation I have hard coded them in here.
Next we look through the m._targets array and create a new rect for each creep.
Then see if the splashrect intersect the target rect, if so it does some damage.
Finally see if the creep is out of health and delete if so.
and Delete projectile.
Hope this helps
A
Awesome! Another great tutorial! I am fascinated! Keep up the great work!
Hiya, love the tutorials. I have a problem, i loaded your version of 6.0 and everything seems to run perfectly however when i scroll over the map it seems like the tiles get underlined or something… it seems like it has the sprites “tearing” over the screen. I have had this problem with every version i tried of your code. Any ideas?
Hi jimmy, we have not been unable to reproduce this kind of error. Maybe it has something to do with your OS, or version of Xcode, or Cocos 2d. Possibly try reinstalling if the problem continues.
Maybe try some other cocos2d projects and see if the same issues occur..
Sorry I can’t be of much use.
Too bad
you guys should really look into it. Because its the only game i have problems with
. I have iphone 4gs clean install 5.01 clean install cocos2d clean install xcode 4.2.1. I read some forums it has to do with the way you call the sprite nodes ( it has issues on iphone 4s resulting in “ghosting/artifacts”). The simulator however works fine for me, my girlfriends iphone 4 version also runs fine Its just my own iphone 4s that has problems. Weird
Thanks for the additional information – We’ll check it out again.
I found the solution in your code on another website
After hours(!!) of fighting these pesky little black lines in my CCTilemapAtlas in the last release of cocos2d (0.9 beta2) I upgraded to the latest release (0.99 rc) yesterday and my beloved friends are back. *bitesHisTongue*
I followed all the tips and tricks I could find
How to fix ——
I decided to use a category to work-around this issue. Create a class called “CCMoveTo+RoundedUpdate” and add this code:
CCMoveTo+RoundedUpdate.h
//
// CCMoveTo+RoundedUpdate.h
//
// Category to rounded move any object and avoid gaps in tilemap.
// –> Overrides “update” code from CCIntervalAction.m
//
// Created by Markus Barta on 09.02.10.
//
#import
#import “cocos2d.h”
@interface CCMoveTo (RoundedUpdate)
@end
CCMoveTo+RoundedUpdate.m
//
// CCMoveTo+RoundedUpdate.m
//
// Created by Markus Barta on 09.02.10.
//
#import “CCMoveTo+RoundedUpdate.h”
@implementation CCMoveTo (RoundedUpdate)
-(void) update: (ccTime) t
{
//NSLog(@”Using category CCMoveTo+RoundedUpdate for movement -> rounded setPosition!”);
[target setPosition: ccp( round(startPosition.x + delta.x * t ), round(startPosition.y + delta.y * t ) )]; //Setting to a rounded pos avoids gaps in tilemap!
}
@end
Another Fix–
I was having the same problem with flickering black lines appearing between my tiles on my TMX map when I moved the map layer around.
I found two different ways to solve it:
Changing my spritesheet png to use a margin and spacing of 2 and then running the spritesheet-artifact-fixer.py on it.
Using my original spritesheet and your code above. I like using the original spritesheet because I can fit more tiles on it with no spacing or margins.
Can someone comment on the preferred method? spritesheet-artifact-fixer.py or rounding? Are we losing anything important by rounding?
Hmmm this does not seem to solve the issue. Also I just ran the program through cocos1.0.1 build, the problem still ensues. Dreemlife can you email me your fixed version of any of these tutorials so i can see whats going on! damn these pesky lines.
Hiya mate, i sent you an email with the fixed ccsprite.m. Runs smooth like a baby now.
yep its sorted now phew! new tutorial coming soon, the first thing we do is to shoot these artifacts in the head!
Thanks Jimmy
Haha
I hear ya mate it was rather nasty
, looking forward to tutorial 8. I will be finishing my website tonight and then i got all weekend to finish programming
Its patched in libraries above 0.96 cocos2d so you probably used an older library to start building this app on
Aha, ok i’m on the issue.
So, if i understand u dreemlife, all that needs to happen is to create a new cocos2d project (starting with the newer library) then add all the existing classes and it should solve the issue?
Im getting my dev license next week so I can sort this out.
Sorry for the inconvenience, this project was started by another contributor (about a year ago) and I forgot to check for lib updates
Keep an eye out for updated source code soon.
Exactly so Aiden, thats what i did. The CCSprite.H u are using is an old one, that contains the flaws that only IOS 5 models are having. It has to do with boundaries around images not being big enough. If you built your complelete project in cocod2d 1.01 it should be fixed.
The cocos2d 1.01 has a new fix inside ccsprite.h that makes the outlines of sprites x = 2 instead of one and therefore it no longer bugs
Here is a link to the fix http://www.cocos2d-iphone.org/forum/topic/4219 talking about it also….
Glad i could help
. Im currently using bits of your code to make a new “Supaplex” kind of game
I really liked your coding of the sprites
thats why i was so keen on solving this issue
Thanks for the heads up, ill sort these shenanigans out asap!
Sounds good let me know how it goes. Im currently making this game into a finished product myself, hopefully going to be on the apple store soonish….. Although there is nothing particularly original in here
might need to add some random crazyness or just make it extremely difficult haha
Btw, if u ent seen it have a little look at Tiny Heroes on app store, great game and will probably give u a few ideas for your supaplex spin off (i’v stolen a few ideas myself hehe)
Nice
If you like i can help with the graphics for the towers also if you haven’t done that already.
I have not checked out Tiny Heroes that much, thankx for the heads up i’ll look into that one. I making the movement like the PACMANlite app so you can use a dpad
on the iphone but that gives me some troubles hehe
.
Something that makes your TD unique is, cool towers/abilities. Hard changing waves on randomness… or mix alot
Im still getting into this programming stuff… i have alot of experience with database and graphics just not programming itsself
. But i’m a quick learner ( reading all books i could possible buy). So if i could offer any assistance, id be glad 2 help out.
oh snap! might well take u up on some artwork. I am NOT an artist lol!
What I will do is to properly design my gameplay then ill get back to you for some image replacements
Me, im programming and audio guy so, The same goes man, if u want some help with your project(s) just hit me up
.
my email is aidenfry@hotmail.co.uk if u wanna converse about stuff without spamming this page hehe.