This is a continuation of one of Ray Wenderlich’s excellent tutorialsfor 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:
- An enemy sprite.
- A ninja star sprite, from Ray’s How to Make A Simple iPhone Game with Cocos2D Tutorial.
- Two images for a button, used later in the tutorial.
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 Java 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.
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.
Creating the Enemies
Okay, now we’ll make the enemies actually appear on the map. Add the following code in HelloWorldScene.m:
//in the HelloWorld 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 NSMutableDictionary * spawnPoint; 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]; } }
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 HelloWorldScene.m, in the HelloWorld 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 thy are significantly easier than the alternatives, and they will work in the simulator (because they don’t need multi-touch). Because of thee 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 HelloWorldScene.h:
// at the top of the file add a forward declaration for HelloWorld, // because our two layers need to reference each other @class HelloWorld; // inside the HelloWorldHud class declaration HelloWorld *_gameLayer; // After the class declaration @property (nonatomic, assign) HelloWorld *gameLayer; // Inside the HelloWorld class declaration int _mode; // After the class declaration @property (nonatomic, assign) int mode;
And to HelloWorldScene.m:
// At the top of the HelloWorldHud implementation @synthesize gameLayer = _gameLayer; // At the top of the HelloWorld implementation @synthesize mode = _mode; // in HelloWorld's init method _mode = 0; // in HelloWorld'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 HelloWorldHud's init method // define the button CCMenuItem *on; CCMenuItem *off; on = [[CCMenuItemImage itemFromNormalImage:@"projectile-button-on.png" selectedImage:@"projectile-button-on.png" target:nil selector:nil] retain]; off = [[CCMenuItemImage itemFromNormalImage:@"projectile-button-off.png" selectedImage:@"projectile-button-off.png" target:nil selector:nil] retain]; 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 HelloWorldHud //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 HelloWorld.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 instance variables to the HelloWorld class (in HelloWorldScene.h):
NSMutableArray *_enemies; NSMutableArray *_projectiles;
Now, add the projectiles to the projectiles array with this code:
// at the end of the launch projectiles section of ccTouchEnded:withEvent: [_projectiles addObject:projectile]; // at the end of projectileMoveFinished: [_projectiles removeObject:sprite];
Next, add the following code to the end of addEnemyAtX:y:
[_enemies addObject:enemy];
Next, add the following method to the HelloWorld class:
- (void)testCollisions:(ccTime)dt { NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init]; // iterate through projectiles for (CCSprite *projectile in _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 _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) { [_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]; } [targetsToDelete release]; } // remove all the projectiles that hit. for (CCSprite *projectile in projectilesToDelete) { [_projectiles removeObject:projectile]; [self removeChild:projectile cleanup:YES]; } [projectilesToDelete release]; }
Finally, initialize the enemy and projectile arrays and schedule the testCollisions: method to be called as often as possible by adding this code to HelloWorld’s init method:
// you need to put these initializations before you add the enemies, // because addEnemyAtX:y: uses these arrays. _enemies = [[NSMutableArray alloc] init]; _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 Cocos2Dtutorial. 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 : CCColorLayer { CCLabel *_label; } @property (nonatomic, retain) CCLabel *label; @end @interface GameOverScene : CCScene { GameOverLayer *_layer; } @property (nonatomic, retain) GameOverLayer *layer; @end
Then replace the template code in GameOverScene.m with the following code:
#import "GameOverScene.h" #import "HelloWorldScene.h" @implementation GameOverScene @synthesize layer = _layer; - (id)init { if ((self = [super init])) { self.layer = [GameOverLayer node]; [self addChild:_layer]; } return self; } - (void)dealloc { [_layer release]; _layer = nil; [super dealloc]; } @end @implementation GameOverLayer @synthesize label = _label; -(id) init { if( (self=[super initWithColor:ccc4(255,255,255,255)] )) { CGSize winSize = [[CCDirector sharedDirector] winSize]; self.label = [CCLabel 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:[HelloWorld scene]]; } - (void)dealloc { [_label release]; _label = nil; [super dealloc]; } @end
The GameOverLayer just puts a label in the middle of the screen, and schedules a transition back to the HelloWorld 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 HelloWorld’s setPlayerPosition: method in HelloWorldScene.m, right after updating the score:
// put the number of melons on your map in place of the '2' if (_numCollected == 2) { [self win]; }
And create the win method in the HelloWorld class in HelloWorldScene.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 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 HelloWorld’s testCollisions: method, in HelloWorldScene.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 HelloWorld class in HelloWorldScene.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!
Download a zip file of the project here. 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