Now we need to define the class for the tiles themselves. We have a few variables we need to track for the tiles and we will use the MTMemoryTile
class to handle some of the touch detection and tile animation.
For this, we will be subclassing CCSprite
. This will allow
us to still treat it like a CCSprite
, but we will enhance it with other methods and properties specific to the tile.
Filename: MTMemoryTile.h
#import <Foundation/Foundation.h> #import "cocos2d.h" #import "SimpleAudioEngine.h" // MemoryTile is a subclass of CCSprite @interface MTMemoryTile : CCSprite { NSInteger _tileRow; NSInteger _tileColumn; NSString *_faceSpriteName; BOOL isFaceUp; } @property (nonatomic, assign) NSInteger tileRow; @property (nonatomic, assign) NSInteger tileColumn; @property (nonatomic, assign) BOOL isFaceUp; @property (nonatomic, retain) NSString *faceSpriteName; // Exposed methods to interact with the tile -(void) showFace; -(void) showBack; -(void) flipTile; -(BOOL) containsTouchLocation:(CGPoint)pos; @end
Here we are declaring the variables with an underscore prefix, but we set the corresponding property without the underscore prefix. This is usually done to avoid accidentally setting the variable value directly, which would bypass the getter and setter methods for the property. This split-naming is finalized in the
@synthesize
statements in the .m
file, where the property will be set to the variable. These statements will be of the basic format:
@synthesize propertyName = _variableName;
We're planning ahead with this class, including the headers for three methods that we will use for the tile animation:
flipTile
,
showFace
, and showBack
. This class will be responsible for handling its own animation.
All animation in our game will be done using cocos2d actions. Actions are essentially transformations of some sort that can be "run" on most types of cocos2d objects (for example, CCLayer
, CCSprite
, and so on). There are quite a number of different actions defined in the framework. Some of the most commonly used are actions such as CCMoveTo
(to move an object), CCScaleTo
(to change the scale of the object), and CCCallFunc
(to call another method). Actions are a "fire and forget" feature. Once you schedule an action, unless you explicitly change the action (such as calling stopAllActions
), the actions will continue until complete. This is further extended by "wrapping" several actions together in a CCSequence
action,
which allows you to chain several actions together, to be run in the order specified.
We will use CCSequence
"chaining" extensively throughout the book. Actions can be run on most cocos2d objects, but they are most commonly called (via the runAction:
method) on the CCSprite
and CCLayer
objects.
Filename: MTMemoryTile.m
@implementation MTMemoryTile @synthesize tileRow = _tileRow; @synthesize tileColumn = _tileColumn; @synthesize faceSpriteName = _faceSpriteName; @synthesize isFaceUp; -(void) dealloc { // We set this to nil to let the string go away self.faceSpriteName = nil; [super dealloc]; } -(void) showFace { // Instantly swap the texture used for this tile // to the faceSpriteName [self setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:self.faceSpriteName]]; self.isFaceUp = YES; } -(void) showBack { // Instantly swap the texture to the back image [self setDisplayFrame:[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:@"tileback.png"]]; self.isFaceUp = NO; } -(void) changeTile { // This is called in the middle of the flipTile // method to change the tile image while the tile is // "on edge", so the player doesn't see the switch if (isFaceUp) { [self showBack]; } else { [self showFace]; } } -(void) flipTile { // This method uses the CCOrbitCamera to spin the // view of this sprite so we simulate a tile flip // Duration is how long the total flip will last float duration = 0.25f; CCOrbitCamera *rotateToEdge = [CCOrbitCamera actionWithDuration:duration/2 radius:1 deltaRadius:0 angleZ:0 deltaAngleZ:90 angleX:0 deltaAngleX:0]; CCOrbitCamera *rotateFlat = [CCOrbitCamera actionWithDuration:duration/2 radius:1 deltaRadius:0 angleZ:270 deltaAngleZ:90 angleX:0 deltaAngleX:0]; [self runAction:[CCSequence actions: rotateToEdge, [CCCallFunc actionWithTarget:self selector:@selector(changeTile)], rotateFlat, nil]]; // Play the sound effect for flipping [[SimpleAudioEngine sharedEngine] playEffect: SND_TILE_FLIP]; } - (BOOL)containsTouchLocation:(CGPoint)pos { // This is called from the CCLayer to let the object // answer if it was touched or not return CGRectContainsPoint(self.boundingBox, pos); } @end
We will not be using a touch handler inside this class, since we will need to handle the matching logic in the main layer anyway. Instead, we expose the containsTouchLocation
method
, so the layer can "ask" the individual tiles if they were touched. This uses the tile's boundingBox
, which is baked-in functionality in cocos2d. A
boundingBox
is a CGRect
representing the smallest rectangle surrounding the sprite image itself.
We also see the
showFace
and
showBack
methods. These methods will set a new display frame for the tile. In order to retain the name of the sprite frame that is used for the face of this tile, we use the faceSpriteName
variable to hold the sprite frame name (which is also the original image filename). We don't need to keep a variable for the tile back, since all tiles will be using the same image, so we can safely hardcode that name.
The
flipTile
method makes use of the CCOrbitCamera
to deform the tile by rotating the "camera" around the sprite image. This is a bit of visual trickery and isn't a perfect flip (some extra deformation occurs nearer the edges of the screen), but it gives a fairly decent animation without a lot of heavy coding or prerendered animations. Here we use a CCSequence
action to queue three actions. The first action, rotateToEdge
, will rotate the tile on its axis until it is edge-wise to the screen. The second calls out to the
changeFace
method, which will do an instant swap between the front and back of the tile. The third action, rotateFlat
, completes the rotation back to the original "flat" orientation. The same flipTile
method can be used for flipping to the front and flipping to the back, because the isFaceUp
Boolean being used allows the
changeTile
method to know whether front or back should be visible. Let's look at following screenshot, which shows the tile flips, in mid-flip:
Tip
Downloading the color images of this book
We also provide you a PDF file that has color images of the screenshots/diagrams used in this book. The color images will help you better understand the changes in the output.
You can download this file from http://www.packtpub.com/sites/default/files/downloads/9007OS_ColoredImages.pdf
Now we have our tile class, we're ready to load some tiles into the
tilesAvailable
array:
Filename: MTPlayfieldLayer.m
-(void) acquireMemoryTiles { // This method will create and load the MemoryTiles // into the tilesAvailable array // We assume the tiles all use standard names for (int cnt = 1; cnt <= maxTiles; cnt++) { // Load the tile into the array // We loop so we add each tile in the array twice // This gives us a matched pair of each tile for (NSInteger tileNo = 1; tileNo <= 2; tileNo++) { // Generate the tile image name NSString *imageName = [NSString stringWithFormat:@"tile%i.png", cnt]; //Create a new MemoryTile with this image MTMemoryTile *newTile = [MTMemoryTile spriteWithSpriteFrameName:imageName]; // We capture the image name for the card face [newTile setFaceSpriteName:imageName]; // We want the tiles to start face down [newTile showBack]; // Add the MemoryTile to the array [tilesAvailable addObject:newTile]; } } }
Here we loop through all the unique tiles we need (up to the value of maxTiles
, which is set to half of the available spaces on the board). Inside that loop, we set up another for loop that counts to 2
. We do this because we need two copies of each tile to assemble our board. Since we have established that our tiles are named as tile#.png
, we create an NSString
with the incremental name, and create an MTMemoryTile
object with a standard CCSprite
constructor. As we said earlier, we want to keep a copy of the
image name for the showFace
method, so we set the
faceSpriteName
variable to that value. It wouldn't be much of a game if we had all the tiles face up, so we call showBack
, so the tiles are face down before they are used on screen. Finally, we add the tile we just created to the tilesAvailable
array. Once this method completes, the
tilesAvailable
array will be the only retain we have on the tiles.
Now we need to draw a randomly selected tile in every position to make a nice grid. First we need to figure out where each tile should be positioned. If we were using a fixed number of tiles, we could use absolute positioning. To account for the dynamic number of tiles, we add a "helper" method to determine positioning as follows:
Filename: MTPlayfieldLayer.m
-(CGPoint) tilePosforRow:(NSInteger)rowNum andColumn:(NSInteger)colNum { // Generate the coordinates for each tile float newX = boardOffsetX + (tileSize.width + padWidth) * (colNum - .5); float newY = boardOffsetY + (tileSize.height + padHeight) * (rowNum - .5); return ccp(newX, newY); }
To calculate the x position, we determine the total footprint of a single tile and the associated padding. We multiply this times the column number minus one half. We add this result to the board offset we calculated earlier. Why do we subtract one half? This is because our positions are based on the complete size of the tile and the padding. What we need is the center point of the tile, because that is our
anchorPoint
(that is the point on which the tile will pivot or rotate.). We need this anchor point left at the center (the default anchorPoint
for a
CCSprite
object, as it happens), because when we flip the tiles, the flip will be based on this anchorPoint
, so we want them to flip around the middle of the tile. Now that we have our tile positioning established,
we can go ahead and start building the tiles on the screen.
Filename: MTPlayfieldLayer.m
-(void) generateTileGrid { // This method takes the tilesAvailable array, // and deals the tiles out to the board randomly // Tiles used will be moved to the tilesInPlay array // Loop through all the positions on the board for (NSInteger newRow = 1; newRow <= boardRows; newRow++) { for (NSInteger newCol = 1; newCol <= boardColumns; newCol++) { // We randomize each card slot NSInteger rndPick = (NSInteger)arc4random() % ([tilesAvailable count]); // Grab the MemoryTile from the array MTMemoryTile *newTile = [tilesAvailable objectAtIndex:rndPick]; // Let the card "know" where it is [newTile setTileRow:newRow]; [newTile setTileColumn:newCol]; // Scale the tile to size float tileScaleX = tileSize.width / newTile.contentSize.width; // We scale by X only (tiles are square) [newTile setScale:tileScaleX]; // Set the positioning for the tile [newTile setPosition:[self tilePosforRow:newRow andColumn:newCol]]; // Add the tile as a child of our batch node [self addChild:newTile]; // Since we don't want to re-use this tile, // we remove it from the array [tilesAvailable removeObjectAtIndex:rndPick]; // We retain the MemoryTile for later access [tilesInPlay addObject:newTile]; } } }
Here we use two nested for loops to iterate through all rows and all columns. We use arc4random()
to select a random tile from the tilesAvailable
array and build a new
MTMemoryTile
object that references the tile selected. After setting the MTMemoryTile
object's variables for which row and column it represents, we set the scale factor for the tile. Since our images are bigger than needed for most game types, we divide the desired tileSize
by the actual contentSize
of the image. When applied, this will correctly scale our image to the desired display size. We only use the x (width) value, since we already enforced in the initWithRows:andColumns:
method that the images will always be square.
We use the
tilePosforRow
method to determine where it should be on the layer, and we add it. After adding it to the layer, we also add the new tile to the tilesInPlay
array and remove it from the
tilesAvailable
array. By removing it from tilesAvailable
, we ensure that we cannot select the same tile twice. After all iterations of the nested loops, the tilesAvailable
array should be empty, and the board should be fully populated with tiles.