Check-a check it out! The game I've been working on for a while, Revolve Ball, has been approved by Apple and is available for general purchase on the App Store. I think that the game mechanic and controls are pretty fun, it's got 40 levels, and is only $0.99! Basically there's no reason not to buy it.
Check out the splash page I made for the game, or go directly to iTunes to start playing! Be sure to leave me a review.... I'll be publishing another update shortly with Game Center leaderboards, and would love to incorporate your feedback.
OK, so I had a minor crisis today as I realized that the music that I made with pxTone and encoded with Audacity was not looping correctly in cocos2d. Even though I carefully cropped my source .wav files, after converting them to .mp3 and loading them into my game, there would be about a half-second delay before the BGM would loop. Turns out that this is due to the way most MP3 encoders handle the format; they add extra padding to the beginning and end of the file.
I came across a forum post where the author of CocosDenshion linked to a post that explained the problem, but didn't offer a solution. I tried to use the utility he referenced, but couldn't get it to work. Then I tried to think why I hadn't run into this problem before. I realized that for Nonogram Madness, I made all my music with GarageBand. On a whim, I loaded my exported .wav into a new GarageBand project, then exported to MP3. It worked perfectly! There are a number of great things about this solution. First, GarageBand is included free with Macintosh computers, and you can get new versions for $5 from the Mac App Store. Second, I don't have to use Audacity, which I have not been a fan of so far.
Oh yeah, I'm going there: reviewing the #1 game on the App Store. Last Friday, during a company-wide meeting, one of my co-workers asked innocently, "So, has anyone played this 'Tiny Wings' game?" Only the entire internets, my friend. Only the entire internets. You've obviously played Tiny Wings, so this review is irrelevant as to whether or not you purchase the game. However, I figured I'd deconstruct it a little bit and try to explain (to myself, mostly) why it's so much more fun than similar games such as Canabalt.
Canabalt inspired a number of other games, and has probably created its' own "runner" genre. In Canabalt, you control a running man, and have to run as far as you can before you inevitably meet a grisly demise. The man runs forward constantly; the only way you can control him is by making him jump from platform to platform. As he runs farther, he also runs faster. The strategy behind a high score in the game is to run into debris that litters the platforms — they'll cause the man to stumble and slow down, which means that you have more time to react to upcoming platforms.
The problem with Canabalt, though, is that it is very unforgiving. Make a mistake, and you have to start all over again from the beginning. Tiny Wings has you controlling a bird that flies between hills. To progress faster through the game, you tap the screen to cause the bird to fall. Move your finger away to allow him to jump off the hills, then tap again to drop into a valley and jump even higher. It's a very satisfying movement mechanic — similar to the old-school Sonic series, in that the game character moves agonizingly slowly if he loses momentum. However, if you miss a jump, you don't instantly lose... you get to climb to the next hill and keep moving. This means that you can still progress in the game without being "perfect," and that a mistake early on won't ruin the whole play session.
Canabalt is also notable for having entirely procedurally-generated content. While this means that you never play the same game twice, it also ensures that the game is very "twitchy;" you never know what's coming ahead, and so fast reflexes are required for getting a high score. The levels in Tiny Wings aren't random, so by playing through a few times you'll be able to figure out the best way to fly faster or collect the most items. It also means that you won't get randomly screwed by the level-generating algorithm.
Tiny Wings has "achievements" — rewards for completing arbitrary goals in the game. Even if you aren't skilled enough to reach the final level, the achievements provide mini-goals that are able to be completed even by beginners. These provide a reason to come back to the game even when the initial fun of the core mechanic has been exhausted. In Canabalt, once you tire of endless jumping, the game is effectively over for you.
My final observation might be somewhat silly, but I think it makes a difference in the overall appeal of the two games. Canabalt has a bleak outlook, enhanced by the monochromatic graphics. No matter how well you play, your character always dies in the end. The bird in Tiny Wings falls asleep if night catches up with him. In my mind, part of the appeal of video games is that you can forget about the real world for a while. I don't need games reminding me about the futility of life and inevitability of death. +1 for Tiny Wings.
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:
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:
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.
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.
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.