Chapter 4

Your First Game

In this chapter you’ll build your first complete game. It won’t win any awards, but you’ll learn how to get the essential elements of cocos2d to work together. I’ll guide you through the individual steps, so you’ll also learn a bit about working with Xcode along the way.

The game is the inversion of the famous Doodle Jump game, aptly named DoodleDrop. The player’s goal is to avoid falling obstacles for as long as possible by rotating the device to move the player sprite. Take a look at the final version in Figure 4–1 to get an idea of what you’ll be creating in this chapter.

images

Figure 4–1. The final version of the DoodleDrop game

Step-by-Step Project Setup

Fire up Xcode now, and I’ll walk you through the steps to create your first cocos2d game. In Xcode, select File images New images New Project… and choose the cocos2d Application template, as shown in Figure 4–2. When asked to enter a name for the new project, enter DoodleDrop and, if necessary, find a suitable location to save the project. Xcode will automatically create a subfolder named DoodleDrop, so you don’t have to create that folder.

images

Figure 4–2. Create the project from the cocos2d Application template.

Xcode should present you with a project view similar to the one in Figure 4–3. Depending on the versions of cocos2d and Xcode you’re using, there may be more files, or the names of the groups may be slightly different.

I’ve already unfolded the Classes and Resources groups because that’s where you’ll be adding the source code and game resource files, respectively. Anything that’s not source code is considered to be a resource, be it an image, an audio file, a text file, or a plist. The grouping is not strictly necessary, but it does make it easier for you to navigate the project if you keep similar files grouped together.

images

Figure 4–3. Let the games begin! The DoodleDrop project at this point is based on the HelloWorld cocos2d project template. Make sure you add subsequent files to the Classes and Resources groups accordingly to keep your game project organized.

The next step you’re faced with is a decision: do you start working with the HelloWorldScene because it’s there already, possibly renaming it later? Or do you go through the extra steps to create your own scene to replace the HelloWorldScene? I chose the latter because eventually you’ll have to add new scenes anyway, so it’s a good idea to learn the ropes here and now and start with a clean slate.

Make sure the Classes group is selected and then select File images New images New File… or right-click the Classes folder and select New File… to open the New File dialog shown in Figure 4–4. Since cocos2d comes with class templates for the most important nodes, it would be a shame not to use them. From the cocos2d User Templates section, select the CCNode class, click Next, and make sure it’s set to Subclass of CCLayer before clicking Next again to bring up the save file dialog in Figure 4–5.

images

Figure 4–4. Adding new CCNode-derived classes is best done using the class templates provided by cocos2d. In this case, we want the CCNode class to be a subclass of CCLayer since we’re setting up a new scene.

I prefer to name classes by function and in a generic way. I’m using GameScene.m in this case. It’s going to be the scene where the DoodleDrop game play takes place, so that name seems appropriate. Be sure that the DoodleDrop target check box is checked. If you’re using Xcode 3, the Also create “GameScene.h” check box must also be checked. Targets are Xcode’s way of creating more or less different versions of the executable. For example, the iPad version of a game is usually created as a separate target. In this case, we have only one target, but once you create an iPad target, you want to make sure that, for example, the iPad’s high-resolution images aren’t accidentally added to the iPhone/iPod touch target.

NOTE: Not reviewing the target check boxes can lead to all kinds of issues, from compile errors to “file not found” errors or crashes during game play when files haven’t been added to the targets that need them. Or you might simply waste space by adding files to targets that don’t need them at all, for example by adding iPad and iPhone 4 high-res images to regular iPhone/iPod touch targets.

images

Figure 4–5. Naming the new scene and making sure it’s added to the appropriate targets

At this point, our GameScene class is empty, and the first thing we need to do to set it up as a scene is to add the +(id) scene method to it. The code we’ll plug in is essentially the same as in Chapter 3, with only the layer’s class name changed. What you’ll almost always need in any class are the –(id) init and –(void) dealloc methods, so it makes sense to add them right away. I’m also a very cautious programmer and decided to add the logging statements introduced in Chapter 3. The resulting GameScene.h is shown in Listing 4–1, and GameScene.m is in Listing 4–2.

Listing 4–1. GameScene.h with the Scene Method

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface GameScene : CCLayer
{
}

+(id) scene;

@end

Listing 4–2. GameScene.m with the Scene Method and Standard Methods Added, Including Logging

#import "GameScene.h"

@implementation GameScene

+(id) scene
{
    CCScene *scene = [CCScene node];
    CCLayer* layer = [GameScene node];
    [scene addChild:layer];
    return scene;
}

-(id) init
{
    if ((self = [super init]))
    {
        CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);
    }

    return self;
}

-(void) dealloc
{
    CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

    // never forget to call [super dealloc]
    [super dealloc];
}

@end

Now you can safely delete the HelloWorldScene class. When asked, select the Also Move to Trash option to remove the file from the hard drive as well, not just from the Xcode project. Select both files and choose Edit images Delete, or right-click the files and choose Delete from the context menu. With the HelloWorldScene class gone, you have to modify DoodleDropAppDelegate.m to change any references to HelloWorldScene to GameScene. Listing 4–3 highlights the necessary changes to the #import and runWithScene statements. I also changed the device orientation to portrait mode since the game is designed to work best in that mode.

Listing 4–3. Changing DoodleDropAppDelegate.m File to Use the GameScene Class Instead of HelloWorldScene

// replace the line #import “HelloWorldScene.h” with this one:
#import "GameScene.h"

- (void) applicationDidFinishLaunching:(UIApplication*)application
{
    …

    // Sets Portrait mode
    [director setDeviceOrientation:kCCDeviceOrientationPortrait];

    …

    // replace HelloWorld with GameScene
    [[CCDirector sharedDirector] runWithScene: [GameScene scene]];
}

Compile and run, and you should end up with…a blank scene. Success! If you run into any problems, compare your project with the DoodleDrop01 project that accompanies this book.

NOTE: Starting with cocos2d version 0.99.5, a file named GameConfig.h was added to the cocos2d Xcode project templates. If portrait mode isn’t working for you and you started a new project based on one of the cocos2d Xcode project templates, it’s probably because the game defaults to autorotation. If you see #define GAME_AUTOROTATION kGameAutorotationUIViewController in GameConfig.h, you have to change it to this line in order to disable autorotation: #define GAME_AUTOROTATION kGameAutorotationNone.

Adding the Player Sprite

Next you’ll add the player sprite and use the accelerometer to control its actions. To add the player image, select the Resources group in Xcode and select File imagesimages Add Files to “DoodleDrop”… or, alternatively, right-click and from the context menu pick Add Files to “DoodleDrop”… to open the file picker dialog. The player image alien.png is located in the Resources folder of the DoodleDrop project supplied with the book. You can also choose your own image, as long as it’s 64 by 64 pixels in size.

