In this series, we will be recreating the popular Atari game Centipede using the Cocos2D game engine for iOS. This is the second installment, where you will learn how to setup the game board and populate it with sprites.
Where We Left Off...
In the first tutorial in this series, I showed you how to download, install, and create a new Cocos2D project. I also showed you how you can prepare your assets using Texture Packer and load them into your game.
Today, I will go into more detail about setting up the user interface for the game as well as drawing the game board.
Step 1: The Sprout Field - Game Object
In the traditional version of the centipede game, mushrooms were you used to define the play area of the centipede. We are going to use that same idea and create what I'm calling sprouts.
Before we can begin talking about any of the "in-game objects", we need to define a superclass that will contain some of the common methods and properties that every object will need. This will save us some redundant code in the future.
Create a new file called GameObject.m and, in the header file, write the following code:
#import "cocos2d.h" #import "GameLayer.h" @interface GameObject : NSObject @property(nonatomic, retain) CCSprite *sprite; @property (nonatomic, assign) GameLayer *gameLayer; @property(nonatomic) CGPoint position; - (id)initWithGameLayer:(GameLayer *)layer; - (CGRect)getBounds; @end
Here is a brief description of each of the properties in here. I'll go into detail about the methods when I show you the implementation.
- sprite: Every object in the game will need something to visually represent it. This will be the sprite/image object used to draw _this_ object.
- gameLayer: Simply a reference to the main game layer. This will be useful for responding to collisions as well as letting objects add their own sprites to the drawing area.
- position: The location of this object in the game world.
Now, let's take a look at the implementation. Open GameObject.m and add the following code:
#import "GameObject.h" @implementation GameObject @synthesize sprite = _sprite; @synthesize gameLayer = _gameLayer; @synthesize position = _position; - (void)dealloc { [_sprite release]; [super dealloc]; } - (id)initWithGameLayer:(GameLayer *)layer { if(self = [super init]) { self.gameLayer = layer; } return self; } // 1 - (void) setPosition:(CGPoint)position { _position = position; _sprite.position = position; } // 2 - (CGRect)getBounds { CGSize size = [self.sprite contentSize]; return CGRectMake(self.position.x - size.width * self.sprite.anchorPoint.x, self.position.y - size.height * self.sprite.anchorPoint.y, size.width, size.height); } @end
1. Since our object's position is different than the sprite's postion, we need to overwrite the setter for the position property to ensure that the sprite gets moved on the screen.
2. This method will be used a little later when we implement our collision detection. It bascially determines the bounding box for our object and returns it.
Step 2: The Sprout Field - Sprout Object
Now that we have the GameObject in place, the Sprout object becomes quite simple. The only purpose for the sprouts is to serve as an obstacle for the player and the centipede.
Create a new GameObject subclass called Sprout. Again, we need to subclass GameObject in order to get some of those boilerplate properties and methods.
Open up Sprout.h and add the following code:
#import "GameObject.h" @interface Sprout : GameObject @property (nonatomic, assign) NSInteger lives; @end
The only addition the Sprout object makes to the GameObject is that it has a lives
property. When the player shoots the sprout we want it to get hit a certain number of times (in this case 3) before it disappears.
The implementation of a Sprout object is also quite simplistic. Add the following code to Sprout.m.
#import "Sprout.h" #import "GameConfig.h" @implementation Sprout @synthesize lives = _lives; - (id) initWithGameLayer:(GameLayer *)layer { // 1 if(self = [super initWithGameLayer:layer]) { // 2 self.sprite = [CCSprite spriteWithSpriteFrameName:@"sprout.png"]; // 3 [self.gameLayer.spritesBatchNode addChild:self.sprite]; // 4 self.lives = kSproutLives; } return self; } // 5 - (void)setLives:(NSInteger)lives { _lives = lives; self.sprite.opacity = (_lives / 3.0) * 255; } @end
1. Since we are overwriting the initWithGameLayer
method, we need to call the super class' version of it as well. This will ensure that everything gets set up properly.
2. This sets up our current sprite to a sprite coming out of our CCSpriteBatchNode
with the name sprout.png.
3. Adds the sprite to the batch node layer to be drawn.
4. Sets the number of lives of the sprout to the kSproutLives
configuration setting (to be defined in a moment)
5. We want the user to be provided with some feedback when they hit a sprout and some idicator as to how much health is left. I have done this by dropping the opacity by a third ever time it has been hit.
As I mentioned in 4, we need to set up some basic game configuration. Luckily, Cocos2D has provided a file called GameConfig.h to do just that. Open it up and add this line:
#define kSproutLives 3
Now that we have created a basic Sprout object, we can use this to generate an entire field for our game.
Step 3: The Sprout Field - Random Locations
The map for our game will be generated at random with a minimum amount of sprouts in the game at any given time. To do this, we need to keep track of where we have already placed sprouts. Also, the number of sprouts that get added to the map will be based on the current level: the more sprouts on the map, the harder the game is for the player. So, open up GameLayer.h and add an ivar and two properties. I'm only going to show the additions rather than reposting the code that we previously wrote.
#import "GameConfig.h" @interface GameLayer : CCLayer { // 1 BOOL _locations[kRows][kColumns]; } // 2 @property (nonatomic, assign) NSInteger level; @property (nonatomic, retain) NSMutableArray *sprouts // ...Other properties...
1. This is a 2D array of boolean values used to denote where on the grid we have already placed Sprout objects. True means one exists and false means one doesn't.
2. This is the current level of the game. Many other events will rely on this property as well. The other property holds the array of Sprout objects currently in play. Make sure to @synthesize
these properties in the implementation file as well as release the sprouts array. This is the last time I'll mention synthesize and release for properties. From now on, I'm going to assume you already know that! ;)
One other thing to note is you need to import GameConfig.h in order to access the kRows and kColumns constants. Don't worry about these for now, we will define them a little later.
Now open up GameLayer.m. Start by adding a private interface for GameLayer at the top so that we can define a few methods that we will be using. Also, be sure to add the import statement for Sprout.h as we will be making heavy use of it here.
Above the @implementation
line, add the following code:
#import "Sprout.h" @interface GameLayer(Private) - (CGPoint)randomEmptyLocation; - (void)placeRandomSprout; @end
These are two helper methods we will be using in order to set up our field. I will go into a little more detail about them during implementation.
Now let's add the following code to the bottom of your init method (inside of the if( (self=[super init]))
statement braces.
// 1 self.level = 1; // 2 for(int i = 0; i < kRows; i++) { for(int j = 0; j < kColumns; j++) { _locations[i][j] = NO; } } // 3 srand(time(NULL)); _sprouts = [[NSMutableArray alloc] init]; for(int i = 0; i < kStartingSproutsCount; i++) { // 4 [self placeRandomSprout]; }
1. Start the game off at level 1
2. Loop over _locations
, initialize them all, and set them to NO
3. Loop kStartingSproutsCount
times and place random sprouts on the grid
This code introduces a few more constants that we have yet to define. Before we move on, let's define these constants and a few more in our GameConfig.h file.
#define kGridCellSize 16 #define kColumns 18 #define kRows 20 #define kStartingSproutsCount 50 #define kGameAreaStartX 24 #define kGameAreaStartY 64 #define kGameAreaHeight 353 #define kGameAreaWidth 288
The last step here is to implement the two methods that we defined above. Add the following methods to your GameLayer.m file:
- (void)placeRandomSprout { // 1 Sprout *sprout = [[[Sprout alloc] initWithGameLayer:self] autorelease]; CGPoint p = [self randomEmptyLocation]; // 2 sprout.position = ccp(p.x * kGridCellSize + kGameAreaStartX, (kGameAreaStartY - kGridCellSize + kGameAreaHeight) - p.y * kGridCellSize - kGridCellSize/2); // 3 [self.sprouts addObject:sprout]; } - (CGPoint) randomEmptyLocation { int column; int row; BOOL found = NO; // 1 while(!found) { // 2 column = (arc4random() % kColumns); row = (arc4random() % kRows); // 3 if(!_locations[row][column]) { found = YES; _locations[row][column] = YES; } } // 4 return CGPointMake(column, row); }
placeRandomSprout
1. We start by initializing a new Sprout object and fetching a random location to put it. At this stage, the location is in grid space, we need to convert it to world space in order to draw it.
2. We do some basic math here to map this sprout from grid space to world space based on the size of the play area and cell size.
3. Finally, the Sprout object gets added to the array of live sprouts
randomEmptyLocation
1. This loop will not complete until we find an empty location. I guess there _could_ be a time when there are no locations available and it will continue forever, however by that stage, the game will be at some ridiculous speed in which no player _should_ be able to play. An optimization might be to put a check in here to ensure that every space isn't filled first.
2. Pick a random row and column
3. Check if a sprout exists at that location, if not, update the _locations
ivar and notify the loop to break.
4. Finally, we just return the new point that was created.
At this point, if you run your code you should see the sprout field created and spread around the play area. I have also made it so sprouts never spawn in the first or last rows to make things interesting for the player and caterpillar.
Your game should now look something like this:
Step 4: The Player Object
The next logical step in creating our interface is to build the Player object. The player object will again be extremely simply thanks to the help of our super class.
Create a new GameObject subclass called player and paste the following code into the Player.h file.
#import "GameObject.h" @interface Player : GameObject @property(nonatomic) NSInteger lives; @property(nonatomic) NSInteger score; @end
A player adds lives and score to the base GameObject. We will use these properties to display the remaining lives as well as the score in the next section.
Now, open up Player.m and add the following code:
#import "Player.h" #import "GameLayer.h" @implementation Player @synthesize score = _score; @synthesize lives = _lives; - (id)initWithGameLayer:(GameLayer *)layer { if(self = [super initWithGameLayer:layer]) { self.sprite = [CCSprite spriteWithSpriteFrameName:@"player.png"]; [self.gameLayer.spritesBatchNode addChild:self.sprite]; } return self; } @end
The initWithGameLayer
method for the Player is exactly the same as the Sprout's method, except it adds a different sprite to the game.
That's all for the Player object. The final step is to initialize the player and add him to the game.
We must first declare a Player object in GameLayer.h. Make sure you also import the Player object here too.
@property (nonatomic, retain) Player *player;
Now, add the following code directly below the code you wrote in the last section the init method:
_player = [[Player alloc] initWithGameLayer:self]; _player.lives = kPlayerStartingLives; _player.position = ccp(kGameAreaStartX + (kGameAreaWidth / 2), 88); _player.score = 0;
Again, this introduces a constant that we need to add to GameConfig.h
#define kPlayerStartingLives 3
If you run the game at this point, you should now see the red player square added to the game.
Step 5: Displaying The Player's Lives
We need to give the player some sort of feedback as to how many lives they have remaining. The way I have chosen to implement this is to display the player's sprite in the upper left corner. So, if the player has 3 lives, it will display 3 red squares.
There are a few ways to handle this requirement. I have chosen to maintain an array of player sprites and use that to manage the display of the player's life count. Start by adding (you guessed it) another property to the GameLayer class called livesSprites:
@property(nonatomic, retain) NSMutableArray *livesSprites;
Next, add the following code to the init method:
// init live sprites _livesSprites = [[NSMutableArray alloc] init]; // Register for lives notifications [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateLives:) name:kNotificationPlayerLives object:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationPlayerLives object:nil];
Notice that I have added the GameLayer as an observer to a notification named kNotificationPlayerLives. Since we might not be decrementing the player's life inside of the GameLayer, we need a way to make the UI respond when they player gets hit. This is the easiest way to make that happen. Also notice that I am posting the notification right away. Since this method is responsible for drawing the player's lives, we need to call it during init to display the lives initially. This also adds another constant called kNotificationPlayerLives. Make sure you add it to your GameConfig.h file. We are going to do the same thing with player score in a moment, so you might as well add the constants for both:
#define kNotificationPlayerLives @"PlayerLivesNotification" #define kNotificationPlayerScore @"PlayerScoreNotification"
Now, we are going to implement the updateLives
method in our code. This method will be responsible for drawing the lives of the player and eventually transitioning to the game over screen when the player's lives go to 0. Add the following method to your GameLayer.m file:
- (void) updateLives:(NSNotification *) notification { NSInteger lifeCount = self.player.lives; // 1 [self.livesSprites enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [self.spritesBatchNode removeChild:obj cleanup:YES]; }]; [self.livesSprites removeAllObjects]; // 2 for(int i = 0; i < lifeCount; i++) { CCSprite *sprite = [CCSprite spriteWithSpriteFrameName:@"player.png"]; sprite.position = ccp(kGameAreaStartX + (i * kGridCellSize * 2), 435); [self.livesSprites addObject:sprite]; [self.spritesBatchNode addChild:sprite]; } }
1. First, we completely clear out the livesSprites array and remove their sprites from the scene.
2. Next, we loop based on the life the player has left, create a new sprite, and add it to the scene.
Now that this is complete, you can Run your application and you should see the player's life along the top:
Step 6: Displaying The Player Score
This process is going to be very similar to what we just did with the player's lives, only we are going to be using a CCLabelTTF to display the player's score. CCLabelTTFs are used in Cocos2D to display text in your games.
Start by opening GameLayer.h and adding the following property:
@property(nonatomic, retain) CCLabelTTF *playerScoreLabel;
Next, go into the init method of GameLayer.m and initialize the label and notifications with the following code:
_playerScoreLabel = [[CCLabelTTF labelWithString:@"0" dimensions:CGSizeMake(100, 25) alignment:UITextAlignmentRight fontName:@"Helvetica" fontSize:18] retain]; _playerScoreLabel.position = ccp(254,435); [self addChild:_playerScoreLabel]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateScore:) name:kNotificationPlayerScore object:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationPlayerScore object:nil];
This code should look very straight forward given the explanation of the player's lives. The next step is to implement the updateScore
method to get the score to display. Add the following method to your GameLayer.m file:
- (void) updateScore:(NSNotification *) notification { [self.playerScoreLabel setString:[NSString stringWithFormat:@"%d",self.player.score]]; }
A CCLabelTTF works very similar to a UILabel in UIKit. You set the string and you are good to go! Now, whenever we update the score in the game, we simply post that score notification and the UI will update accordingly.
The final version of your game should look like this:
The project is really starting to look like a game now!
Next Time
The next tutorial will cover the most complex (and most fun) part of this series. We will be implementing the caterpillar object along with its AI.
Happy Coding!
Comments