Enemies and Combat: How to Create a Tile Based Game with Cocos2D Part 3

by

Update: Fully updated for Cocos2D 2.1 rc0Tiled 0.9.0 (released Feb 2013), Xcode 4.6, and ARC to match the newly updated part 1 & 2!

This is a continuation of one of Ray Wenderlich’s excellent tutorials for making games with Cocos2D for iPhone. If you haven’t read them already, visit his website and check them out. In particular, this is a “Part 3″ to his “How to Make a Tile Based Game with Cocos2D” 2-part series. If you haven’t read those yet, you should start with his part 1.

In part 2, Ray covered how to make collidable areas in the map, how to use tile properties, how to make collectable items and modify the map dynamically, and how to make a “Heads up display” to show the score.

In this tutorial, we’ll add enemies, make it so your ninja can throw ninja stars at them, and add some win/lose game logic. But first, download this zip file of additional resources for the new enhancements (thank you for permission to use the art, Ray).

The zip file contains:

Don’t forget to add them to your project before continuing.

Adding Enemies

At the end of part 2, the project is very cool, but it’s not much of a game. Your ninja can wander around eating melons with no difficulty at all, and there’s no way to win or lose. If there were enemies who chased your ninja, the game would be a lot more exciting.

Enemy Spawn Points

Okay, go to Tiled (these screenshots use the Qt version) and open your Tile Map (TileMap.tmx).

Add an object to the Objects layer, somewhere not too near the player. This will be an enemy spawn point. Name it “EnemySpawn1″.

The objects in an object group are stored in an NSMutableDictionary, with the object name as the key. This means that each spawn point must have a unique name. Although we could iterate through all the keys to see which ones start with “EnemySpawn”, that is an inefficient operation. Instead, we will use a property to indicate that a given object represents an enemy spawn point.

EnemySpawnPoints

Give the object a property “Enemy”, with a value of 1. If you want to expand on this tutorial and add other types of enemies, you can use other values of the enemy property to indicate the type of enemy.

Now make six to ten more of these Enemy Spawn Point objects, at varying distances from the player. Give each one the “Enemy” property, with a value of 1. Save the map and go to Xcode.

Showing Enemy Spawn Points on the map in Tiled

Creating the Enemies

Okay, now we’ll make the enemies actually appear on the map. Add the following code in HelloWorldLayer.m:


// in the HelloWorldLayer class
-(void)addEnemyAtX:(int)x y:(int)y {
  CCSprite *enemy = [CCSprite spriteWithFile:@"enemy1.png"];
  enemy.position = ccp(x, y);
  [self addChild:enemy];
}

// in the init method - after creating the player
// iterate through objects, finding all enemy spawn points
// create an enemy for each one
for (spawnPoint in [objectGroup objects]) {
  if ([[spawnPoint valueForKey:@"Enemy"] intValue] == 1){
    x = [[spawnPoint valueForKey:@"x"] intValue];
    y = [[spawnPoint valueForKey:@"y"] intValue];
    [self addEnemyAtX:x y:y];
  }
}

The first loop iterates through the list of objects, checking to see if they are enemy spawn points. If they are, it gets their X and Y positions in the same way as the player’s. Then, it adds an enemy in the right location by calling the addEnemyAtX:y: method.

The addEnemyAtX:y: method is fairly basic. It just creates an enemy sprite at the x, y location passed in.

If you build and run this, you’ll see that there are enemies in the locations you specified in Tiled. Awesome!

There’s just one problem – the enemies don’t chase you!

Making them Move

So, now we’ll add some code to make the enemy sprites chase the player. Because the player might be moving, we must re-orient the enemy occasionally. To do this, we move the enemy in 10-pixel long segments, re-orienting the enemy before each segment.

Add the following code to HelloWorldLayer.m, in the HelloWorldLayer class:


// callback. starts another iteration of enemy movement.
- (void) enemyMoveFinished:(id)sender {
  CCSprite *enemy = (CCSprite *)sender;

  [self animateEnemy: enemy];
}

// a method to move the enemy 10 pixels toward the player
- (void) animateEnemy:(CCSprite*)enemy
{
  // speed of the enemy
  ccTime actualDuration = 0.3;

  // Create the actions
  id actionMove = [CCMoveBy actionWithDuration:actualDuration
    position:ccpMult(ccpNormalize(ccpSub(_player.position,enemy.position)), 10)];
  id actionMoveDone = [CCCallFuncN actionWithTarget:self
    selector:@selector(enemyMoveFinished:)];
  [enemy runAction:
    [CCSequence actions:actionMove, actionMoveDone, nil]];
}

// add this at the end of addEnemyAtX:y:
// Use our animation method and
// start the enemy moving toward the player
  [self animateEnemy:enemy];