Xcode will then ask you details about how and where to add the files, as in Figure 4–6. Make sure the Add To Targets check boxes are set for each target that will use the files, which in this case is only the DoodleDrop target. The defaults are good enough.

TIP: The preferred image format for iOS games is PNG (which stands for Portable Network Graphics). It’s a compressed file format, but unlike JPG, the compression is lossless, retaining all pixels of the original image unchanged. Although you can also save JPEG files without compression, the same image in PNG format is typically smaller than an uncompressed JPEG file. This affects only the app size, not the memory (RAM) usage of the textures. In Chapter 16, you’ll also learn about TexturePacker, a tool that manages images for you. It allows you to convert images into various compressed formats or reduce the color depth while retaining the best possible image quality through dithering and other techniques.

images

Figure 4–6. You’ll see this dialog whenever you add resource files. In most cases you should use these default settings.

Now we’ll add the player sprite to the game scene. I decided to add it as a CCSprite* member variable to the GameScene class. This is easier for now, and the game is simple enough for everything to go into the same class. Generally, that’s not the recommended approach, so the projects following this one will create separate classes for individual game components as a matter of good code design.

Listing 4–4 shows the addition of the CCSprite* member to the GameScene header file.

Listing 4–4. The CCSprite* Player Is Added as a Member Variable to the GameScene Class

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface GameScene : CCLayer
{
    CCSprite* player;
}

+(id) scene;

@end

Listing 4–5 contains the code I’ve added to the init method to initialize the sprite, assign it to the member variable, and position it at the bottom center of the screen. I’ve also enabled accelerometer input.

Listing 4–5. Enabling Accelerometer Input and Creating and Positioning the Player Sprite

-(id) init
{
    if ((self = [super init]))
    {
        CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

        self.isAccelerometerEnabled = YES;

        player = [CCSprite spriteWithFile:@"alien.png"];
        [self addChild:player z:0 tag:1];

        CGSize screenSize = [[CCDirector sharedDirector] winSize];
        float imageHeight = [player texture].contentSize.height;
        player.position = CGPointMake(screenSize.width / 2, imageHeight / 2);
    }

    return self;
}

The player sprite is added as a child with a tag of 1, which will later be used to identify and separate the player sprite from all other sprites. Notice that I don’t retain the player sprite. Since we’ll add it as a child to the layer, cocos2d will retain it, and since the player sprite is never removed from the layer, that’s sufficient to keep the player sprite without specifically retaining it. Not retaining an object whose memory is managed by another class or object is called keeping a weak reference.

CAUTION: File names on iOS devices are case-sensitive. If you try to load Alien.png or ALIEN.PNG, it will work in the simulator but not on any iOS device because the real name is alien.png in all lowercase. That’s why it’s a good idea to stick to a naming convention like rigorously keeping all file names in all lowercase. Why lowercase? Because filename in all uppercase are typically harder to read.

You set the initial position of the player sprite by centering the x position at half the screen width, which puts the sprite in the center horizontally. Vertically we want the bottom of the player sprite’s texture to align with the bottom of the screen. If you remember from the previous chapter, you know that the sprite texture is centered on the node’s position. Positioning the sprite vertically at 0 would cause the bottom half of the sprite texture to be below the screen. That’s not what we want; we want to move it up by half the texture height.

This is done by the call to [player texture].contentSize.height, which returns the sprite texture’s content size. What exactly is the content size? In Chapter 3, I mentioned that the texture dimensions of iOS devices can only be powers of two. But the actual image size may be less than the texture size. For example, this is the case if the image is 100 by 100 pixels while the texture has to be 128 by128 pixels. The contentSize property of the texture returns the original image’s size of 100 by 100 pixels. In most cases, you’ll want to work with the content size, not the texture size. Even if your image is a power of two, you should use contentSize because the texture might be a texture atlas containing multiple images. Texture atlases are described in Chapter 6.

By taking half of the image height and setting this as the position on the y-axis, the sprite image will align neatly with the bottom of the screen.

TIP: It’s good practice to avoid using fixed positions wherever you can. If you simply set the player position to 160,32, you are making two assumptions you should avoid. First, you’re assuming the screen width will be 320 pixels, but that will not hold true for every iOS device. Second, you’re assuming that the image height is 64 pixels, but that might change too. Once you start to make assumptions like these, you’re forming a habit to do so throughout the project.

The way I wrote the positioning code involves a bit more typing, but in the long run this pays off big time. You can deploy to different devices and it’ll work, and you can use different image sizes and it’ll work. There’s no need to change this particular code anymore. One of the most time-consuming tasks a programmer faces is having to change code that was based on assumptions.

Imagine yourself three months down the road with lots of images and objects added to your game, having to change all those fixed numbers to create an iPad version that obviously also requires images of different sizes and then doing the same thing again to adjust the game to the iPhone 4’s Retina Display. At that point, you have three different Xcode projects you need to maintain and add features to. Eventually it will lead to “copy & paste hell,” which is even more undesirable—don’t go there!

Accelerometer Input

One last step, and then we should be done tilting the player sprite around. As I demonstrated in Chapter 3, you have to add the accelerometer method to the layer that receives accelerometer input. Here I use the acceleration.x parameter and add it to the player’s position; multiplying by 10 is to speed up the player’s movement.

-(void) accelerometer:(UIAccelerometer *)accelerometer
        didAccelerate:(UIAcceleration *)acceleration
{
    CGPoint pos = player.position;
    pos.x += acceleration.x * 10;
player.position = pos;
}

Notice something odd? I wrote three lines where one might seem to suffice:

// ERROR: lvalue required as left operand of assignment
player.position.x += acceleration.x * 10;

Unlike other programming languages such as Java, C++, and C#, writing something like player.position.x += value won’t work with Objective-C properties. The position property is a CGPoint, which is a regular C struct data type. Objective-C properties simply can’t assign a value to a field in a struct directly. The problem lies in how properties work in Objective-C and also how assignment works in the C language, on which Objective-C is based.

The statement player.position.x is actually a call to the position getter method [player position], which means you’re actually retrieving a temporary position and then trying to change the x member of the temporary CGPoint. But the temporary CGPoint would then get thrown away. The position setter [player setPosition] simply will not be called automagically. You can only assign to the player.position property directly, in this case a new CGPoint. In Objective-C you’ll have to live with this unfortunate issue—and possibly change programming habits if you come from a Java, C++, or C# background.

This is why the previous code has to create a temporary CGPoint object, change the point’s x field, and then assign the temporary CGPoint to player.position. Unfortunately, this is how it needs to be done in Objective-C.

First Test Run

Your project should now be at the same level as the one in the DoodleDrop02 folder of the code provided with this chapter. Give it a try now. Make sure you choose to run the app on the device, because you won’t get accelerometer input from the simulator. Test how the accelerometer input behaves in this version.

