Hey, welcome back to yet another installment of the “multitouch asteroids” tutorial series. If you haven’t been following along, you can always go back and review part one or part two. This tutorial will focus on fleshing out the asteroids-style game that we created, including making both a title scene and “how to play” scene, as well as storing player high scores.
As it stands right now, starting our app takes the player directly into the action without any warning. In addition, there’s no explanation of the controls, which would definitely be confusing for the first-time player. An easy way to fix this problem would be to create an introductory title screen, that shows off the name of the game and has buttons that start the game, view instructions, or view the high scores.
We’re going to be a bit forward thinking and create three new layer classes all at the same time, then go back and fill in the implementation details later. Open up your project in Xcode, right-click the Classes group, then select Add > New File. Make the new file a subclass of CCLayer, and name it “TitleScene.m”. Now go ahead and do the same thing two more times, naming each new source file “SourceScene.m” and “ControlsScene.m”, respectively. In each of the three new header files, add a + (id)scene class method declaration, between the @interface and @end statements so the code looks like this:
// TitleScene.h
@interface TitleLayer : CCLayer { }
+ (id)scene;
@end
// ControlsScene.h
@interface ControlsLayer : CCLayer { }
+ (id)scene;
@end
// ScoresScene.h
@interface ScoresLayer : CCLayer { }
+ (id)scene;
@end
Next, go into each of the new .m files, and add the following code between @implementation and @end, which will create a generic CCScene, then add your layer to it.
+ (id)scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object.
// Be sure to specify the "ScoresLayer" class in "ScoresScene.m", etc.
ControlsLayer *layer = [ControlsLayer node];
// add layer as a child to scene
[scene addChild:layer];
// return the scene
return scene;
}
- (id)init
{
if ((self = [super init]))
{
// Code goez here
}
return self;
}
These will be your basic steps every time you want to add a new scene/layer to a cocos2d project. Now, let’s focus on the title scene. We’ll probably want to have a large bit of text that displays the name of the game, as well as the buttons necessary to navigate through the different scenes we create. This is the pattern that we’ll follow for the high scores scene as well as the game controls scene. Add the following in the init method of the title scene class, after the “code goez here” comment.
// Get window size
CGSize windowSize = [CCDirector sharedDirector].winSize;
// Create text label for title of game - "@stroids" - don't sue me Atari!
CCLabelTTF *title = [CCLabelTTF labelWithString:@"@stroids" fontName:@"Courier" fontSize:64.0];
// Position title at center of screen
[title setPosition:ccp(windowSize.width / 2, windowSize.height / 2)];
// Add to layer
[self addChild:title z:1];
This just puts a big TrueType text label smack in the center of the screen with the title of the game. Next we’ll want to create three buttons that link to the different areas of the game — the “how to play” scene, the high scores scene, and the actual game itself.
// Set the default CCMenuItemFont font
[CCMenuItemFont setFontName:@"Courier"];
// Create "play," "scores," and "controls" buttons - when tapped, they call methods we define: playButtonAction and scoresButtonAction
CCMenuItemFont *playButton = [CCMenuItemFont itemFromString:@"play" target:self selector:@selector(playButtonAction)];
CCMenuItemFont *scoresButton = [CCMenuItemFont itemFromString:@"scores" target:self selector:@selector(scoresButtonAction)];
CCMenuItemFont *controlsButton = [CCMenuItemFont itemFromString:@"controls" target:self selector:@selector(controlsButtonAction)];
// Create menu that contains our buttons
CCMenu *menu = [CCMenu menuWithItems:playButton, scoresButton, controlsButton, nil];
// Align buttons horizontally
[menu alignItemsHorizontallyWithPadding:20.0];
// Set position of menu to be below the title text
[menu setPosition:ccp(windowSize.width / 2, title.position.y - title.contentSize.height / 1.5)];
// Add menu to layer
[self addChild:menu z:2];
I’m being lazy here and just creating text-based buttons. An exercise for the reader might be to create some nifty-looking graphical buttons to use instead. You can see the first method I call is to set the font that will be used for subsequent CCMenuItemFont buttons. Next, the three buttons are instantiated, added to a menu, aligned horizontally, then added to the layer. You can see that each of these buttons calls a different method when it is tapped, so let’s create those methods now. After the init method in TitleScene.m, add the following:
- (void)playButtonAction
{
NSLog(@"Switch to GameScene");
[[CCDirector sharedDirector] replaceScene:[GameLayer scene]];
}
- (void)scoresButtonAction
{
NSLog(@"Switch to ScoresScene");
[[CCDirector sharedDirector] replaceScene:[ScoresLayer scene]];
}
- (void)controlsButtonAction
{
NSLog(@"Switch to ControlsScene");
[[CCDirector sharedDirector] replaceScene:[ControlsLayer scene]];
}
These are simple methods that just switch the active scene class running in the app. Make sure to #import the GameLayer.h, ScoresLayer.h and ControlsLayer.h headers at the top of the file, otherwise you’ll get errors because you tried to create an object the current scene knows nothing about.
The last thing we’re going to do in the TitleScene class is initialize the high scores data structure, which will be stored using NSUserDefaults. This is kind of a hacky way to store scores, but it’s easy and it works, so why not?
// Place the following at the end of the init method in TitleScene.m
// Get user defaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// Register default high scores - this could be more easily done by loading a .plist instead of manually creating this nested object
NSDictionary *defaultDefaults = [NSDictionary dictionaryWithObject:[NSArray arrayWithObjects:[NSNumber numberWithInt:0], [NSNumber numberWithInt:0], [NSNumber numberWithInt:0], [NSNumber numberWithInt:0], [NSNumber numberWithInt:0], nil] forKey:@"scores"];
[defaults registerDefaults:defaultDefaults];
[defaults synchronize];
The NSUserDefaults object is like an NSDictionary, in which you can store various values associated with a “key.” Here, I’m storing an NSArray (filled with zeros) associated with the key “scores.” The thing to remember here is that this zero’d out array is only used the first time the app is launched, or if the NSUserDefaults are otherwise erased. As you may be able to tell by now, we’re going to store the top five high scores. Of course, the first time the game is played, the top scores will all be zero.
Next, let’s delve into the ControlsScene class. Not too much new here… we’ll just create a few labels that explain how to play the game, along with a button that takes the player back to the title screen.
// Goes in the init method of ControlsScene.m
// Get window size
CGSize windowSize = [CCDirector sharedDirector].winSize;
// Create title label
CCLabelTTF *title = [CCLabelTTF labelWithString:@"how to play" fontName:@"Courier" fontSize:32.0];
[title setPosition:ccp(windowSize.width / 2, windowSize.height - title.contentSize.height)];
[self addChild:title];
// Brief description ov how to control the game:
// Tap = Shoot
// Pinch = Rotate
// Swipe = Move
// Create label that will display the controls - manually set the dimensions due to multi-line content
CCLabelTTF *controlsLabel = [CCLabelTTF labelWithString:@"tap = shoot\npinch = rotate\nswipe = move" dimensions:CGSizeMake(windowSize.width, windowSize.height / 3) alignment:CCTextAlignmentCenter fontName:@"Courier" fontSize:16.0];
[controlsLabel setPosition:ccp(windowSize.width / 2, windowSize.height / 2)];
[self addChild:controlsLabel];
// Create button that will take us back to the title screen
CCMenuItemFont *backButton = [CCMenuItemFont itemFromString:@"back" target:self selector:@selector(backButtonAction)];
// Create menu that contains our buttons
CCMenu *menu = [CCMenu menuWithItems:backButton, nil];
// Set position of menu to be below the scores
[menu setPosition:ccp(windowSize.width / 2, controlsLabel.position.y - controlsLabel.contentSize.height)];
// Add menu to layer
[self addChild:menu z:2];
Also, make sure to create the backButtonAction method which will return the player back to the title screen. This method will go after init but before the @end of the class implementation.
- (void)backButtonAction
{
NSLog(@"Switch to TitleScene");
[[CCDirector sharedDirector] replaceScene:[TitleLayer scene]];
}
The ScoresScene class will be almost exactly the same as ControlsScene, with the notable exception of displaying the stored high scores instead of a static string of instructions.
// Put the following in the init method of ScoresLayer
// Get window size
CGSize windowSize = [CCDirector sharedDirector].winSize;
// Get scores array stored in user defaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// Get high scores array from "defaults" object
NSArray *highScores = [defaults arrayForKey:@"scores"];
// Create title label
CCLabelTTF *title = [CCLabelTTF labelWithString:@"high scores" fontName:@"Courier" fontSize:32.0];
[title setPosition:ccp(windowSize.width / 2, windowSize.height - title.contentSize.height)];
[self addChild:title];
// Create a mutable string which will be used to store the score list
NSMutableString *scoresString = [NSMutableString stringWithString:@""];
// Iterate through array and print out high scores
for (int i = 0; i < [highScores count]; i++)
{
[scoresString appendFormat:@"%i. %i\n", i + 1, [[highScores objectAtIndex:i] intValue]];
}
// Create label that will display the scores - manually set the dimensions due to multi-line content
CCLabelTTF *scoresLabel = [CCLabelTTF labelWithString:scoresString dimensions:CGSizeMake(windowSize.width, windowSize.height / 3) alignment:CCTextAlignmentCenter fontName:@"Courier" fontSize:16.0];
[scoresLabel setPosition:ccp(windowSize.width / 2, windowSize.height / 2)];
[self addChild:scoresLabel];
// Create button that will take us back to the title screen
CCMenuItemFont *backButton = [CCMenuItemFont itemFromString:@"back" target:self selector:@selector(backButtonAction)];
// Create menu that contains our buttons
CCMenu *menu = [CCMenu menuWithItems:backButton, nil];
// Set position of menu to be below the scores
[menu setPosition:ccp(windowSize.width / 2, scoresLabel.position.y - scoresLabel.contentSize.height)];
// Add menu to layer
[self addChild:menu z:2];
The real wonky line in this code is [scoresString appendFormat:@"%i. %i\n", i + 1, [[highScores objectAtIndex:i] intValue]];. This appends additional text to the end of the mutable string that is used to display the high scores. highScores is the NSArray stored in the NSUserDefaults that stores the scores. An NSArray can only hold objects derived from NSObject, so that's why we wrap each number with NSNumber before putting it in the array. To get a regular integer from an NSNumber, you use the intValue method; e.g. int myNumber = [myNSNumber intValue];. Finally, don't forget to add the backButtonAction method to the ScoresLayer class after the init method.
- (void)backButtonAction
{
NSLog(@"Switch to TitleScene");
[[CCDirector sharedDirector] replaceScene:[TitleLayer scene]];
}
OK, so we're getting close to finishing the improvements that make this project seem more like "finished" game. When we left off programming the actual game class, the player could play indefinitely. We'll make a modification to the GameScene class so that a "game over" message is displayed when the player's ship is destroyed, and their score will be saved if its' high enough. Open up GameScene.h and add - (void)gameOver; at the bottom of the class method declaration list. Then open GameScene.m and add the implementation:
- (void)gameOver
{
// Reset the ship's position, which also removes all bullets
[self resetShip];
// Hide ship
ship.visible = NO;
// Get window size
CGSize windowSize = [CCDirector sharedDirector].winSize;
// Show "game over" text
CCLabelTTF *title = [CCLabelTTF labelWithString:@"game over" fontName:@"Courier" fontSize:64.0];
// Position title at center of screen
[title setPosition:ccp(windowSize.width / 2, windowSize.height / 2)];
// Add to layer
[self addChild:title z:1];
// Create button that will take us back to the title screen
CCMenuItemFont *backButton = [CCMenuItemFont itemFromString:@"back to title" target:self selector:@selector(backButtonAction)];
// Create menu that contains our button
CCMenu *menu = [CCMenu menuWithItems:backButton, nil];
// Set position of menu to be below the "game over" text
[menu setPosition:ccp(windowSize.width / 2, title.position.y - title.contentSize.height)];
// Add menu to layer
[self addChild:menu z:2];
// Get scores array stored in user defaults
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// Get high scores array from "defaults" object
NSMutableArray *highScores = [NSMutableArray arrayWithArray:[defaults arrayForKey:@"scores"]];
// Iterate thru high scores; see if current point value is higher than any of the stored values
for (int i = 0; i < [highScores count]; i++)
{
if (points >= [[highScores objectAtIndex:i] intValue])
{
// Insert new high score, which pushes all others down
[highScores insertObject:[NSNumber numberWithInt:points] atIndex:i];
// Remove last score, so as to ensure only 5 entries in the high score array
[highScores removeLastObject];
// Re-save scores array to user defaults
[defaults setObject:highScores forKey:@"scores"];
[defaults synchronize];
NSLog(@"Saved new high score of %i", points);
// Bust out of the loop
break;
}
}
}
This method will get called when an asteroid runs into the ship, so in the update method in the big loop that cycles through all the asteroid objects, the first if conditional will be changed to look like this:
// Check for collisions vs. asteroids
for (int i = 0; i < [asteroids count]; i++)
{
Asteroid *a = [asteroids objectAtIndex:i];
// Check if asteroid hits ship
if ([a collidesWith:ship])
{
// Game over, man!
[self gameOver];
}
// ... rest of loop here
}
The last thing you'll have to do is change the AppDelegate so that the app gets launched with the TitleScene class instead of the GameScene class. Add #import "TitleScene.h" to the top of the AppDelegate file, and then change the last line in the applicationDidFinishLaunching method.
- (void) applicationDidFinishLaunching:(UIApplication*)application
{
// ... other cocos2d init stuff here
[[CCDirector sharedDirector] runWithScene: [TitleLayer scene]];
}
Try building and running the app to see these changes in effect. The game could still be polished further, but it's looking a heck of a lot better than where we left it at the end of the previous tutorial. At this point you could theoretically put it out on the App Store (albeit for free, since it's pretty bare bones). Feel free to experiment with the code that you have... add your own graphics, or maybe an alien ship or powerups. You can download the Xcode project for reference. And make sure to tune in to the final part of the tutorial, where we'll add some particle systems and sound effects to make the game even more interesting.
· Posted by nathan in Programming, Tutorial · cocos2d, games, objective-c, programming, tutorial, ui