The animateEnemy: method creates two actions. The first one tells it to move 10 pixels toward the player, over a duration of 0.3 seconds. You can change the duration to make the enemies move faster or slower. The second action will call the enemyMoveFinished: method. We combine these with a CCSequence action so that the enemyMoveFinished: method will be called when the enemy stops moving. In addEnemyAtX:y:, we call animateEnemy: to start the enemy moving toward the player.

The enemyMoveFinished: method calls the animateEnemy: method, continuing with the next segment of the animation. The math in the definition of actionMove basically calculates a path going 10 pixels toward the player.

Neat! But wouldn’t it look much more impressive if the enemies pointed toward the player? Add the following code to animateEnemy:


// immediately before creating the actions in animateEnemy
// rotate to face the player
CGPoint diff = ccpSub(_player.position,enemy.position);
float angleRadians = atanf((float)diff.y / (float)diff.x);
float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
float cocosAngle = -1 * angleDegrees;
if (diff.x < 0) {
  cocosAngle += 180;
}
enemy.rotation = cocosAngle;

This code finds the direction that the player is in relative to the enemy, and rotates the enemy to point towards the player.

Ninja Stars

Okay, but our player is a NINJA! He should be able to defend himself!

We will add modes to the game. Modes are not the best way to implement this, but they are significantly easier than the alternatives, and they will work in the simulator (because they don’t need multi-touch). Because of these advantages, we will use them in this tutorial. The UI will be set up such that the user can easily switch between moving mode and ninja star throwing mode. We will add a button to switch into ninja star throwing mode and back to moving mode.

Now, we will set up some properties to allow easy communication between the two layers. Add to HelloWorldLayer.h:

// in HelloWorldLayer, outside the curly braces
@property (assign) int mode;

And to HelloWorldLayer.m:

// just after the import statements at the top of the file, but before the @implementation HudLayer

@interface HudLayer ()
// if you are setting your deployment target for iOS 5.0 or later then replace "unsafe_unretained" with "weak" which is then auto set to nil by the runtime. Not supported before 5.0 though.
@property (unsafe_unretained) HelloWorldLayer *gameLayer;
@end

// in HelloWorldLayer's init method
_mode = 0;

// in HelloWorldLayer's scene method
// after layer.hud = hud
hud.gameLayer = layer;

For more detail about how the button works, visit Ray’s tutorial on the topic.

Add the folowing code, which defines a button, to HelloWorldScene.m:

// in HudLayer's init method
// define the button
CCMenuItem *on;
CCMenuItem *off;

on = [CCMenuItemImage itemWithNormalImage:@"projectile-button-on.png"
  selectedImage:@"projectile-button-on.png" target:nil selector:nil];
off = [CCMenuItemImage itemWithNormalImage:@"projectile-button-off.png"
  selectedImage:@"projectile-button-off.png" target:nil selector:nil];

CCMenuItemToggle *toggleItem = [CCMenuItemToggle itemWithTarget:self
  selector:@selector(projectileButtonTapped:) items:off, on, nil];
CCMenu *toggleMenu = [CCMenu menuWithItems:toggleItem, nil];
toggleMenu.position = ccp(100, 32);
[self addChild:toggleMenu];

In the HudLayer implementation add the new method to respond to the button tap:


// callback for the button
// mode 0 = moving mode
// mode 1 = ninja star throwing mode
- (void)projectileButtonTapped:(id)sender
{
  if (_gameLayer.mode == 1) {
    _gameLayer.mode = 0;
  } else {
    _gameLayer.mode = 1;
  }
}

Build and run. A button will appear on the lower left, and you will be able to turn it on and off, but it won’t actually affect the game. Our next step is to add projectile launching.

Launching Projectiles

Next, we will add code to detect which mode the user is in and determine what to do when the screen is tapped based on the mode.

Put the code currently in ccTouchEnded:withEvent: within the if portion of the following if/else block as indicated by the comment:

if (_mode == 0) {
  // old contents of ccTouchEnded:withEvent:
} else {
  // code to throw ninja stars will go here
}

This will result in movement only occuring in move mode. The next step is to add code that will launch a ninja star to the else portion of the if block.

Add the following cleanup method to HelloWorldLayer.m:


- (void) projectileMoveFinished:(id)sender {
  CCSprite *sprite = (CCSprite *)sender;
  [self removeChild:sprite cleanup:YES];
}

And now, in the else block where it says:

// code to throw ninja stars will go here

put the following code:

// Find where the touch is
CGPoint touchLocation = [touch locationInView: [touch view]];
touchLocation = [[CCDirector sharedDirector] convertToGL: touchLocation];
touchLocation = [self convertToNodeSpace:touchLocation];

// Create a projectile and put it at the player's location
CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png"];
projectile.position = _player.position;
[self addChild:projectile];

