Now that we have the tile animation added, now we should do something more flashy with the score itself.
Filename: MTPlayfieldLayer.m
-(void) animateScoreDisplay { // We delay for a second to allow the tiles to get // to the scoring position before we animate CCDelayTime *firstDelay = [CCDelayTime actionWithDuration:1.0]; CCScaleTo *scaleUp = [CCScaleTo actionWithDuration:0.2 scale:2.0]; CCCallFunc *updateScoreDisplay = [CCCallFunc actionWithTarget:self selector:@selector(updateScoreDisplay)]; CCDelayTime *secondDelay = [CCDelayTime actionWithDuration:0.2]; CCScaleTo *scaleDown = [CCScaleTo actionWithDuration:0.2 scale:1.0]; [playerScoreDisplay runAction:[CCSequence actions: firstDelay, scaleUp, updateScoreDisplay, secondDelay, scaleDown, nil]]; } -(void) updateScoreDisplay { // Change the score display to the new value [playerScoreDisplay setString: [NSString stringWithFormat:@"%i", playerScore]]; // Play the "score" sound [[SimpleAudioEngine sharedEngine] playEffect:SND_SCORE]; }
We finally settled on scaling the score up, change it to the new value, and scale it back to normal. This is all done with standard cocos2d actions, so we could add in more flair with other effects. A CCRotateTo
action might add a nice touch by spinning the score around when it updates. For this game, we will stick to this simpler animation. We leave it as a challenge to the reader to add these types of enhancements for more "visual flair."
Now we come to the point where we decide how the player can win or lose. You win after you have successfully matched all the tiles on the board. Losing is less obvious in a one-player game like this. Our approach is to give the player a number of lives. When you take a turn and fail to match the tiles, you lose a life. Lose all of them, and it's game over. The challenge comes from deciding how many lives the player should have. After testing several approaches, we determined the most exciting way would be to have the number of lives set to half the number of tiles currently on the board. If the board has 20 tiles in play, the player has 10 lives. Once the player makes a successful match, the lives are recalculated based on the new number of tiles in play. This gives some level of excitement as the lives are dwindling, and it encourages the player to think about their moves more carefully.
Filename: MTPlayfieldLayer.m
-(void) animateLivesDisplay { // We delay for a second to allow the tiles to flip back CCScaleTo *scaleUp = [CCScaleTo actionWithDuration:0.2 scale:2.0]; CCCallFunc *updateLivesDisplay = [CCCallFunc actionWithTarget:self selector:@selector(updateLivesDisplay)]; CCCallFunc *resetLivesColor = [CCCallFunc actionWithTarget:self selector:@selector(resetLivesColor)]; CCDelayTime *delay = [CCDelayTime actionWithDuration:0.2]; CCScaleTo *scaleDown = [CCScaleTo actionWithDuration:0.2 scale:1.0]; [livesRemainingDisplay runAction:[CCSequence actions: scaleUp, updateLivesDisplay, delay, scaleDown, resetLivesColor, nil]]; } -(void) updateLivesDisplayQuiet { // Change the lives display without the fanfare [livesRemainingDisplay setString:[NSString stringWithFormat:@"%i", livesRemaining]]; } -(void) updateLivesDisplay { // Change the lives display to the new value [livesRemainingDisplay setString:[NSString stringWithFormat:@"%i", livesRemaining]]; // Change the lives display to red [livesRemainingDisplay setColor:ccRED]; // Play the "wrong" sound [[SimpleAudioEngine sharedEngine] playEffect:SND_TILE_WRONG]; [self checkForGameOver]; } -(void) calculateLivesRemaining { // Lives equal half of the tiles on the board livesRemaining = [tilesInPlay count] / 2; } -(void) resetLivesColor { // Change the Lives counter back to blue [livesRemainingDisplay setColor:ccBLUE]; }
The preceding section of code looks very similar to the score methods. We leverage cocos2d actions to animate the lives display, only this time we also turn the text red when the number of lives is reduced, and then change it back to blue at the end of the CCSequence
of actions. One item of note here is the
updateLivesDisplayQuiet
method. This method is called when the player makes a successful match to let us change the lives to their new value without the "oh-no" fanfare that we use when the player loses a life.
We now have two game over conditions to consider. If livesRemaining
is zero, the player loses. If the
tilesInPlay
array is empty, the player has won. This feels like a good time to put the code together into a single method to check these conditions.
Filename: MTPlayfieldLayer.m
-(void) checkForGameOver { NSString *finalText; // Player wins if ([tilesInPlay count] == 0) { finalText = @"You Win!"; // Player loses } else if (livesRemaining <= 0) { finalText = @"You Lose!"; } else { // No game over conditions met return; } // Set the game over flag isGameOver = YES; // Display the appropriate game over message CCLabelTTF *gameOver = [CCLabelTTF labelWithString:finalText fontName:@"Marker Felt" fontSize:60]; [gameOver setPosition:ccp(size.width/2,size.height/2)]; [self addChild:gameOver z:50]; }
We have added extra flash and flair in the code, but we haven't tied it all together yet. Most of the new code is integrated into the
checkForMatch
method, so let's see how that looks with everything integrated:
Filename: MTPlayfieldLayer.m
-(void) checkForMatch { // Get the MemoryTiles for this comparison MTMemoryTile *tileA = [tilesSelected objectAtIndex:0]; MTMemoryTile *tileB = [tilesSelected objectAtIndex:1]; // See if the two tiles matched if ([tileA.faceSpriteName isEqualToString:tileB.faceSpriteName]) { // We start the scoring, lives, and animations [self scoreThisMemoryTile:tileA]; [self scoreThisMemoryTile:tileB]; [self animateScoreDisplay]; [self calculateLivesRemaining]; [self updateLivesDisplayQuiet]; [self checkForGameOver]; } else { // No match, flip the tiles back [tileA flipTile]; [tileB flipTile]; // Take off a life and update the display livesRemaining--; [self animateLivesDisplay]; } // Remove the tiles from tilesSelected [tilesSelected removeAllObjects]; }
Now we have a fully functional game, complete with scoring, lives, a way to win and a way to lose. There is only one necessary element still missing.
A major mistake some casual game designers make is to downplay the importance of audio. When you are playing a quiet game without the aid of a computer, there are always subtle sounds. Playing cards give a soft "thwap" sound when playing solitaire. Tokens in board games click as they tap their way around the board. Video games should have these "incidental" sound effects, too. These are the button clicks, the buzzers when something goes wrong, and so forth.
We will be using CocosDenshion
, the
audio engine that is bundled with cocos2d. CocosDenshion
includes a very easy to use interface appropriately named SimpleAudioEngine
. To initialize it, you need to import it into your classes (including the AppDelegate.m
file) and add one line near the end of
the application:didFinishLaunchingWithOptions:
method (before the return YES;
line).
Filename: AppDelegate.m
// Initialize the SimpleAudioEngine [SimpleAudioEngine sharedEngine];
For our implementation, we want to preload all of our sound effects so there is no lag the first time the sound effect is played. We do this with a method that is called from the initWithRows:andColumns:
method
of our MTPlayfieldLayer
.
Filename: MTPlayfieldLayer.m
-(void) preloadEffects { // Preload all of our sound effects [[SimpleAudioEngine sharedEngine] preloadEffect:SND_TILE_FLIP]; [[SimpleAudioEngine sharedEngine] preloadEffect:SND_TILE_SCORE]; [[SimpleAudioEngine sharedEngine] preloadEffect:SND_TILE_WRONG]; [[SimpleAudioEngine sharedEngine] preloadEffect:SND_SCORE]; }
The
preloadEffect
method of SimpleAudioEngine
actually takes an NSString
as an argument. We have defined constants to hold the names of the sound files. (These constants are at the top of the MTPlayfieldLayer.m
file, above the @implementation
statement.)
#define SND_TILE_FLIP @"button.caf" #define SND_TILE_SCORE @"whoosh.caf" #define SND_TILE_WRONG @"buzzer.caf" #define SND_SCORE @"harprun.caf"
Why do we do this? By using #define
statements in a single location, we can easily change the sound files we are using in one place, rather than relying on find-and-replace functionality to change the filenames throughout our code. Having done this, anywhere we want to play the button.caf
file, we can simply refer to it as SND_TILE_FLIP
(no quotes around it), and Xcode takes care of the rest.
We have peppered the code with various playing of these sound effects, but we won't go into detail on where each sound is triggered. When you want to play a sound effect, you can call it with a single line of code, as follows:
[[SimpleAudioEngine sharedEngine] playEffect:SND_SCORE];
It doesn't get much simpler than that!