Book Image

Creating Games with cocos2d for iPhone 2

By : Paul Nygard
Book Image

Creating Games with cocos2d for iPhone 2

By: Paul Nygard

Overview of this book

Cocos2d for iPhone is a simple (but powerful) 2D framework that makes it easy to create games for the iPhone. There are thousands of games in the App Store already using cocos2d. Game development has never been this approachable and easy to get started. "Creating Games with cocos2d for iPhone 2" takes you through the entire process of designing and building nine complete games for the iPhone, iPod Touch, or iPad using cocos2d 2.0. The projects start simply and gradually increase in complexity, building on the lessons learned in previous chapters. Good design practices are emphasized throughout. From a simple match game to an endless runner, you will learn how to build a wide variety of game styles. You will learn how to implement animation, actions, create "artificial randomness", use the Box2D physics engine, create tile maps, and even use Bluetooth to play between two devices. "Creating games with cocos2d for iPhone 2" will take your game building skills to the next level.
Table of Contents (16 chapters)
Creating Games with cocos2d for iPhone 2
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Animating the score


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."

Adding lives and game over

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];
}

Bringing it all together

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.

It's quiet...too quiet

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!