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