// Determine where we wish to shoot the projectile to
int realX;

// Are we shooting to the left or right?
CGPoint diff = ccpSub(touchLocation, _player.position);
if (diff.x > 0)
{
  realX = (_tileMap.mapSize.width * _tileMap.tileSize.width) +
             (projectile.contentSize.width/2);
} else {
  realX = -(_tileMap.mapSize.width * _tileMap.tileSize.width) -
             (projectile.contentSize.width/2);
}
float ratio = (float) diff.y / (float) diff.x;
int realY = ((realX - projectile.position.x) * ratio) + projectile.position.y;
CGPoint realDest = ccp(realX, realY);

// Determine the length of how far we're shooting
int offRealX = realX - projectile.position.x;
int offRealY = realY - projectile.position.y;
float length = sqrtf((offRealX*offRealX) + (offRealY*offRealY));
float velocity = 480/1; // 480pixels/1sec
float realMoveDuration = length/velocity;

// Move projectile to actual endpoint
id actionMoveDone = [CCCallFuncN actionWithTarget:self
  selector:@selector(projectileMoveFinished:)];
[projectile runAction:
  [CCSequence actionOne:
		    [CCMoveTo actionWithDuration: realMoveDuration
        		                position: realDest]
                    two: actionMoveDone]];

This will launch the ninja star in the direction the player tapped. For exact details on how this works, check out the Shooting Projectiles section of Ray’s How to Make a Simple Game in Cocos2D tutorial. However, the comments should explain roughly what is happening.

The projectileMoveFinished: method removes the projectile from the layer once it gets off the map. This method is absolutely critical. Once we start doing collision detection, we will need to iterate through all of the ninja stars. If we don’t remove the ones that leave the screen, the list will grow larger and larger and the game will slow down.

Build the project and run it. Now, you can throw ninja stars at the enemies!

Collision Detection

The next step is to destroy the enemies when they are hit. Add the following two properties to the HelloWorldLayer class (in HelloWorldLayer.m) after @interface HelloWorldLayer() where the other properties are defined:

@property (strong) NSMutableArray *enemies;
@property (strong) NSMutableArray *projectiles;

Now, add the projectiles to the projectiles array with this code:

// at the end of the launch projectiles section of ccTouchEnded:withEvent:
  [self.projectiles addObject:projectile];

  // at the end of projectileMoveFinished:
  [self.projectiles removeObject:sprite];

Next, add the following code to the end of addEnemyAtX:y:

  [self.enemies addObject:enemy];

Next, add the following method to the HelloWorldLayer class:

- (void)testCollisions:(ccTime)dt {

  NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];

  // iterate through projectiles
  for (CCSprite *projectile in self.projectiles) {
    CGRect projectileRect = CGRectMake(
      projectile.position.x - (projectile.contentSize.width/2),
      projectile.position.y - (projectile.contentSize.height/2),
      projectile.contentSize.width,
      projectile.contentSize.height);

    NSMutableArray *targetsToDelete = [[NSMutableArray alloc] init];

    // iterate through enemies, see if any intersect with current projectile
    for (CCSprite *target in self.enemies) {
      CGRect targetRect = CGRectMake(
        target.position.x - (target.contentSize.width/2),
        target.position.y - (target.contentSize.height/2),
        target.contentSize.width,
        target.contentSize.height);

      if (CGRectIntersectsRect(projectileRect, targetRect)) {
        [targetsToDelete addObject:target];
      }
    }

    // delete all hit enemies
  for (CCSprite *target in targetsToDelete) {
      [self.enemies removeObject:target];
      [self removeChild:target cleanup:YES];
    }

    if (targetsToDelete.count > 0) {
      // add the projectile to the list of ones to remove
      [projectilesToDelete addObject:projectile];
    }
  }

  // remove all the projectiles that hit.
  for (CCSprite *projectile in projectilesToDelete) {
    [self.projectiles removeObject:projectile];
    [self removeChild:projectile cleanup:YES];
  }
}

Finally, initialize the enemy and projectile arrays and schedule the testCollisions: method to be called as often as possible by adding this code to HelloWorldLayer’s init method:

  // you need to put these initializations before you add the enemies,
  // because addEnemyAtX:y: uses these arrays.
  self.enemies = [[NSMutableArray alloc] init];
  self.projectiles = [[NSMutableArray alloc] init];
  [self schedule:@selector(testCollisions:)];

As above, for exact details on how this works, check out the Collision Detection section of Ray’s How to Make a Simple Game in Cocos2D tutorial. However, the comments should explain roughly what is happening. Build the project and run it. Try throwing ninja stars at the enemies now, and they will disappear! The next step: winning and losing.

Winning and Losing

The Game Over Scene