If you haven’t installed your development provisioning profiles in Xcode for this particular project yet, you’ll get a CodeSign error. Code signing is required to run an app on an iOS device. Please refer to Apple’s documentation to learn how to create and install the necessary development provisioning profiles (http://developer.apple.com/ios/manage/provisioningprofiles/howto.action).

Player Velocity

Notice how the accelerometer input isn’t quite right? It’s reacting slowly, and the motion isn’t fluid. That’s because the player sprite doesn’t experience true acceleration and deceleration. Let’s fix that now. The accompanying code changes are found in the DoodleDrop03 project.

The concept for implementing acceleration and deceleration is not to change the player’s position directly but to use a separate CGPoint variable as a velocity vector. Every time an accelerometer event is received, the velocity variable accumulates input from the accelerometer. Of course, that means we also have to limit the velocity to an arbitrary maximum; otherwise, it’ll take too long to decelerate. The velocity is then added to the player position every frame, regardless of whether accelerometer input was received.

NOTE: Why not use actions to move the player sprite? Well, move actions are a bad choice whenever you want to change an object’s speed or direction very often, say multiple times per second. Actions are designed to be relatively long-lived objects, so creating new ones frequently creates additional overhead in terms of allocating and releasing memory. This can quickly drain a game’s performance.

Worse yet, actions don’t work at all if you don’t give them any time to do their work. That’s why adding a new action to replace the previous one every frame won’t show any effect whatsoever. Many cocos2d developers have stumbled across this seemingly odd behavior.

For example, stopping all actions and then adding a new MoveBy action to an object every frame will not make it move at all! The MoveBy action will change the object’s positiononly in the next frame. But that’s when you’re already stopping all actions again and adding another new MoveBy action. Repeat ad infinitum, but the object will simply not move at all. It’s like the clichéd donkey: push it too hard, and it’ll become a stubborn, immobile object.

Let’s go through the code changes. The playerVelocity variable is added to the header:

@interface GameScene : CCLayer
{
    CCSprite* player;
    CGPoint playerVelocity;
}

If you wonder why I’m using a CGPoint instead of float, who’s to say you’ll never want to accelerate up or down a little? So, it doesn’t hurt to be prepared for future expansions.

Listing 4–6 shows the accelerometer code, which I changed to use the velocity instead of updating the player position directly. It introduces three new design parameters for the amount of deceleration, the accelerometer sensitivity, and the maximum velocity. Those are values that don’t have an optimum; you need to tweak them and find the right settings that work best with your game’s design (which is why they’re called design parameters).

Deceleration works by reducing the current velocity before adding the new accelerometer value multiplied by the sensitivity. The lower the deceleration, the quicker the player can change the alien’s direction. The higher the sensitivity, the more responsive the player will react to accelerometer input. These values interact with each other since they modify the same value, so be sure to tweak only one value at a time.

Listing 4–6. GameScene Implementation Gets playerVelocity

-(void) accelerometer:(UIAccelerometer *)accelerometer
        didAccelerate:(UIAcceleration *)acceleration
{
    // controls how quickly velocity decelerates (lower = quicker to change direction)
    float deceleration = 0.4f;
    // determines how sensitive the accelerometer reacts (higher = more sensitive)
    float sensitivity = 6.0f;
    // how fast the velocity can be at most
    float maxVelocity = 100;

    // adjust velocity based on current accelerometer acceleration
    playerVelocity.x = playerVelocity.x * deceleration + acceleration.x * sensitivity;

    // we must limit the maximum velocity of the player sprite, in both directions
    if (playerVelocity.x > maxVelocity)
    {
        playerVelocity.x = maxVelocity;
    }
    else if (playerVelocity.x < - maxVelocity)
    {
        playerVelocity.x = - maxVelocity;
    }
}

Now playerVelocity will be changed, but how do you add the velocity to the player’s position? You do so by scheduling the update method in the GameSceneinit method, by adding this line:

// schedules the –(void) update:(ccTime)delta method to be called every frame
[self scheduleUpdate];

You also need to add the –(void) update:(ccTime)delta method as shown in Listing 4–7. The scheduled update method is called every frame, and that’s where we add the velocity to the player position. This way, we get a smooth constant movement in either direction regardless of the frequency of accelerometer input.

Listing 4–7. Updating the Player’s Position with the Current Velocity

-(void) update:(ccTime)delta
{
    // Keep adding up the playerVelocity to the player's position
    CGPoint pos = player.position;
    pos.x += playerVelocity.x;

    // The Player should also be stopped from going outside the screen
    CGSize screenSize = [[CCDirector sharedDirector] winSize];
    float imageWidthHalved = [player texture].contentSize.width * 0.5f;
    float leftBorderLimit = imageWidthHalved;
    float rightBorderLimit = screenSize.width - imageWidthHalved;

    // preventing the player sprite from moving outside the screen
    if (pos.x < leftBorderLimit)
    {
        pos.x = leftBorderLimit;
        playerVelocity = CGPointZero;
    }
    else if (pos.x > rightBorderLimit)
    {
        pos.x = rightBorderLimit;
        playerVelocity = CGPointZero;
    }

    // assigning the modified position back
    player.position = pos;
}

TIP: The contentSize width is not divided by two but rather multiplied by 0.5 in order to calculate imageWidthHalved. This is a conscious choice and leads to the same results because any division can be rewritten as a multiplication.

The update method is called in every frame, and every piece of code that runs in every frame should be running at top speed. And since the ARM CPUs of the iOS devices don’t support division operations in hardware, multiplications are generally a bit faster. It’s not going to be noticeable in this case, but it’s a good habit to get into since, unlike other (premature) optimizations, it doesn’t make the code harder to read or maintain.

Another option for making this code just a little bit faster would be to precalculate values like imageWidthHalved, leftBorderLimit, and rightBorderLimit and then store them in instance variables. This is going to work as long as the player’s texture remains the same while the game is running. The trade-off here is higher memory usage for fewer CPU cycles, and you’ll generally find that this trade-off is very common for most code optimizations. But, as long you don’t need to squeeze out speed, it’s advisable to write readable and flexible code first and optimize only when you really must.

A boundary check prevents the player sprite from leaving the screen. Once again, we have to take the player texture’s contentSize into account, since the player position is at the center of the sprite image but we don’t want either side of the image to be off the screen. For this, we calculate imageWidthHalved and then use it to check whether the newly updated player position is within the left and right border limits. The code may be a bit verbose at this point, but that makes it easier to understand. And that’s it with the player accelerometer input logic.

TIP: You’ll notice that this straightforward implementation of accelerometer control doesn’t give you the same dynamic feeling that you may be used to from games like Tilt to Live. The reason is that smooth, dynamic accelerometer controls require additional computations on the input values that go beyond the scope of this book.

The technique to smooth the accelerometer input is called filtering. To be specific, you might want to search the cocos2d forum primarily for high-pass and low-pass filtering. Filtering the accelerometer input values reduces the effects of sudden peaks in acceleration (high-pass filter) as well as cancels out the effects of gravity or simply one’s natural hand tremor caused by our pulse (low-pass filter).

In addition, there are several ways to have an accelerometer-controlled object appear to have momentum. To achieve this effect, you would apply an easing function to the input values. Easing functions can simulate the effect of an object wanting to continue to move in the same direction with the same velocity unless an external force is exerted on the object. In other words, easing can emulate Newton’s first and second laws of motion.

Adding Obstacles

This game isn’t any good until we add something for the player to avoid. The DoodleDrop04project introduces an abomination of nature: a six-legged man-spider. Who wouldn’t want to avoid that?

As with the player sprite, you should add the spider.png to the Resources group. Then the GameScene.hfile gets three new member variables added to its interface: a spidersCCArray whose class reference is shown in Listing 4–9 and the spiderMoveDuration and numSpidersMoved, which are used in Listing 4–12:

@interface GameScene : CCLayer
{
    CCSprite* player;
    CGPoint playerVelocity;

    CCArray* spiders;
    float spiderMoveDuration;
    int numSpidersMoved;
}

And in the GameSceneinit method add the call to the initSpiders method discussed next, right after scheduleUpdate:

-(id) init
{
    if ((self = [super init]))
    {
        …

        [self scheduleUpdate];
        [self initSpiders];
    }
    return self;
}

After that a fair bit of code is added to the GameScene class, beginning with the initSpiders method in Listing 4–8, which is creating the spider sprites.

Listing 4–8. For Easier Access, Spider Sprites Are Initialized and Added to a CCArray

-(void) initSpiders
{
    CGSize screenSize = [[CCDirector sharedDirector] winSize];

    // using a temporary spider sprite is the easiest way to get the image's size
    CCSprite* tempSpider = [CCSprite spriteWithFile:@"spider.png"];
    float imageWidth = [tempSpider texture].contentSize.width;

    // Use as many spiders as can fit next to each other over the whole screen width.
    int numSpiders = screenSize.width / imageWidth;

    // Initialize the spiders array using alloc.
    spiders = [[CCArray alloc] initWithCapacity:numSpiders];

    for (int i = 0; i < numSpiders; i++)
{
        CCSprite* spider = [CCSprite spriteWithFile:@"spider.png"];
        [self addChild:spider z:0 tag:2];

        // Also add the spider to the spiders array.
        [spiders addObject:spider];
    }

    // call the method to reposition all spiders
    [self resetSpiders];
}

There are a few things to note. I create a tempSpiderCCSprite only to find out the sprite’s image width, which is then used to decide how many spider sprites can fit next to each other. The easiest way to get an image’s dimensions is by simply creating a temporary CCSprite. Note that I did not add the tempSpider as child to any other node. This means its memory will be released automatically.

This is in contrast to the spiders array I’m using to hold references to the spider sprites. This array must be created using alloc; otherwise, its memory would be released, and subsequent access to the sprites array would crash the app with an EXC_BAD_ACCESS error. And since I took control over managing the sprites’ array memory, I must not forget to release the spiders array in the dealloc method, as shown here:

-(void) dealloc
{
    CCLOG(@"%@: %@", NSStringFromSelector(_cmd), self);

    // The spiders array must be released, it was created using [CCArray alloc]
    [spiders release];
    spiders = nil;        

    // Never forget to call [super dealloc]!
    [super dealloc];
}

The CCArray class is, at this time of writing, an undocumented but fully supported class of cocos2d. You can find the CCArray class files in the cocos2d/Support group in the Xcode project. It’s used internally by cocos2d and is similar to Apple’s NSMutableArray class—except that it performs better. The CCArray class implements a subset of the NSArray and NSMutableArray classes and also adds new methods to initialize a CCArray from an NSArray. It also implements fastRemoveObject and fastRemoveObjectAtIndex methods by simply assigning the last object in the array to the deleted position in order to avoid copying parts of the array’s memory. This is faster, but it also means objects in CCArray will change positions, so if you rely on a specific ordering of objects, you shouldn’t use the fastRemoveObject methods. In Listing 4–9 you can see the full CCArray class reference because it doesn’t implement all of the methods of NSArray and NSMutableArray while adding its own.

Listing 4–9. CCArray Class Reference

+ (id) array;
+ (id) arrayWithCapacity:(NSUInteger)capacity;
+ (id) arrayWithArray:(CCArray*)otherArray;
+ (id) arrayWithNSArray:(NSArray*)otherArray;

- (id) initWithCapacity:(NSUInteger)capacity;
- (id) initWithArray:(CCArray*)otherArray;
- (id) initWithNSArray:(NSArray*)otherArray;

- (NSUInteger) count;
- (NSUInteger) capacity;
- (NSUInteger) indexOfObject:(id)object;
- (id) objectAtIndex:(NSUInteger)index;
- (id) lastObject;
- (BOOL) containsObject:(id)object;

#pragma mark Adding Objects

- (void) addObject:(id)object;
- (void) addObjectsFromArray:(CCArray*)otherArray;
- (void) addObjectsFromNSArray:(NSArray*)otherArray;
- (void) insertObject:(id)object atIndex:(NSUInteger)index;

#pragma mark Removing Objects

- (void) removeLastObject;
- (void) removeObject:(id)object;
- (void) removeObjectAtIndex:(NSUInteger)index;
- (void) removeObjectsInArray:(CCArray*)otherArray;
- (void) removeAllObjects;
- (void) fastRemoveObject:(id)object;
- (void) fastRemoveObjectAtIndex:(NSUInteger)index;

- (void) makeObjectsPerformSelector:(SEL)aSelector;
- (void) makeObjectsPerformSelector:(SEL)aSelector withObject:(id)object;

- (NSArray*) getNSArray;

At the end of Listing 4–8, the method [self resetSpiders] is called; this method is shown in Listing 4–10. The reason for separating the initialization of the sprites and positioning them is that eventually there will be a game over, after which the game will need to be reset. The most efficient way to do so is to simply move all game objects to their initial positions. However, that may stop being feasible once your game scene gets to a certain complexity. Eventually, it’ll be easier to simply reload the whole scene, at the cost of having the player wait for the scene to reload.

CAUTION: Speaking of reloading a scene, you may be tempted to write something like [[CCDirector sharedDirector] replaceScene:[GameScene node]]; within the GameScene class, or even [[CCDirector sharedDirector] replaceScene:self];, to reload the same scene. Both attempts will cause a crash! Instead, you should load another (intermediary) scene first before loading the same scene again. You’ll learn how to write a loading scene in Chapter 5.

Listing 4–10. Resetting Spider Sprite Positions

-(void) resetSpiders
{
    CGSize screenSize = [[CCDirector sharedDirector] winSize];

    // Get any spider to get its image width
    CCSprite* tempSpider = [spiders lastObject];
    CGSize size = [tempSpider texture].contentSize;

    int numSpiders = [spiders count];
    for (int i = 0; i < numSpiders; i++)
    {
        // Put each spider at its designated position outside the screen
        CCSprite* spider = [spiders objectAtIndex:i];
        spider.position = CGPointMake(size.width * i + size.width * 0.5f,images
            screenSize.height + size.height);

        [spider stopAllActions];
    }

    // Unschedule the selector just in case. If it isn't scheduled it won't do anything.
    [self unschedule:@selector(spidersUpdate:)];

    // Schedule the spider update logic to run at the given interval.
    [self schedule:@selector(spidersUpdate:) interval:0.7f];

    // reset the moved spiders counter and spider move duration (affects speed)
    numSpidersMoved = 0;
    spiderMoveDuration = 4.0f;
}

Once again I obtain a reference to one of the existing spiders temporarily to get its image size via the texture’s contentSize property. I don’t create a new sprite here since there are already existing sprites of the same kind, and since all spiders use the same image with the same size, I don’t even care which sprite I’m getting. So, I simply choose the get the last spider from the array.

Each spider’s position is then modified so that together they span the entire width of the screen. Half of the image size’s width is added, and once again this is because of the sprite’s texture being centered on the node’s position. As for the height, each sprite is also set to be one image size above the upper screen border. This is an arbitrary distance, as long as the image isn’t visible, which is all I want to achieve. Because the spider might still be moving when the reset occurs, I’ll also stop all of its actions at this point.

TIP: To save a few CPU cycles, it’s good practice not to use method calls in the conditional block of for or other loops if it’s not strictly necessary. In this case, I created a variable numSpiders to hold the result of [spiders count], and I use that in the conditional check of the for loop. The count of the array remains the same during the for loop’s iterations because the array itself isn’t modified in the loop. That’s why I can cache this value and save the repeated calls to [spiders count] during each iteration of the for loop.

I’m also scheduling the spidersUpdate: selector to run every 0.7 seconds, which is how often another spider will drop down from the top of the screen. But before doing so, I make sure the same selector is unscheduled, just to be safe. The resetSpiders method may be called while spidersUpdate: is still scheduled, and I don’t want the method to be called twice, effectively doubling the spider drop-down rate. The spidersUpdate: method, shown in Listing 4–11, randomly picks one of the existing spiders, checks whether it is idle, and lets it fall down the screen by using a sequence of actions.

Listing 4–11. The spidersUpdate: Method Frequently Lets a Spider Fall

-(void) spidersUpdate:(ccTime)delta
{
    // Try to find a spider which isn't currently moving.
    for (int i = 0; i < 10; i++)
    {
        int randomSpiderIndex = CCRANDOM_0_1() * [spiders count];
        CCSprite* spider = [spiders objectAtIndex:randomSpiderIndex];

        // If the spider isn't moving it won’t have any running actions.
        if ([spider numberOfRunningActions] == 0)
        {
            // This is the sequence which controls the spiders' movement
            [self runSpiderMoveSequence:spider];

            // Only one spider should start moving at a time.
            break;
        }
    }
}

I don’t let any listing pass without some curiosity, do I? In this case, you might wonder why I’m iterating exactly ten times to get a random spider. The reason is that I don’t know if the randomly generated index will get me a spider that isn’t moving already, so I want to be reasonably sure that eventually a spider is randomly picked that is currently idle. If after ten tries—and this number is arbitrary—I did not have the luck to get an idle spider chosen randomly, I’ll simply skip this update and wait for the next.

I could brute-force my way and just keep trying to find an idle spider using a do/while loop. However, it’s possible that all spiders could be moving at the same time, since this depends on design parameters such as the frequency with which new spiders are being dropped. In that case, the game would simply lock up, looping endlessly trying to find an idle spider. Moreover, I’m not so keen on trying too hard; it really doesn’t matter much for this game if I’m unable to send another spider falling down for a couple of seconds. That said, if you check out the DoodleDrop04 project, you’ll see I added a logging statement that will print out how many retries it took to find an idle spider.

Since the movement sequence is the only action the spiders perform, I simply check whether the spider is running any actions at all, and if not, I assume it is idle. And that brings us to the runSpiderMoveSequence in Listing 4–12.

Listing 4–12. Spider Movement Is Handled by an Action Sequence

-(void) runSpiderMoveSequence:(CCSprite*)spider
{
    // Slowly increase the spider speed over time.
    numSpidersMoved++;
    if (numSpidersMoved % 8 == 0 && spiderMoveDuration > 2.0f)
    {
        spiderMoveDuration -= 0.1f;
    }

    // This is the sequence which controls the spiders' movement.
    CGPoint belowScreenPosition = CGPointMake(spider.position.x,images
        -[spider texture].contentSize.height);
    CCMoveTo* move = [CCMoveTo actionWithDuration:spiderMoveDuration
                                         position:belowScreenPosition];
    CCCallFuncN* callDidDrop = [CCCallFuncN actionWithTarget:self
                                                    selector:@selector(spiderDidDrop:)];
    CCSequence* sequence = [CCSequence actions:move, callDidDrop, nil];
    [spider runAction:sequence];
}

The runSpiderMoveSequence method keeps track of the number of dropped spiders. Every eighth spider, the spiderMoveDuration is decreased, and thus any spider’s speed is increased. In case you are wondering about the % operator, it’s called the modulo operator. The result is the remainder of the division operation, meaning if numSpidersMoved is divisible by 8, the result of the modulo operation will be 0.

The action sequence consists only of a CCMoveTo action and a CCCallFuncN action. It could be improved to let spiders drop down a bit, wait, and then drop all the way, as evil six-legged man-spiders would normally do. I leave this improvement up to you. For now it’s only important to know that I chose the CCCallFuncN variant because I want the spiderDidDrop method to be called with the spider sprite as a sender parameter. This way, I get a reference to the spider that has reached its destination (it dropped past the player character), and I don’t have to jump through hoops to find the right spider. Listing 4–13 reveals how it’s done by resetting the spider position to just above the top of the screen whenever a spider has reached its destination just below the screen.

Listing 4–13. Resetting a Spider Position So It Can Fall Back Down Again

-(void) spiderDidDrop:(id)sender
{
    // Make sure sender is actually of the right class.
    NSAssert([sender isKindOfClass:[CCSprite class]], @"sender is not a CCSprite!");
    CCSprite* spider = (CCSprite*)sender;

    // move the spider back up outside the top of the screen
    CGPoint pos = spider.position;
    CGSize screenSize = [[CCDirector sharedDirector] winSize];
    pos.y = screenSize.height + [spider texture].contentSize.height;
    spider.position = pos;
}

NOTE: Being a defensive programmer, I’ve added the NSAssert line to make sure that sender is of the right class, since I’m assuming that sender will be a CCSprite, but it might not be one.

Indeed, when I first ran this code, I forgot to use CCCallFuncN and actually used a CCCallFunc, which led to sender being nil, since CCCallFunc doesn’t pass the sender parameter. NSAssert caught this case, too. With sender being nil, the method isKindOfClass was never called and the return value became nil, causing the NSAssert to trigger. It wasn’t the error I expected, but NSAssert caught it anyway. With that information, it was easy to figure out what I was doing wrong and fix it.

Once I’m sure that sender is of the class CCSprite, I can cast it to CCSprite* and use it to adjust the sprite’s position. The process should be familiar by now.

So far, so good. You might want to try the game and play it a little. I think you’ll quickly notice what’s still missing. Hint: read the next headline.

Collision Detection

You may be surprised to see that collision detection can be as simple as in Listing 4–14. Admittedly, this only checks the distance between the player and all spiders, which makes this type of collision detection a radial check. For this type of game, it’s sufficient. The call to [self checkForCollision] is added to the end of the –(void) update:(ccTime)delta method.

Listing 4–14. A Simple Range-Check or Radial Collision-Check Suffices

-(void) checkForCollision
{
    // Assumption: both player and spider images are squares.
    float playerImageSize = [player texture].contentSize.width;
    float spiderImageSize = [[spiders lastObject] texture].contentSize.width;
    float playerCollisionRadius = playerImageSize * 0.4f;
    float spiderCollisionRadius = spiderImageSize * 0.4f;

    // This collision distance will roughly equal the image shapes.
    float maxCollisionDistance = playerCollisionRadius + spiderCollisionRadius;

    int numSpiders = [spiders count];
    for (int i = 0; i < numSpiders; i++)
{
        CCSprite* spider = [spiders objectAtIndex:i];

        if ([spider numberOfRunningActions] == 0)
        {
            // This spider isn't even moving so we can skip checking it.
           continue;
        }

        // Get the distance between player and spider.
        float actualDistance = ccpDistance(player.position, spider.position);

        // Are the two objects closer than allowed?
        if (actualDistance < maxCollisionDistance)
        {
            // Game Over (just restart the game for now)
            [self resetSpiders];
            break;
        }
    }
}

The image sizes of the player and spider are used as hints for the collision radii. The approximation is good enough for this game. If you check the DoodleDrop05 project, you’ll also notice that I’ve added a debug drawing method that renders the collision radii for each sprite.

NOTE: The correct plural of radius is radii. You can also say radiuses without being expelled from the country, though I wouldn’t dare say it in the vicinity of programmers. Less critically but also hotly debated is the plural of vertex, which is correctly spelled vertices, but vertexes is also deemed acceptable.

I’m iterating over all the spiders but ignoring those that aren’t moving at the moment because they’ll definitely be out of range. The distance between the current spider and the player is calculated by the ccpDistance method. This is another undocumented but fully supported cocos2d method. You can find these and other useful math functions in the CGPointExtension files in the cocos2d/Support group in the Xcode project.

The resulting distance is then compared to the sum of the player’s and spider’s collision radius. If the actual distance is smaller than that, a collision has occurred. Since no game-over has been implemented, I chose to simply reset the spiders to restart the game.

Labels and Bitmap Fonts

Labels are the second most important graphical element of cocos2d games, right after sprites. The easiest and most flexible solution seems to be the CCLabelTTF class, but its performance is terrible if you need to change the displayed text frequently. The alternative is the CCLabelBMFont class, which renders bitmap fonts instead of TrueType fonts. Along with that I’ll introduce you to Glyph Designer, an elegant tool for converting TrueType fonts into bitmap fonts and enhancing them along the way with effects like shadows, color gradients, and so on.

Adding the Score Label

The game needs some kind of scoring mechanism. I decided to add a simple time-lapse counter as the score. I start by adding the score’s Label in the init method of the GameScene class:

scoreLabel = [CCLabelTTF labelWithString:@"0" fontName:@"Arial" fontSize:48];
scoreLabel.position = CGPointMake(screenSize.width / 2, screenSize.height);

// Adjust the label's anchorPoint's y position to make it align with the top.
scoreLabel.anchorPoint = CGPointMake(0.5f, 1.0f);

// Add the score label with z value of -1 so it's drawn below everything else
[self addChild:scoreLabel z:-1];

I consciously chose a CCLabelTTF object, which would likely be the first choice for most beginning cocos2d programmers. I’ll show you how quickly these label objects rear their ugly heads, though. Add the following code anywhere in the update: method so that the score label will be updated like a stopwatch counter every second:

// Update the Score (Timer) once per second.
totalTime += delta;
int currentTime = (int)totalTime;
if (score < currentTime)
{
    score = currentTime;
    [scoreLabel setString:[NSString stringWithFormat:@"%i", score]];
}

The delta parameter of the update: method is continuously added to the totalTime member variable to keep track of the time that has passed. Because totalTime is a floating-point variable, I simply assign it to an integer variable, effectively removing the fractional part. That makes it possible to compare the score with the currentTime; if the score is lower, currentTime becomes the new score, and the CCLabelTTF’s string is updated.

And that’s where things get ugly. In the previous chapter, I mentioned that updating a CCLabelTTF’s text is slow. The whole texture is re-created using iOS font-rendering methods, and they take their time, besides allocating a new texture and releasing the old one. If you play this version of the game on the device, you may notice how the spiders seem to jump a little every time the score label changes. The game is no longer running smoothly.

If you want to see this effect more pronounced, comment out the line with the if statement so that [scoreLabel setString:…] is run every frame. On my iPhone 3GS, the framerate suddenly drops from a previous constant 60 frames per second to below 30—all this because of one measly CCLabelTTF updated every frame!

But keep in mind that CCLabelTTF is slow only when changing its string frequently. If you create the CCLabelTTF once and never change it, it’s just as fast as any other CCSprite of the same dimensions.

Introducing CCLabelBMFont

Labels that update fast at the expense of more memory usage, like any other CCSprite, are the specialty of the CCLabelBMFont class. I’ve replaced the CCLabelTTF with a CCLabelBMFont in DoodleDrop07. It’s relatively straightforward; besides changing the declaration of the scoreLabel variable from CCLabelTTF to CCLabelBMFont in the header file, you only have to change the line in the init method, as shown here:

scoreLabel = [CCLabelBMFont labelWithString:@"0" fntFile:@"bitmapfont.fnt"];

NOTE: Bitmap fonts are a great choice for games because they are fast, but they do have one disadvantage. The size of any bitmap font is fixed. If you need the same font but larger or smaller in size, you can scale the CCLabelBMFont—but you lose image quality scaling up, and you’re potentially wasting memory scaling down. The other option is to create a separate font file with the new size, but this uses up more memory since each bitmap font comes with its own texture, even if only the font size changed.

But there’s a catch, obviously. You need to add the bitmapfont.fnt file as well as the accompanying bitmapfont.png, which are both in the project’s Resources folder. More importantly, you do want to create your own bitmap fonts sooner or later. The tool to use for that used to be Hiero; that was before Glyph Designer came along. Now Hiero is only the tool of choice if you really don’t want to spend any money. Hiero was written by Kevin James Glass. It’s a free Java Web application and is available from http://slick.cokeandcode.com/demos/hiero.jnlp.

The downside is, it’s a free Java web application. It will ask you to trust the application because of a missing security certificate. On the other hand, many developers use the tool, and so far there has been no evidence that the application is untrustworthy. Hiero also “features” several odd and downright annoying bugs, including an obnoxious one that has the resulting image file flipped upside down. If you see only garbage instead of a bitmap font text in your app, you may have to flip the bitmap font PNG image upside down with an image-editing program. I have documented these issues and how to fix them in my Hiero tutorial: www.learn-cocos2d.com/knowledge-base/tutorial-bitmap-fonts-hiero/.

Some developers also swear by BMFont. But as a Windows program, it requires a Windows computer or Windows installed in a virtual machine on your Mac. That’s why it’s not more widely used in the Mac developer community. You can download BMFont from www.angelcode.com/products/bmfont/.

Finally, along came a tool that satisfied everyone who wouldn’t hesitate to trade a few dollars for convenience and reliability: Glyph Designer.

Creating Bitmap Fonts with Glyph Designer

After the first edition of this book was printed, the guys at www.71squared.com released their Hiero replacement tool called Glyph Designer. Although it’s not free, it’s definitely worth every cent.

You can download a trial version of Glyph Designer on http://glyphdesigner.71squared.com, and if you’re already familiar with Hiero, you’ll notice a striking similarity in features, although the user interface is a lot easier to use and encourages experimentation. Mike Daley also mentioned in an episode of the Cocos2D Podcast available at http://cocos2dpodcast.wordpress.com that Glyph Designer will get a new feature that allows you to share font designs with other users of the tool.

Figure 4–7 shows Glypgh Designer in action. The process of creating a bitmap font is relatively playful, and it doesn’t hurt to change the various knobs, buttons, and colors as you please. I’ll outline just the basic editing areas for you.

images

Figure 4–7. Glyph Designer allows you to create bitmap fonts from any TrueType font. It can export FNT and PNG files compatible with the CCLabelBMFont class of cocos2d.

On the left side you have the list of TrueType fonts, and if those aren’t enough, you can use the Load Font icon to load any TTF file. Below the list you can change the size of the font with the slider and also switch to bold, italic, and other font styles.

TIP: Creating Retina-enabled bitmap fonts is easy. Create your font as usual and export it. This will be your non-Retina or SD font. Then simply change the size of the font in Glyph Designer to twice its normal size; for example, move the slider from a font size of 30 to a font size of 60. Then reexport the font using the same name but adding the -hd suffix. Now you have the same font in regular/SD and Retina/HD sizes.

Cocos2d will automatically recognize and use the font with the -hd suffix if Retina support is enabled and the game is running on a Retina device.

In the center of the screen, you see the resulting texture atlas used for your current font settings. You’ll notice that the texture atlas size and the order of the glyphs frequently change as you modify the font settings. You can also select a glyph and see its info on the right pane under Glyph Info.

Further down on the right pane you can change the texture atlas settings, although in most cases you don’t have to. Glyph Designer makes sure that the texture atlas size is always large enough to contain all the glyphs in a single texture.

With the Glyph Fill settings, you can change the color and the way glyphs are being filled, including a gradient setting. Alongside that, you have the option to change the Glyph Outline, which is a black thin line around each glyph, and the Glyph Shadow, which allows you to create a 3D-ish look of the font.

At the very bottom on the right pane you find the Included Glyphs section. With that you can choose from a predefined selection of glyphs to include in the atlas. If you absolutely know for sure that you won’t be needing certain characters, you can also enter your own list of characters to reduce the size of the texture. This is especially helpful for score strings where you may only need digits plus very few extra characters.

Once you are satisfied with your bitmap font, you can save the entire project so that you can restore previous settings. To save the font in a format usable by cocos2d, you have to save it via File images Export in the .fnt (Cocos2d Text) format. You can then add the FNT and PNG files created by Glyph Designer to your Xcode project and use the FNT file with the CCLabelBMFont class.

CAUTION: If you try to display characters using a CCLabelBMFont, which are not available in the .fntfile, they will simply be skipped and not displayed. For example, if you do [label setString:@“Hello, World!”] but your bitmap font contains only lowercase letters and no punctuation characters, you’ll see the string “ello orld” displayed instead.

Simply Playing Audio

I’ve added some audio files to complete this game. In the Resources folder of the DoodleDrop07 project, you’ll find the audio files named blues.mp3 and alien-sfx.cafthat you can add to your project. The first choice and the easiest way to play audio files in cocos2d is by using the SimpleAudioEngine. Audio support is not an integral part of cocos2d; this is the domain of CocosDenshion, a third-party addition to cocos2d and fortunately distributed with cocos2d.

TIP: If you’re looking for an alternative sound engine, I recommend ObjectAL available from http://kstenerud.github.com/ObjectAL-for-iPhone, which is the preferred audio engine in Kobold2D (see Chapter 16). ObjectAL has a cleanly written API and excellent documentation.

Because CocosDenshion is treated as separate code from cocos2d, you have to add the corresponding header files whenever you use the CocosDenshion audio functionality, like so:

#import "GameScene.h"
#import "SimpleAudioEngine.h"

You’ll find playing music and audio using the SimpleAudioEngine is straightforward, as shown here:

[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"blues.mp3" loop:YES];
[[SimpleAudioEngine sharedEngine] playEffect:@"alien-sfx.caf"];

For music and longer speech files, playing MP3 files is the preferred choice. Note that you can play only one MP3 file in the background at a time. Technically, it’s possible to play two or more MP3 files, but only one can be decoded in hardware. The extra strain on the CPU is undesirable for games, so playing multiple MP3 files at the same time is out of the question for most. This also means that short-lived sound effects should not be in MP3 format. For those audio effects, I’ve had only good experiences with 16-bit PCM (uncompressed) audio in either the WAV or CAF file format. The sampling rate can be 22.5 kHz for most game sound effects, unless you need or want crystal-clear audio quality, in which case you should use 44.1 kHz.

A good and complete audio-editing tool for Mac OS X is Audacity, which you can download for free from http://audacity.sourceforge.net. If you need only to quickly convert audio files from one format to another, possibly changing some basic settings such as sampling rate, I recommend SoundConverter, which was developed by Steve Dekorte. The tool is free to use for files up to 500KB in size, and the license to use SoundConverter without restrictions is just $15. You can download SoundConverter from http://dekorte.com/projects/shareware/SoundConverter/.

A free alternative to SoundConverter is the command-line tool afconvert. Familiarity with Terminal is recommended. You can do a lot with afconvert, but being a command-line tool, you’ll also have to type all settings. To get help for afconvert, open the Terminal app and type the following:

afconvert -h

The preferred audio format for iOS devices is 16-bit, little endian, linear PCM packaged as CAF file (Apple CAF audio format code: LEI16), according to Apple’s Audio Coding How To, which contains generally helpful advice for audio programming (http://developer.apple.com/library/ios/#codinghowtos/AudioAndVideo/_index.html).

To convert any audio file that afconvert supports to the preferred iOS audio format, you would run the afconvert command like this:

afconvert -f caff -d LEI16 myInputFile.mp3 myOutputFile.caf

The -f (or -file) switch denotes the file format, which is caff for CAF files. With the -d switch, you specify the audio data format, here LEI16. You can get a list of the audio data formats supported by afconvert by running afconvert with the -hf switch.

NOTE: If you ever find yourself in the situation where an audio file just won’t play or results in a garbled mess of noise, don’t worry. There are countless audio applications and numerous audio codecs that all create their own variations of the respective formats. Some are unable to play on iOS devices but play fine otherwise. Particularly, WAV files seem to be affected, which is why I prefer to use Apple’s more native audio container format CAF. Typically, the way you can fix broken audio files is to open the audio file in an audio-editing program that you know is capable of saving iOS-compatible audio files and then save it again. You can do this with the aptly named SoundConverter or the audio application of your choice. Usually, after this resave, the file will play just fine on the iOS device.

Porting to iPad

With all coordinates taking the screen’s size into account, the game should simply scale up without any problems when running it on the iPad’s bigger screen. And it does. Just like that. In contrast, if you had been using fixed coordinates, you’d be facing a serious rework of your game.

One Universal App or Two Separate Apps?

When porting your app to iPad, you generally have to decide whether your app will be treated as a single (Universal) app on the App Store or whether it should be treated as two separate apps. Both options have their pros and cons, and generally you could say that Universal apps are better and fairer for customers, whereas separate apps are usually better for developers.

Universal apps include code and assets for both iPhone/iPod touch and iPad devices. This has the drawback that all assets are added to the same Xcode target, increasing the app’s size. That’s the technical drawback; there are no performance penalties.

With a Universal app, you will not be able to set different prices for iPhone/iPod touch and iPad versions, and user reviews and comments for all devices will be listed under the same app. Moreover, you won’t know which percentage of your download’s respective purchases were made by iPad users.

Regardless of that, Universal apps will still be ranked separately by device in the App Store charts. If the user downloads or purchases the app on an iPhone or iPod touch device, it is accounted for in the iPhone charts. The same goes for downloads/purchases on the iPad, which add to your app’s ranking in the iPad charts. That leaves the question of how iTunes downloads/purchases are accounted for. They are simply accounted for in the iPhone rankings. That makes it impossible to even estimate how many of your users are iPad users, unless you add analytics tracking code to your app.

Splitting your app into two separate apps for iPhone/iPod touch and iPad allows you to keep the game assets separate. Most importantly, if an iOS user wants both the iPhone and iPad versions, he’ll have to buy both. That is good for you but bad for the customer. And some won’t hesitate to give your app a worse rating just because of that.

But since your app will be treated as two entirely separate apps in the App Store, at least the customer reviews and comments will be specific to the particular app version. You’ll also be able to optimize each app’s description and screenshots for the target platform and update each version separately. Splitting your app is also a good choice if your app has been on the App Store for a while, since adding support for new devices in a Universal app will not have your app appear in the What’s New section of the App Store.

Porting to iPad with Xcode 3

Porting an iPhone project is a simple process. If you are using Xcode 3, then select the target in the Groups & Files pane that you want to convert and select Project images Upgrade Current Target for iPad…, which will bring up the dialog in Figure 4–8.

images

Figure 4–8. Upgrading a target for iPad in Xcode 3 gives you two choices. Universal application requires you to add both iPhone and iPad assets to the same target, increasing the app’s download size.

For a simple game, the default One Universal application is fine. It does have the drawback that all assets are added to the same target, increasing the app’s size. That’s the only technical drawback; there are no performance penalties. The resulting app can be run on both the iPhone and iPad. Choosing the Two device-specific applications option lets you keep game assets separate, but you end up with two apps, which you would have to submit to the App Store individually.

The easier option for now is to choose One Universal application and run the code. On the device, the app will automatically detect which device is connected and run the appropriate version. If you want to try it in the iPad Simulator, just select the iPad Simulator as the active executable, as in Figure 4–9.

images

Figure 4–9. To run the new Universal app in the iPad Simulator, make it the active executable.

Porting to iPad with Xcode 4

Xcode 4 makes porting apps incredibly straightforward, so much so that you can change the targeted device with a simple drop-down.

First, select the project DoodleDrop in the Project Navigator. This brings up the list of targets where you select the DoodleDrop target and then choose the Summary tab. Under the iOS Application Target section, there’s a pop-up control labeled Devices that gives you three choices: iPhone, iPad, and Universal.

Depending on the device setting, your app is either built for iPhone or iPad specifically or supports both iPhone and iPad as a Universal app. In Figure 4–10 the target is set to build a Universal app.

images

Figure 4–10. Changing the application target to build a Universal app

Now you may be wondering, what if I want two separate targets for iPhone and iPad? This can be useful to charge different prices for iPhone and iPad versions or simply to reduce the download size of either version.

In that case, all you have to do is to select the target, DoodleDrop in this case, and choose Edit images Duplicate from the menu. Or just right-click the target and select Duplicate. This creates a duplicate of the target, which allows you to set one target’s Devices setting to iPhone and the other target’s Devices setting to iPad. Now you have a separate target for each device type, and you might want to label the targets accordingly to avoid confusion.

Summary

I hope you had fun building this first game. It was surely a lot to take in, but I’d rather err on the side of too much information than too little.

At this point, you’ve learned how to create your own game-layer class and how to work with sprites. You’ve used the accelerometer to control the player and added velocity to allow the player sprite to accelerate and decelerate, giving it a more dynamic feel.

I also introduced you to the undocumented CCArray class, cocos2d’s replacement for NSMutableArray. This should be your preferred choice when you need to store a list of data. Simple radial collision detection using the distance check method from the likewise undocumented CGPointExtensions was also on the menu.

And for dessert you had a potpourri of labels, bitmap fonts, and the Glyph Designer tool, garnished with some audio programming.

What’s left? Maybe going through the source code for this chapter. I’ve added a finalized version of this game that includes some game-play improvements, a startup menu, and a game-over message.

There’s just one thing about the DoodleDrop project I haven’t mentioned yet: it’s all in one class. For a small project, this may suffice, but it’ll quickly get messy as you move on to implement more features. You need to add some structure to your code design. The next chapter will arm you with cocos2d programming best practices and show you how to lay out your code and the various ways information can be passed between objects if they are no longer in the same class.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset