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

On to the playfield


Now we're ready to get to the playfield layer itself. We know we need to keep track of the size of the game screen, how big each tile should be, how big the game board should be, and how much spacing we need between the tiles when they are laid out in a grid.

Creating the playfield header

In the header, we also have the declaration for the class method initWithRows:andColumns: that we called in the MTPlayfieldScene class.

Filename: MTPlayfieldLayer.h

#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "MTMemoryTile.h"
#import "SimpleAudioEngine.h"
#import "MTMenuScene.h"

@interface MTPlayfieldLayer : CCLayer {
    CGSize size; // The window size from CCDirector

    CCSpriteBatchNode *memorysheet;
    
    CGSize tileSize; // Size (in points) of the tiles
    
    NSMutableArray *tilesAvailable;
    NSMutableArray *tilesInPlay;
    NSMutableArray *tilesSelected; 

    CCSprite *backButton;
    
    NSInteger maxTiles;
    
    float boardWidth; // Max width of the game board
    float boardHeight; // Max height of the game board
    
    NSInteger boardRows; // Total rows in the grid
    NSInteger boardColumns; // Total columns in the grid

    NSInteger boardOffsetX; // Offset from the left
    NSInteger boardOffsetY; // Offset from the bottom
    NSInteger padWidth; // Space between tiles
    NSInteger padHeight; // Space between tiles
    
    NSInteger playerScore; // Current score value
    CCLabelTTF *playerScoreDisplay; // Score label

    NSInteger livesRemaining; // Lives value
    CCLabelTTF *livesRemainingDisplay; // Lives label
    BOOL isGameOver;
}

+(id) layerWithRows:(NSInteger)numRows
         andColumns:(NSInteger)numCols;

@end

One item to point out in the header is the CGSize size variable. This is a convenience variable we use to avoid repetitive typing. The name size is an abbreviation for winSize, which is a value that the CCDirector class will provide for you that identifies the size of the screen, in points. You can read the value from the CCDirector every time you use it, but doing so will make your code lines a bit longer. Our approach will work fine, as long as we do not support both portrait and landscape modes in the same layer. If we do allow both orientations, then the value we have cached in the size variable will be incorrect. Since our app only allows LandscapeLeft and LandscapeRight orientations, the size is identical in both orientations, so the size will be stable for our game.

Creating the playfield layer

In the MTPlayfieldLayer.m file, we implement our custom layerWithRows:andColumns: and initWithRows:andColumns: methods as follows:

Filename: MTPlayfieldLayer.m

+(id) layerWithRows:(NSInteger)numRows
         andColumns:(NSInteger)numCols {
  return [[[self alloc] initWithRows:numRows
                    andColumns:numCols] autorelease];
}

-(id) initWithRows:(NSInteger)numRows
        andColumns:(NSInteger)numCols {
    if (self == [super init]) {
        
        self.isTouchEnabled = YES;
        
        // Get the window size from the CCDirector
        size = [[CCDirector sharedDirector] winSize];
        
        // Preload the sound effects
        [self preloadEffects];
        
        // make sure we've loaded the spritesheets
        [[CCSpriteFrameCache sharedSpriteFrameCache]
         addSpriteFramesWithFile:@"memorysheet.plist"];
        memorysheet = [CCSpriteBatchNode
                batchNodeWithFile:@"memorysheet.png"];
        
        // Add the batch node to the layer
        [self addChild:memorysheet];
        
        // Add the back Button to the bottom right corner
        backButton = [CCSprite spriteWithSpriteFrameName:
                      @"backbutton.png"];
        [backButton setAnchorPoint:ccp(1,0)];
        [backButton setPosition:ccp(size.width - 10, 10)];
        [memorysheet addChild:backButton];
        
        // Maximum size of the actual playing field
        boardWidth = 400;
        boardHeight = 320;
        
        // Set the board rows and columns
        boardRows = numRows;
        boardColumns = numCols;
        
        // If the total number of card positions is
        // not even, remove one row
        // This against an impossible board
        if ( (boardRows * boardColumns) % 2 ) {
            boardRows--;
        }
        
        // Set the number of images to choose from
        // We need 2 of each, so we halve the total tiles
        maxTiles = (boardRows * boardColumns) / 2;
        // Set up the padding between the tiles
        padWidth = 10;
        padHeight = 10;
        
        // We set the desired tile size
        float tileWidth = ((boardWidth -
                            (boardColumns * padWidth))
                           / boardColumns) - padWidth;
        float tileHeight = ((boardHeight -
                             (boardRows * padHeight))
                            / boardRows) - padHeight;

        // We force the tiles to be square
        if (tileWidth > tileHeight) {
            tileWidth = tileHeight;
        } else {
            tileHeight = tileWidth;
        }
        
        // Store the tileSize so we can use it later
        tileSize = CGSizeMake(tileWidth, tileHeight);

        // Set the offset from the edge
        boardOffsetX = (boardWidth - ((tileSize.width +
                        padWidth) * boardColumns)) / 2;
        boardOffsetY = (boardHeight - ((tileSize.height+
                        padHeight) * boardRows)) / 2;
        
        // Set the score to zero
        playerScore = 0;
        
        // Initialize the arrays

        // Populate the tilesAvailable array
        [self acquireMemoryTiles];
        
        // Generate the actual playfield on-screen
        [self generateTileGrid];

        // Calculate the number of lives left
        [self calculateLivesRemaining];
        
        // We create the score and lives display here
        [self generateScoreAndLivesDisplay];
    }
  return self;
}

The class method layerWithRows:andColumns: is the method we saw in the MTPlayfieldScene class earlier. The class method calls the alloc and initWithRows: andColumns: methods, and then wraps it all in an autorelease call since it is a convenience method. The instance method initWithRows:AndColumns: (called by the class method) sets up a few of the variables we established in the header, including the assignment of our passed numRows and numColumns parameters into the instance variables boardRows and boardColumns.

Memory games are traditionally played with a square or rectangular layout. They also need an even number of tiles in the game, since there will be two of each type of tile. Because we are allowing flexible parameters for the number of rows and the number of columns, there are certain combinations that will not work. Requesting five rows and five columns means we will have 25 tiles on the board, which is impossible to win. To protect our game from these invalid values, we multiply the boardRows times the boardColumns. If the result is not even (using the % 2 check), then we remove one boardRow from the game. From the prior example, if we requested a five by five board (resulting in 25 tiles), the code would alter it to a four by five grid, which has 20 tiles.

We also set the tileSize value here, based on an even spacing of the tiles, along with the extra pad space we will be using between the tiles. Because we need square tiles, there is also the additional check to force the tiles to be square if the source images are not. This will distort the images, but it won't disrupt the game mechanics. Additionally, the boardOffsetX and boardOffsetY variables simply ensure the board will be nicely centered in the available board space.