Now, let’s create a new scene that will serve as our “You Win” or “You Lose” indicator. In Xcode select the Classes folder in the project tree and go to File\New File, and choose Objective-C class, and make sure subclass of NSObject is selected. Click Next, then type in GameOverScene as the filename, and make sure “Also create GameOverScene.h” is checked.

Then replace the template code in GameOverScene.h with the following code:

#import "cocos2d.h"

@interface GameOverLayer : CCLayerColor {
}
@property (nonatomic, strong) CCLabelTTF *label;
@end

@interface GameOverScene : CCScene {
}
@property (nonatomic, strong) GameOverLayer *layer;
@end

Then replace the template code in GameOverScene.m with the following code:

#import "GameOverScene.h"
#import "HelloWorldLayer.h"

@implementation GameOverScene

- (id)init {

  if ((self = [super init])) {
    self.layer = [GameOverLayer node];
    [self addChild:_layer];
  }
  return self;
}

@end

@implementation GameOverLayer

-(id) init
{
  if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {

    CGSize winSize = [[CCDirector sharedDirector] winSize];
    self.label = [CCLabelTTF labelWithString:@"" fontName:@"Arial" fontSize:32];
    _label.color = ccc3(0,0,0);
    _label.position = ccp(winSize.width/2, winSize.height/2);
    [self addChild:_label];

    [self runAction:[CCSequence actions:
      [CCDelayTime actionWithDuration:3],
      [CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)],
      nil]];

  }
  return self;
}

- (void)gameOverDone {

  [[CCDirector sharedDirector] replaceScene:[HelloWorldLayer scene]];

}

@end

The GameOverLayer just puts a label in the middle of the screen, and schedules a transition back to the HelloWorldLayer’s scene to occur 3 seconds in the future.

Winning

Now, let’s add code to end the game when the player has all the melons. Add the following code to HelloWorldLayer’s setPlayerPosition: method in HelloWorldLayer.m, right after updating the score:

  // put the number of melons on your map in place of the '6'
  if (self.numCollected == 6) {
    [self win];
  }

And create the win method in the HelloWorldLayer class in HelloWorldLayer.m, before the setPlayerPosition: method:

- (void) win {
  GameOverScene *gameOverScene = [GameOverScene node];
  [gameOverScene.layer.label setString:@"You Win!"];
  [[CCDirector sharedDirector] replaceScene:gameOverScene];
}

Now add the following code to the top of the HelloWorldScene.m file:

#import "GameOverScene.h"

Build and run. You will now win when you collect all of the melons. Victory!

Losing

For this simple tutorial, the player loses if a single enemy touches the ninja. Add the following loop to the end of HelloWorldLayer’s testCollisions: method, in HelloWorldLayer.m:


  for (CCSprite *target in _enemies) {
    CGRect targetRect = CGRectMake(
      target.position.x - (target.contentSize.width/2),
      target.position.y - (target.contentSize.height/2),
      target.contentSize.width,
      target.contentSize.height );

    if (CGRectContainsPoint(targetRect, _player.position)) {
      [self lose];
    }
  }

This loop iterates through all of the enemies and ends the game if any of them are touching the player.
Now, create the lose method in the HelloWorldLayer class in HelloWorldLayer.m:

- (void) lose {
  GameOverScene *gameOverScene = [GameOverScene node];
  [gameOverScene.layer.label setString:@"You Lose!"];
  [[CCDirector sharedDirector] replaceScene:gameOverScene];
}

Build and run, and when the monsters hit you, you will lose!

Get the Code!

The code’s moved to github and you can check it out or download it from the project page. Thank you for following along, that’s all for now.

Where to next?

Suggestions:

  • Multiple levels
  • Multiple types of enemy
  • Health, and a health bar in the Hud layer
  • More kinds of collectible items (healing, etc.)
  • A main menu
  • A better user interface for throwing ninja stars
About these ads

