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

A stack of tiles


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.

The memory tile class

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

Loading tiles

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.

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