125 Responses to “Enemies and Combat: How to Create a Tile Based Game with Cocos2D Part 3”

  1. Collisions and Collectables: How To Make a Tile Based Game with Cocos2D Part 2 | Ray Wenderlich Says:

    [...] enjoyed this series, our good friends from Geek and Dad have developed a follow-up to the series: Enemies and Combat: How To Make a Tile-Based Game with Cocos2D Part 3! Check it out to see how you can extend the game to add enemies, projectiles, and a win/lose [...]

  2. Ray Wenderlich Says:

    Great job guys!! The tutorial is very clear and well written, and the finished product is actually quite fun. I am sure this will be very useful to a lot of people, and I’ve put a link to it at the end of part 2.

    One small note – it appears that the “enemyKilled.wav” may be missing from the final project. Other than that again great work!!

  3. Tweets that mention Enemies and Combat: How to Create a Tile Based Game with Cocos2D Part 3 « Geek And Dad's Blog -- Topsy.com Says:

    [...] This post was mentioned on Twitter by Ray Wenderlich, Dad. Dad said: Enemies & Combat: How to Create a Tile Based Game with Cocos2D, Part 3: http://wp.me/pv6Bm-bm (a continuation of @rwenderlich's tutorial) [...]

  4. Dad Says:

    Hey Ray, thanks for catching the goof! Geek was going to add the sound effects for throwing the ninja stars, for hitting an enemy, winning and losing, but then decided the tutorial was getting too long so he took out the start he’d made with those changes and missed some.

    A fixed .zip archive has been posted!

  5. Marin Todorov Says:

    Wow – collaborative tutorial writing ! I like it !

    Great job, really well written – clear and to the point. I’ve been following as well the cocos2d series on Rey’s site and I was thinking all of them can be glued together for some time.

    Anyhow – cheers for sharing !
    Marin

  6. Chris Says:

    Great tutorial! The only thing I wasn’t a fan of is the fact that the enemies come at you no matter where they are on the map. So I added a little conditional that checked the distance between each enemy and the hero sprite, and only attacked as the hero got close – made for a more challenging game I think.

    -(void) animateEnemy:(CCSprite*)enemy {
    	//speed of the enemy
    	ccTime actualDuration = .2;
    	id actionMove;
     	int distanceFromPlayer = ccpDistance(_player.position, enemy.position);
    	
    	if (distanceFromPlayer < 350) { //Check whether enemy can "see" Ninja before moving towards him.
    	  actionMove = [CCMoveBy actionWithDuration:actualDuration 
    		position:ccpMult(ccpNormalize(ccpSub(_player.position,enemy.position)),10)];
    	} else {
    	  actionMove = [CCMoveBy actionWithDuration:actualDuration 
    		position:ccpMult(ccpNormalize(ccpSub(_player.position,enemy.position)),0)];
    	}
    
    	id actionMoveDone = [CCCallFuncN actionWithTarget:self 
    	selector:@selector(enemyMoveFinished:)];
    	[enemy runAction:[CCSequence actions:actionMove, actionMoveDone, nil]]; 
    }
    

    Down the road I guess you would evolve this into a patrolling vs attack mode for the enemy.

    • Dad Says:

      @Chris – hey, nice modification! Thanks for sharing it.

      Geek’s at Camp this week, but when he gets back I bet he’ll change his game to incorporate your enhancement.

      Thanks again!

    • Nathaniel Says:

      I did what Chris here was thinking of. I added patrolling to this. You might have figured this out already but what I did was use the object layer and created a region instead of just a point. These are labeled EnemyPatrolx where x is replaced by the enemy’s id, which is a property assigned to the spawn point. This is just for testing, it probably isn’t the most efficient was of doing this.

      //Here is the enemy spawn point code in - (id)init
      		spawnPoint = nil;
      		for (spawnPoint in [objects objects]) {
      			if ([[spawnPoint valueForKey:@"Enemy"] intValue] == 1){
      				x = [[spawnPoint valueForKey:@"x"] intValue];
      				y = [[spawnPoint valueForKey:@"y"] intValue];
      				[self addEnemyAtX:x y:y withID:[[spawnPoint valueForKey:@"ID"] intValue]];
      			}
      		}
      	}
      //Here is the add enemy function code
      - (void)addEnemyAtX:(int)x y:(int)y withID:(int)ID {
      	CCSprite *enemy = [CCSprite spriteWithFile:@"enemy1.png"];
      	enemy.position = ccp(x, y);
      	enemy.tag = ID;
      	[self addChild:enemy];
      	[self animateEnemy:enemy];
      	[_enemies addObject:enemy];
      }
      
      //And finally the animate enemy code
      - (void) animateEnemy:(CCSprite*)enemy {
      	CCTMXObjectGroup *objects = [_tileMap objectGroupNamed:@"Objects"];
      	NSMutableDictionary *patrolArea = [objects objectNamed:[NSString stringWithFormat:@"EnemyPatrol%d", enemy.tag]];
      	CGFloat x = [[patrolArea valueForKey:@"x"] floatValue];
      	CGFloat y = [[patrolArea valueForKey:@"y"] floatValue];
      	CGFloat w = [[patrolArea valueForKey:@"width"] floatValue];
      	CGFloat h = [[patrolArea valueForKey:@"height"] floatValue];
      	CGRect patrolAreaRect = CGRectMake(x, y, w, h);
      	//NSLog(@"If rect %@ contains point %@", NSStringFromCGRect(patrolAreaRect), NSStringFromCGPoint(_player.position));
      	ccTime actualDuration = 0.3;
      	CGPoint target;
      	if(CGRectContainsPoint(patrolAreaRect, _player.position)) { // move toward player
      		id actionMove = [CCMoveBy actionWithDuration:actualDuration position:ccpMult(ccpNormalize(ccpSub(_player.position,enemy.position)), 10)];
      		id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(enemyMoveFinished:)];
      		[enemy runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
      		target = _player.position;
      		NSLog(@"Enemy %d: Moving to target: %@", enemy.tag, NSStringFromCGPoint(target));
      	} else { // move around inside patrol zone
      		target = CGPointMake((frand() * w) + x, (frand() * h) + y);
      		actualDuration = ccpDistance(enemy.position, target) / 25.0;
      		id actionMove = [CCMoveTo actionWithDuration:actualDuration position:target];
      		id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(enemyMoveFinished:)];
      		[enemy runAction:[CCSequence actions:actionMove, actionMoveDone, nil]];
      		NSLog(@"Enemy %d: Moving to point: %@ in %f seconds", enemy.tag, NSStringFromCGPoint(target), actualDuration);
      	}
      	CGPoint diff = ccpSub(target,enemy.position);
      	float angleRadians = atanf((float)diff.y / (float)diff.x);
      	float angleDegrees = CC_RADIANS_TO_DEGREES(angleRadians);
      	float cocosAngle = -1 * angleDegrees;
      	if (diff.x &lt; 0) cocosAngle += 180;
      	enemy.rotation = cocosAngle;
      }
      
  7. Leon Says:

    This sentence confuse me: realX = (_tileMap.mapSize.height * _tileMap.tileSize.height) + (projectile.contentSize.width/2);

    i think it’s better to replace (_tileMap.mapSize.height * _tileMap.tileSize.height) with (_tileMap.mapSize.width * _tileMap.tileSize.width), though they have same function since the height equals the width, but actually you must want to compute the width, right?

    • Dad Says:

      @Leon – definitely. I didn’t review Geek’s code in detail but that looks like something left over from a different approach (like he was calculating Y first or something). I’ve changed it in the blog post and in the downloadable project zip file.

      As I was doing this I’m also noticing the black lines being drawn from time to time when scrolling; I’m not able to get them to happen in Ray’s part 2 sample, but I don’t see what is different in the part 3 code…. The tmx map fields for spacing and margin are identical, as are the png files. So I don’t see why they should be different in behavior… The black lines sure are annoying though!

  8. Kunio Says:

    Great tutorial!!!!
    I’m new to the cocos2d and need some help.
    When I tried to run the project, I am getting following error:

    Cocos2d.h: No such file or directory.

    I tried to add my cocos2d files but no luck.
    How can I add cocos2d library to existing project?

    Thanks

    • Dad Says:

      So sorry! Turns out when I put in the fix suggested by Leon I used the latest source from svn, but I think that Geek, in his rush prepare to leave for camp, didn’t check in his last changes so I posted fixes to old code!

      A new archive has been uploaded that has Leon’s fix for clarity and actually builds, so anyone who downloaded one that doesn’t Please download it again! (from the link at the end of the post above).

      Sorry about that!

  9. RobertKreed Says:

    say you wanted the Enemies to come back after about a minute, how would you do that?

  10. RobertKreed Says:

    Would it work the same way as the syn guy is doing it? since we are talking about sprites and not something that is part of the tile set.

    • Dad Says:

      Good point, it is different in that regard. Basically you will want to have a schedule: call that does the spawn enemy steps again (where it makes a new sprite for the enemy at the spawn point).

      If you want to only respawn an enemy from a particular spawn point when that enemy has gone away, then you’ll have to attach the spawn point info to each enemy somehow.

      Might be sufficient to just say that you’ll respawn enemies from each spawn point at a given interval.

  11. Jeff Says:

    This is a great tutorial and it seems to work great. I have made some changes and was testing out on device and noticed that the framerates drop quite a bit if while shooting. To the point where it has little delays and is jumpy when enemies are hit, etc. I was curious if anyone else had this. Was thinking maybe of something where as soon as projectiles leave the viewable area they should be removed so they can’t continue to move and/or hit enemies that are off screen.

    Unrelated question also: I have been trying to get tilemap games to run smoothly for a while and they always seem to suck up too much memory. Has anyone had success with trying to only show parts of the map that are viewable as a player moves around? I have heard people talk about turning on and off layers but I can’t see how it could be smooth when a player is moving around the map.

    • Dad Says:

      Hi Jeff – be interesting for you to take the unchanged one and test that on your same device – is it slow also? If not, then something you did made it slower :-) (what device is coming up slow?).

      I’m not completely familiar with Geek’s code, but he’s off at camp for 3 more weeks so I’ll try to answer your question:

      It appears that the projectileMoveFinished method is supposed to remove the projectiles once they are done moving.

      For more general questions, the cocos2d-iphone forum is a good place to ask questions.

  12. Jeff Says:

    Yeah I will try out the unchanged one. The shooting logic I’m pretty sure I kept in place though but I will check. Just noticed that when shooting, the projectiles seem to slow down and/or stutter the screen until they are removed and complete their animations off screen.

    Tilemaps in general seem to be crushing my performance so I have been fighting that for a while trying to find a good solution. The way it seemingly takes the entire map into memory even if not displayed is the main problem I think and just have to find a good way to combat that.

    • Dad Says:

      Hi Jeff,
      You didn’t mention what device you’re on…

      What size are your maps? I’m using 50×50 here and it’s working pretty well. 60fps on the 3GS, ~51 on the iPad. In my case I also have another layer on top doing the obscuring of the entire map and then showing what the player has visited, with a ‘fog of war’ effect for places they’ve visited but that are not near any player currently. Also animating the player movement and scrolling. No projectiles in my prototype yet though.

      Are you calling
      -(CCSprite*) tileAt:(CGPoint)tileCoordinate;
      anywhere?

      From what I recall seeing when reading the cocos2d code, the way the tilemap is implemented for efficiency it doesn’t use full sprites for each tile unless you call the above routine, at which point it creates a CCSprite and adds it as a child to the layer.

      Therefore, if you do this a bunch, I suspect you will see performance start to go down fast. Haven’t tried it, but that’s my guess.

      Also, how many projectiles can be flying around at once? This is a mobile platform and you just might be asking too much of it. Game design can ameliorate this somewhat – ninja starts have to be pulled from sheaths, fireball spells take a moment to “recharge”, etc. :-) The other thing is to establish a ‘range’ for projectiles and not automatically let them fly to the edge of the screen. This lets you dispose of them more quickly and thus have fewer flying in the air at once.

      Logging to the console in the code to make sure things are getting cleaned up is helpful.

      Well, I’m new to this game dev stuff, so not sure how useful any of that will be – just guessing based on reading the cocos2d code and 22+ years of professional non-game mac/iphone dev.

      • Jeff Says:

        Thanks a lot for responding and looking into this… I’m running a 3G with iOS4. I have run a test using a custom tilemap that is 100×100, but now am using the exact tilemap in your example to test.

        I believe I am using -(CCSprite*) tileAt:(CGPoint)tileCoordinate;, but not sure how much. (I don’t have my Macbook here to check.)

        Basically the FPS is a decent 45+ most the time until shooting prjoectiles. Then it can drop way down into the single digits until the projectiles finish moving offscreen. So I think first stop I will write something to destroy them when they leave the visible screen.

        In an early tutorial by Ray I remember him using “onTouchesEnded” instead of the single touch. Do you think this could have anything to do with it? When I get home I can try using the other method to see but was just curious.

        Anyway, thanks a lot for putting this together and for looking into my questions. I really appreciate it and it is helping me make progress for sure.

  13. Some rough iPad OpenGL Performance and Techniques notes… « Geek And Dad's Blog Says:

    [...] since we are only playing around with 2d games at this point. Since this post is coming out after Geek’s tutorial on cocos2d-iphone tilemaps (a part 3 to Ray Wenderlich’sgreat set of tutorials), any reader following along already [...]

  14. I wanna make an iPhone game…now what? « iPhone Friend Says:

    [...] and Dad. This tutorial brought a lot of things together for me and is a continuation of Ray Wenderlich’s “How [...]

  15. Adrian Roman Says:

    Thank you for the tutorial.
    I think I found a bug in orienting the projectiles. Here is the modified code:

    	CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png"];
    	projectile.position = _player.position;
    	[self addChild:projectile];
    	
    	// Determine where we wish to shoot the projectile to
    	float realX;
    	
    	// Are we shooting to the left or right?
    	CGPoint diff = ccpSub(touchLocation, _player.position);
    	if (diff.x > 0)
    	{
    		realX = (_tileMap.mapSize.width * _tileMap.tileSize.width)  + (projectile.contentSize.width/2.);
    	} else {
    		realX = -(_tileMap.mapSize.width * _tileMap.tileSize.width) - (projectile.contentSize.width/2.);
    	}
    	float ratio = ((float) diff.y) / (float) diff.x;
    // HERE WAS THE PROBLEM !!!!
    	float realY = ((realX - projectile.position.x) * ratio) + projectile.position.y;
    	CGPoint realDest = ccp(realX, realY);
    	
    	// Determine the length of how far we're shooting
    	int offRealX = realX - projectile.position.x;
    	int offRealY = realY - projectile.position.y;
    	float length = sqrtf((offRealX*offRealX) + (offRealY*offRealY));
    	float velocity = 240;
    	//480/1; // 480pixels/1sec
    	float realMoveDuration = length/velocity;
    	
    	// Move projectile to actual endpoint
    	id actionMoveDone = [CCCallFuncN actionWithTarget:self
    	 selector:@selector(projectileMoveFinished:)];
    	[projectile runAction:
    	 [CCSequence actionOne:
    	  [CCSpawn actions:[CCMoveTo actionWithDuration:realMoveDuration position: realDest],
    	                   [CCRotateBy actionWithDuration: realMoveDuration angle: 720],
    	                   nil]
    			   two: actionMoveDone]];
    	
    	 [_projectiles addObject:projectile];	
    
    

    It also have a change to rotate the projectile as it moves.

  16. CyberGreg Says:

    Guys – awesome work! I had been struggling how to implement hit points in my game (instead of bullets think of a floating score moving up) and your code helped me realize a solution – many thanks!!!

  17. Marinus Says:

    Thanks a lot for this comprehensive tutorial. You just saved me a lot of time!!

    Marinus

  18. Yann Says:

    Hello, thank you for your excellent tutorials !!!
    Unfortunately I have a problem with this one when I use my own .png images to create a tiles. With those supplies no problem, but when I load a personal picture I obtain this error message in console :
    CCTexture2D. Can’t create Texture. UIImage is nil
    Couldn’t add image : /users/yannouss/library/Application Support/xxxxxxxxxx/59B8Axxxxxxxxxxxx/image.png in CCTextureCache
    Application ‘TileGame’ exited abnormally with signal 8: Floating point exception

    I try to edit images with fireworks or paintbrush, but the result is the same. The paths are correct in the file. Tmx and my images are not larger than the tutorial. Can you help me? thank you in advance.

    Yann.

    • Dad Says:

      Hi Yann. I don’t really know what the problem is. Not seen anything like what you describe.

      “paintbrush” ? isn’t that a Windows paint application? I wonder if there is a byte-order problem of some kind. Try opening the file in Preview.app on the mac, and then saving it as a copy. (If it doesn’t open in Preview then you know the .png file isn’t really a .png file or is corrupted).

      Here’s an interesting test you might do: copy the included image, then do a very small edit to the copy in the same program you are using for the one’s you’ve been creating. Then use that copy in the program – does that work?

    • John Says:

      Not sure if you got this sorted but I know what your problem is, you need to find this in your .tmx file

      /users/yannouss/library/Application Support/xxxxxxxxxx/59B8Axxxxxxxxxxxx/image.png

      and change it to

      image.png

      Then in future when you create your maps reference the images after they have been copied into your resources folder.

      Cheers

  19. Yann Says:

    Thank you for your fast response.
    In fact I use firework under windows and I transfer the image to macOs (under a Virtual Machine). That’s why I did a test with paintbrush is an application that runs on MacOS, but the result is the same. In both cases the image does not appear in the viewer macOs … I have the impression that png images are not generated in the same way as the editing tool.
    I’ll do your test tonight, now I’m at work.
    Can you recommend a simple and free .png editor for mac? It’s an environment I do not know well.
    Again thank you anyway, I am experienced developer on windows but not mac / iphone and your tutorials help me a lot to begin.

    Yann.

    • Dad Says:

      Acorn from http://flyingmeat.com is pretty darn good and they now have a Free version so you are in luck! If you like it, please consider purchasing the paid version as soon as you can come up with the money because that’s how we keep the small independent software developer alive and making us good stuff!

    • Dad Says:

      oh, and it was my son, the “Geek” of Geek & Dad, who did this tutorial, so I’ll pass on your thanks for it to him. I’m just handling the comments because he’s off building a game (actually several, an iPhone game, a trading card game, and a set of simplified rules for an pen and paper RPG game).

  20. Stephen Pearson Says:

    Hi,

    Thanks for doing these tutorials I am finding them so helpful in learning iphone dev and cocos2d. Do you have any more plans in create some more tutorials to include different levels.

    Thanks

    • Dad Says:

      Hi Stephen, Thanks, glad you found the tutorial helpful. Geek wrote it and he has added levels to the game he built starting from the code in the tutorial. However, it takes quite a bit of work and time to put together a tutorial and he’s been so busy that he hasn’t had time. I’ll let him know that a tutorial showing how to do levels would be appreciated.

    • Geek Says:

      Hi Stephen,
      I’m glad you liked the tutorial. I’m sorry, but I’m not planning any more tutorials at this time. However, Ray Wenderlich, who wrote the first two tutorials in this series, also wrote many other tutorials. Although they are not about tile-based games specifically, one series includes a tutorial on making multiple levels. I hope that you find what you want there.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

Join 1,166 other followers

%d bloggers like this: