Chapter    7

Scrolling with Joy

Continuing with the beginnings of the game from Chapter 6, you’ll now turn it into something resembling an actual shoot-’em-up game. The very first thing will be to make the player’s ship controllable. Accelerometer controls don’t make sense in this case; a virtual joypad would be much more appropriate. But instead of reinventing the wheel, you’ll use a cool source code package called SneakyInput to add a virtual joypad to this cocos2d game.

Moving the player’s ship around is one thing. You also want the background to scroll, to give the impression of moving into a certain direction. To make that happen, you’ll implement your own solution for parallax scrolling, because CCParallaxNode is too limited—it doesn’t allow an infinitely scrolling parallax background.

In addition, in this chapter I illustrate what you’ve learned about texture atlases and sprite batching in Chapter 6. There’s one texture atlas containing all the game’s graphics because there’s no need to group the images separately when using a texture atlas.

Advanced Parallax Scrolling

Because CCParallaxNode doesn’t allow infinite scrolling, for this shooting game you’ll add a ParallaxBackground node, which does just that. Moreover, it uses a CCSpriteBatchNode to speed up the rendering of the background images.

Creating the Background as Stripes

First, I want to illustrate how I made the background stripes that will create the parallaxing effect. This is crucial to understanding how the texture atlas TexturePacker creates can help you save memory and performance and also save time positioning the individual stripes. Figure 7-1 shows the background layer as a Seashore image composed of individual parallax layers. This image is also in the Assets folder of the ScrollingWithJoy01 project, as background-parallax.xcf.

9781430244165_Fig07-01.jpg

Figure 7-1.  The source image for the parallax scrolling background

Each stripe is on its own layer in the image-editing program Seashore. In Figure 7-2 you can see the various layers, and the Assets folder holds images named bg0.png through bg6.png, which correspond to the seven individual stripes making up the background.

I recommend creating the parallax background image this way for several reasons. You can create the image as a whole, but you’re able to save each layer to an individual file. All these files are 960 × 640 pixels in size, which may seem wasteful at first, but you’re not adding the individual images to the game—instead, you’re adding them to a texture atlas. Because TexturePacker removes the surrounding transparent space of each image, it shrinks the individual stripes down to a bare minimum. You can see this in Figure 7-3.

9781430244165_Fig07-02.jpg

Figure 7-2.  Each stripe of the background is on its own layer. This helps in creating the individual images and positioning them in the game

9781430244165_Fig07-03.jpg

Figure 7-3.  The background stripes as they would appear in a texture atlas

Splitting the stripes into individual images is not only helpful for drawing the images at the correct z-order. Strictly speaking, the images bg5.png and bg6.png can be at the same z-order because they don’t overlap, yet I chose to save them into separate files. In Figure 7-3 you can see these two files as the two topmost stripes. Notice how little space they actually use up in the texture atlas; that’s because TexturePacker removed most of the surrounding transparent parts of these images.

Now suppose I left both these stripes in the same 960 × 640 image—one would be at the top and the other at the bottom of the image, with a big gaping hole of transparency in between them. TexturePacker can’t remove the transparent part between the two sprites, so they would have remained as a 960 × 640 image in the texture atlas, which is a lot more space than they take up as individual images.

Splitting the stripes into individual images also helps maintain a high framerate. The iOS devices are very limited in fill rate (that is, the number of pixels they can draw every frame). Because images frequently overlap each other, the iOS device often has to draw the same pixel several times in every frame. The extreme scenario would be a full-screen image on top of another full-screen image. You can see only one of the two images, but the device actually has to draw both. The technical term for that is overdraw. Separating the background into individual stripes with as little overlap as possible reduces the number of pixels drawn.

Tip  By using 16-bit textures (RGB565, RGBA5551, and RGBA4444) or even PVR compressed textures, you can improve the rendering performance of your game. Of course, you lose some image quality, but in many cases it’s hardly noticeable on the iPhone screen, even if you do see some artifacts on your computer’s screen. TexturePacker lets you reduce the color depth of your images while retaining most of the image quality through dithering techniques. And if you experience long load times, saving images in the compressed pvr.czz format is worth a try because this particular image file format loads noticeably faster than other ones.

Re-creating the Background in Code

You may be wondering by now how you can put these images back together in the source code without spending a lot of time properly positioning these stripped-down images. The answer is you don’t have to. Because all these images were saved as full-screen images, TexturePacker stores the image offsets, and cocos2d then uses these offsets when rendering the sprites. All you really have to do is center each of these images on the screen, and they will be at the correct place.

Look at the code for the ParallaxBackground node newly added to the ScrollingWithJoy01 project. The header file is pretty straightforward:

@interface ParallaxBackground : CCNode
{
    CCSpriteBatchNode* spriteBatch;
    int numSprites;
}
@end

I only kept a reference to the CCSpriteBatchNode around because I’ll be accessing it in the code frequently. Storing a node as an instance variable is faster than asking cocos2d for the node via the getNodeByTag method, and more so the more children the node has. Keeping a reference to a node by storing it as an instance variable saves you a few CPU cycles. It’s nothing too dramatic, and it’s certainly not worth keeping several hundreds of member variables around. But it’s very convenient in cases where you need to access a particular node frequently.

In the init method of the ParallaxBackground class, the CCSpriteBatchNode is created, and all seven background images are added from the texture atlas, as shown in Listing 7-1.

Listing 7-1.  Loading the Background Images

-(id) init
{
    if ((self = [super init]))
    {
        CGSize screenSize = [CCDirector sharedDirector].winSize;
 
     // Get the game's texture atlas texture by adding it to the cache
     CCTexture2D* gameArtTexture = [[CCTextureCache sharedTextureCache] ←
         addImage:@"game-art.pvr.ccz"];
 
     // Create the background spritebatch
     spriteBatch = [CCSpriteBatchNode batchNodeWithTexture:gameArtTexture];
     [self addChild:spriteBatch];
 
     // Add the 6 different layer objects and position them on the screen
     for (int i = 0; i < 7; i++)
     {
     NSString* frameName = [NSString stringWithFormat:@"bg%i.png", i];
     CCSprite* sprite = [CCSprite spriteWithSpriteFrameName:frameName];
     sprite.position = CGPointMake(screenSize.width / 2, ←
         screenSize.height / 2);
     [spriteBatch addChild:sprite z:i];
     }
 
     scrollSpeed = 1.0f;
     [self scheduleUpdate];
    }
    return self;
}

First you’re getting the game-art.pvr.czz texture from the CCTextureCache. Normally you’d use the textureForKey method to access the already loaded texture. But if you’re not sure whether the texture is already in the cache, you can also use the addImage method instead of textureForKey. That also loads the texture if it wasn’t cached—otherwise addImage just returns the cached texture. As long as the image is included in the app bundle, addImage is guaranteed to return a valid texture.

With the CCSpriteBatchNode created and set up, the next step is to load the seven individual background images. I deliberately chose to number them from 0 to 6 so you can use stringWithFormat to create the filenames as strings in a very effective way:

NSString* frameName = [NSString stringWithFormat:@"bg%i.png", i];

With that sprite frameName, create a CCSprite as usual and then position it at the center of the screen:

sprite.position = CGPointMake(screenSize.width / 2, screenSize.height / 2);

Of course, once you create an iPad version of this project, the images won’t fit perfectly anymore because they were designed for a screen of 960 × 640 resolution. To create an iPad version, follow the exact same steps except make your original image 1024 × 768 in size, or even double that if you also want to support Retina iPad resolutions. You can then downscale the 960 × 640 images from the higher resolution images easily just by cropping the borders.

Tip  It takes surprisingly little effort to re-create the source image from individual sprite frames in cocos2d, and it’s all thanks to TexturePacker saving the image offsets for you. It’s also a great way to create your game screen layouts. You can have an artist design each screen as separate layers, as many as needed, using the native screen resolution (960 × 640 for iPhone and 2048 × 1536 for iPad). Then export each layer as an individual full-screen file with transparency. Next create a texture atlas from these files and you’ll have the artist-envisioned screen design in cocos2d with no hassle of positioning individual files and no wasted memory either. TexturePacker takes care of scaling down the images to standard resolution, if needed.

Because the ParallaxBackground class is derived from CCNode, you only need to add it to the GameLayer init method after the sprite frames are loaded to add the ParallaxBackground to the game, like this:

-(id) init
{
    if ((self = [super init]))
    {
        sharedGameLayer = self;
 
        // if the background shines through we want to be able to see it!
        glClearColor(1, 1, 1, 1);
 
        // Load all of the game's artwork up front.
        CCSpriteFrameCache* frameCache = [CCSpriteFrameCache sharedSpriteFrameCache];
        [frameCache addSpriteFramesWithFile:@"game-art.plist"];
 
        CGSize screenSize = [CCDirector sharedDirector].winSize;

        ParallaxBackground* background = [ParallaxBackground node];
        [self addChild:background z:-1];
 
        /*
        // add a background image
        CCSprite* background = [CCSprite spriteWithSpriteFrameName:@"background.png"];
        background.position = CGPointMake(screenSize.width / 2, screenSize.height / 2);
        [self addChild:background];
        */
 
        ...

You also need to import the ParallaxBackground.h header file at the top of the GameLayer.m file:

#import "ParallaxBackground.h"

The parallax background replaces the CCLayerColor and the background CCSprite, which were placeholders from Chapter 6, so you also want to remove or comment out any reference to the background sprite or CCLayerColor. In addition, I set the OpenGL clear color to white—later this will help you see a visual artifact that occurs in the early versions of the parallax scrolling code.

Moving the ParallaxBackground

In the ScrollingWithJoy01 project, I also added a quick-and-dirty scrolling of the background stripes. It does show a parallax effect, although the images quickly leave the screen, revealing the blank background behind them. Figure 7-4 isn’t exactly what I had in mind, but I’m getting there.

9781430244165_Fig07-04.jpg

Figure 7-4.  The background stripes are moving, but they’re also leaving the screen forever

Note  You’ll also notice that Figure 7-4 doesn’t look at all like Figure 7-1 or the game project. Figure 7-4 uses dummy graphics to better illustrate the parallax images “leaving” the screen. It’s much harder to see with the star background stripes. You’ll notice this effect much better in motion. What you can see in the image is that the individual stripes have moved at different rates, which is what parallax scrolling is all about.

Listing 7-2 shows that the code to make the background scroll is surprisingly simple if you ignore for a moment the flaw seen in Figure 7-4.

Listing 7-2.  Moving the Background Stripes

-(void) update:(ccTime)delta
{
    for (CCSprite* sprite in spriteBatch.children)
    {
         CGPoint pos = sprite.position;
         pos.x - = (scrollSpeed + sprite.zOrder) * (delta * 20);
         sprite.position = pos;
    }
}

Every background image’s x position gets subtracted a bit every frame to scroll it from right to left. How much an image moves depends on the predefined scrollSpeed plus the sprite’s zOrder. The delta multiplier is used to make the scrolling speed independent of the framerate, which is then multiplied by 20 to make the scrolling reasonably fast. Images that are closer to the screen scroll faster. However, using the zOrder property causes the stripes that should be at the same visual depth to scroll at different speeds.

The position is also multiplied by the delta time to make the scrolling speed independent of the framerate. The delta time itself is just a tiny fraction—it’s the time between two calls to the update method. At exactly 60 frames per second (FPS), it’s 1/60 of a second, which is a delta time of 0.167 seconds. For that reason, I multiply delta by 20 just to get a reasonably fast scroll; otherwise the images move too slowly.

Parallax Speed Factors

Somehow the stripes of the same color need to scroll at the same speed, and the stripes should repeat so that the background doesn’t show up. My solutions to these issues are in the ScrollingWithJoy02 project.

The first change has to do with scrolling speed. I decided to use a NSMutableArray to store the speed factor with which individual stripes move. Other solutions are available, but this allows me to illustrate a key issue of NSMutableArray and, in fact, all iOS SDK collection classes: they can store only objects, never values such as integers and floating-point numbers.

The way around this is to box numbers into an NSNumber object. The following code is the newly added NSMutableArray* speedFactors, which stores floating-point values. The array is defined in the ParallaxBackground class header:

@interface ParallaxBackground : CCNode
{
    CCSpriteBatchNode* spriteBatch;
    int numStripes;
 
    NSMutableArray* speedFactors;
    float scrollSpeed;
}
@end

Then it’s filled with factors in the init method of the ParallaxBackground class. Notice how NSNumber numberWithFloat is used to store a float value inside the array. I explain the code that adds additional background sprites in the next section. I added the following code just above the scheduleUpdate line in the init method:

Listing 7-3.  Modified init methodof the ParallaxBackground class

...
numStripes = 7;
 
// Add 7 more stripes, flip them and position them next to their neighbor stripe
for (int i = 0; i < numStripes; i++)
{
    NSString* frameName = [NSString stringWithFormat:@"bg%i.png", i];
    CCSprite* sprite = [CCSprite spriteWithSpriteFrameName:frameName];
 
    // Position the new sprite one screen width to the right
    sprite.position = CGPointMake(screenSize.width + screenSize.width / 2, ←
     screenSize.height / 2);
 
    // Flip the sprite so that it aligns perfectly with its neighbor
    sprite.flipX = YES;
 
    // Add the sprite using the same tag offset by numStripes
    [spriteBatch addChild:sprite z:i tag:i + numStripes];
}
 
// Initialize the array that contains the scroll factors for individual stripes.
speedFactors = [NSMutableArray arrayWithCapacity:numStripes];
[speedFactors addObject:[NSNumber numberWithFloat:0.3f]];
[speedFactors addObject:[NSNumber numberWithFloat:0.5f]];
[speedFactors addObject:[NSNumber numberWithFloat:0.5f]];
[speedFactors addObject:[NSNumber numberWithFloat:0.8f]];
[speedFactors addObject:[NSNumber numberWithFloat:0.8f]];
[speedFactors addObject:[NSNumber numberWithFloat:1.2f]];
[speedFactors addObject:[NSNumber numberWithFloat:1.2f]];
NSAssert(speedFactors.count == numStripes,
    @"speedFactors count does not match numStripes!");
 
scrollSpeed = 1.0f;
[self scheduleUpdate];

The final assert is simply a safety check for human error. Consider that you may be adding or removing stripes from the background for whatever reason but might forget to adjust the number of values you’re adding to the speedFactors array. If you forget to modify the speedFactors initialization, the assert will remind you of it, instead of potentially crashing the game seemingly at random at a later time.

In Figure 7-5 you can see which speed factor is applied to which stripe. Stripes with higher speed factors move faster than those with slower ones, which creates the parallax effect. Once again, I use dummy graphics to make each individual stripe clearly visible.

9781430244165_Fig07-05.jpg

Figure 7-5.  The speed factors applied to each background stripe

To use the newly introduced speed factors, change theupdate method to this:

-(void) update:(ccTime)delta
{
    for (CCSprite* sprite in spriteBatch.children)
    {
        NSNumber* factor = [speedFactors objectAtIndex:sprite.zOrder];

        CGPoint pos = sprite.position;

        pos.x - = (scrollSpeed * factor.floatValue) * (delta * 50);
        sprite.position = pos;
    }
}

Based on the sprite’s zOrder property, a speed factor NSNumber is obtained from the speedFactors array. This is multiplied by the scrollSpeed to speed up or slow down the movement of individual stripes. You can’t multiply the NSNumber object directly because it’s a wrapper class storing a primitive data type such as float, int, char, or others. NSNumber has a floatValue property that returns the floating-point value stored in it, and it has a number of other properties to retrieve different data types as well. You could also use intValue, even though this NSNumber stores a floating-point value. It’s essentially the same as casting a float to an int. Once again, the delta time is also factored in to make the scrolling speed independent of the framerate.

By using the speedFactors array and giving the same-colored stripes the same factor, the background stripes will now move as expected. But there’s still the issue of making endless scrolling.

Scrolling to Infinity and Beyond

Also in ScrollingWithJoy02 is the first step toward endless scrolling. You’ll find in Listing 7-4, which repeats the snippet of code first introduced in Listing 7-3, seven more background stripes to theCCSpriteBatchNode, although with a slightly different setup.

Listing 7-4.  Adding Off-Screen Background Images

// Add seven more stripes, flip them, and position them next to their neighbor stripe
for (int i = 0; i < numStripes; i++)
{
    NSString* frameName = [NSString stringWithFormat:@"bg%i.png", i];
    CCSprite* sprite = [CCSprite spriteWithSpriteFrameName:frameName];
 
    // Position the new sprite one screen width to the right
    sprite.position = CGPointMake(screenSize.width + screenSize.width / 2, ←
        screenSize.height / 2);
 
    // Flip the sprite so that it aligns perfectly with its neighbor
    sprite.flipX = YES;
 
    // Add the sprite using the same tag offset by numStripes
    [spriteBatch addChild:sprite z:i tag:i + numStripes];
}

The idea is to add one more stripe of the same type each. They’re positioned so that they align with the right end of the original stripe’s position. In effect, this doubles the total width of the background stripes, and that’s enough for endless scrolling. But I’ll get to that shortly.

First I need to point out that the new neighboring images’ x coordinates are flipped (mirrored along the y-axis) so that the images match visually where they’re aligned, to avoid any sharp edges. The new images also get a different tag number, one that’s offset by the number of stripes in use. This way it’s easy to get to the neighboring stripe by either adding or deducting numStripes from the tag number.

Right now, the background image scrolls just a bit longer before showing the blank canvas behind the images. But you’re not stopping there; instead you’ll complete the effort and add seamless infinite scrolling.

First change the anchorPoint property of the original stripes to make things a little easier. The x position is set to 0, and the stripes are left-aligned with the anchorPoint’s x coordinate also set to 0. The same goes for the additional, flipped stripes, which now only need to be offset by the screen width to make them align with the original stripes. The changes to the init method of the ParallaxBackground class are highlighted in Listing 7-5.

Listing 7-5.  Adding Off-Screen Background Images

numStripes = 7;
 
// Add the 7 different stripes and position them on the screen
for (int i = 0; i < numStripes; i++)
{
    NSString* frameName = [NSString stringWithFormat:@"bg%i.png", i];
    CCSprite* sprite = [CCSprite spriteWithSpriteFrameName:frameName];
    sprite.anchorPoint = CGPointMake(0, 0.5f);
    sprite.position = CGPointMake(0, screenSize.height / 2);
    [spriteBatch addChild:sprite z:i tag:i];
}
 
// Add 7 more stripes, flip them and position them next to their neighbor stripe
for (int i = 0; i < numStripes; i++)
{
    NSString* frameName = [NSString stringWithFormat:@"bg%i.png", i];
    CCSprite* sprite = [CCSprite spriteWithSpriteFrameName:frameName];
 
    // Position the new sprite one screen width to the right
    sprite.anchorPoint = CGPointMake(0, 0.5f);
    sprite.position = CGPointMake(screenSize.width, screenSize.height / 2);
 
    // Flip the sprite so that it aligns perfectly with its neighbor
    sprite.flipX = YES;
 
    // Add the sprite using the same tag offset by numStripes
    [spriteBatch addChild:sprite z:i tag:i + numStripes];
}

The stripes’ anchorPoint is changed from its default (0.5f, 0.5f) to (0, 0.5f) to make them left-aligned. Doing that makes it easier to work with the parallax sprites, because in this particular case you don’t want to have to take into account that that texture’s origin and the sprite’s x position aren’t at the same location. Figure 7-6 shows how this makes it easier to calculate the x position.

9781430244165_Fig07-06.jpg

Figure 7-6.  Anchor point moved to the left for simplicity

You can see in Listing 7-6 how this is helpful in the changed update method, which now gives us endless scrolling.

Listing 7-6.  Moving the Image Pairs Seamlessly

-(void) update:(ccTime)delta
{
    for (CCSprite* sprite in spriteBatch.children)
    {
     NSNumber* factor = [speedFactors objectAtIndex:sprite.zOrder];
 
     CGPoint pos = sprite.position;
     pos.x - = (scrollSpeed * factor.floatValue) * (delta * 50);
 
     // Reposition stripes when they're out of bounds
     CGSize screenSize = [CCDirector sharedDirector].winSize;
     if (pos.x < −screenSize.width)
     {
         pos.x + = screenSize.width * 2;
     }

     sprite.position = pos;
    }
}

The stripes’ x position now only needs to be checked if it’s less than negative screen width and, if it is, multiplied by twice the screen width. Essentially, this moves the sprite that has just left the screen on the left side to the right side, just outside the screen. This repeats forever with the same two sprites, giving the effect of endless scrolling.

Tip  Notice that the background of the screen scrolls, but the ship stays in place. Inexperienced game developers often have the misconception that everything on the screen needs to be scrolling to achieve the effect of game objects passing by the player character as he progresses through the game world. Instead, you can much more easily create the illusion of objects moving on the screen by moving the background layers but keeping the player character fixed in place. Popular examples of games that make use of this visual illusion are Super Turbo Action Pig, Canabalt, Super Blast, DoodleJump, and Zombieville USA. Typically, the game objects scrolling into view are randomly generated shortly before they appear, and when they leave the screen they’re removed from the game. Chapter 11 makes use of the same effect where the player character remains in the center of the screen while only the game world underneath the player is actually moving, giving the impression that the player moves around in the world.

Fixing the Flicker

So far, so good. Only one issue remains. If you look closely, you’ll notice a vertical, flickering line appearing where the two background stripes meet. That’s where they align with each other. This line appears because of rounding errors in their positions in combination with subpixel rendering. From time to time, a 1-pixel-wide gap can appear for just a fraction of a second. Sometimes it may appear every other frame, other times only occasionally; it depends on the scrolling speed. It’s still noticeable and is something you should get rid of for a commercial-quality game.

The simplest solution is to overlap the stripes by just 1 pixel. In the ScrollingWithJoy02 project change the initial positions for the flipped background stripes in the init method by subtracting 1 pixel from the x position:

sprite.position = CGPointMake(screenSize.width - 1, screenSize.height / 2);

This also requires updating the stripe-repositioning code in the update method so that the stripe is positioned 2 pixels farther to the left than before:

// Reposition stripes when they're out of bounds
if (pos.x < −screenSize.width)
{
    pos.x + = (screenSize.width * 2) - 2;
}

Why 2 pixels? Well, because the initial position of the flipped stripes is already moved to the left by 1 pixel, we have to move all of them 2 pixels to the left each time they flip around to maintain the same distance and to keep the overlap of 1 pixel.

An alternative solution would be to update only the position of the currently leftmost sprite and then find the sprite that is aligned to the right of it and offset it by exactly the screen width. This way, you also avoid the rounding errors. Figure 7-7 shows the finished result with the final graphics.

9781430244165_Fig07-07.jpg

Figure 7-7.  The result: an infinitely scrolling parallax background

Repeat, Repeat, Repeat

Another neat trick deserves mention. You can set any texture to repeat over a certain rectangular area. If you make this area big enough, you can have this texture repeat nearly endlessly. At least several thousand pixels or dozens of screen areas can be covered with a repeating texture, with no penalty to memory usage.

The trick is to use the GL_REPEAT texture parameter supported by OpenGL. But it only works with square images that are exactly a power of 2, like 32 × 32 or 512 × 512 pixels. Listing 7-7 shows the code.

Listing 7-7.  Repeating Background with GL_REPEAT

CGRect repeatRect = CGRectMake(−5000, -5000, 5000, 5000);
CCSprite* sprite = [CCSprite spriteWithFile:@"square.png" rect:repeatRect];
ccTexParams params =
{
    GL_LINEAR, // texture minifying function
    GL_LINEAR, // texture magnification function
    GL_REPEAT, // how texture should wrap along X coordinates
    GL_REPEAT // how texture should wrap along Y coordinates
};
[sprite.texture setTexParameters:&params];

In this case, you must initialize the sprite with a rect that determines the area the sprite will occupy. The ccTexParams struct is initialized with the wrap parameters for the texture coordinates set to GL_REPEAT. Don’t worry if that doesn’t mean anything to you. These OpenGL parameters are then set on the sprite’s texture using the CCTexture2D method setTexParameters.

The result is a tiled area repeating the same square.png image over and over again. If you move the sprite, the whole area covered by the repeatRect is moved. You could use this trick to remove the bottommost background stripe and replace it with a smaller image that simply repeats. I’ll leave that up to you as an exercise.

A Virtual Joypad

Because the iOS devices all use a touchscreen for input and have no buttons, D-pads, or analog joypads like conventional mobile gaming devices do, you need something called a virtual joypad. This emulates the behavior of digital or analog thumbsticks by allowing you to touch the screen where the digipad or thumbstick is displayed and move your finger over it to control the action on the screen. Buttons are also designated areas of the touchscreen that you can tap or hold to cause actions on the screen. Figure 7-8 shows a virtual joypad in action.

9781430244165_Fig07-08.jpg

Figure 7-8.  A skinned analog thumbstick and fire button created with SneakyInput

Caution  Virtual joypads resemble joypad controllers in looks but never in feel. The user is still touching a flat surface with no feedback to the fingers as to how far the virtual analog stick was moved or whether the virtual button has actually been pressed or was missed. A number of players feel uneasy controlling a game with virtual joypads. It has become more or less common wisdom that a virtual joypad should have no more than two or three controller elements, usually a stick/pad (sometimes limited to two directions) and one or two buttons. Adding more can make your game exponentially harder to control. Also consider that you can often replace at least one axis of the directional buttons with input from the accelerometer, so that the user can tilt the device to move left or right instead of holding the corresponding virtual buttons.

Introducing SneakyInput

Over time, many a developer has faced the problem of implementing a virtual joypad. There are many ways to go about it, and even more ways to fail at it. But why spend time on that if there’s a ready-to-use solution?

This is generally sound advice. Before you program anything that seems reasonably common, which others have probably worked on before, always check to see whether a general-purpose solution is available that you can just use instead of having to spend a lot of time creating it yourself. In this case, SneakyInput is just too good to be ignored.

SneakyInput was created by Nick Pannuto, with skinning examples by CJ Hanson. SneakyInput is open source software and free to download, but if you like this product, please consider making a donation to Nick Pannuto here: http://pledgie.com/campaigns/9124.

Kobold2D users don’t need to concern themselves—SneakyInput is already part of the Kobold2D distribution. Kobold2D users can skip the next two sections.

Download SneakyInput

The SneakyInput source code is hosted on GitHub, a social coding web site: http://github.com/sneakyness/SneakyInput.

It may not be immediately obvious what you have to do to download the source code from GitHub. When you click a file, you see the actual source code displayed in your browser. But you want the full source code project, not individual files. What you do is locate either the Downloads tab below the upper right-hand corner of the web site, or the ZIP button on the left side of the page just between the Clone in Mac and HTTP|Git Read-Only buttons. Then save the file to your computer and extract it.

Because SneakyInput comes with an example project that has cocos2d integrated, it’s likely that the cocos2d version used by the SneakyInput demo project may not be the most current version, or may not even compile. For the rest of the chapter that doesn’t matter, as I’ll cherry-pick the code that works with cocos2d 2.0.

Integrating SneakyInput

You already have a working project and you don’t want to use the project provided by SneakyInput. So how do you get it to work with your project?

This issue isn’t limited to SneakyInput but possibly any source code project you can download that comes already bundled with its own version of cocos2d. In most cases, and as long as the programming language of that source code is Objective-C, you only have to figure out which of the project’s files are necessary and add them to your own project. There’s no clear guideline, however, because every project is different.

I can tell you which files you need to add to your project regarding SneakyInput, however. It consists at its core of four classes:

  • SneakyButton and SneakyButtonSkinnedBase
  • SneakyJoystick and SneakyJoystickSkinnedBase

The remaining files aren’t needed but may serve as references, except for the ColoredCircleSprite and ColoredSquareSprite classes, which are incompatible with cocos2d 2.0. Figure 7-9 shows the selection in the Add Files To … dialog.

9781430244165_Fig07-09.jpg

Figure 7-9.  You need these files to get SneakyInput to work in your own project; the other files are used only for example code

The HelloWorldScene class is created by a cocos2d project template and most likely doesn’t contain anything but example code. Of course, I already have an AppDelegate in my project, so I don’t need to be adding SneakyInput’s AppDelegate class—it might be in conflict with the existing AppDelegate. Then there are two classes explicitly suffixed with Example, which indicates that these files are not core classes for SneakyInput but further example code.

After adding the SneakyInput button and joystick classes, your project won’t compile anymore. That’s because SneakyInput classes haven’t been converted to ARC. You should do so now by selecting Edit image Refactor image Convert to Objective-C ARC … from Xcode’s menu. Make sure only the ScrollingWithJoy03 project is selected before running the ARC conversion check. When Xcode is done checking you’ll see a preview; click Save to accept all changes.

Tip  If you ever run into third-party code that can’t be refactored to ARC, you can do one of two things. One is to compile that code as a separate static library with ARC disabled and linking that library with your app, just like what you did with cocos2d in Chapter 2. The alternative is to add the compiler flag –fobjc-no-arc to every file that should be compiled without ARC. To add the flag, in the target’s Build Phases tab expand the Compile Sources section and enter the flag on the right-hand side to the Compiler Flags column. Note that code requiring either of these approaches may be generally incompatible with ARC. Consult with the author or other users of the code to find out whether the code is safe to use with ARC.

Now the code will compile but not without four warnings complaining about sharedDispatcher being deprecated. You can choose to ignore the warnings but eventually you’re going to have to fix them, so you may as well do it now. The lines in question all start with [CCTouchDispatcher sharedDispatcher], for example:

[[CCTouchDispatcher sharedDispatcher] removeDelegate:self];

To fix the warning, change the lines so that they use the touch dispatcher provided by CCDirector instead:

[[CCDirector sharedDirector].touchDispatcher removeDelegate:self];

Touch Button to Shoot

Let’s try this. With the SneakyInput source code added to the project ScrollingWithJoy03, the first goal is to add a button that allows the player to shoot bullets from the player’s ship. You’re going to add a separate InputLayer class to the project by adding an Objective-C class template file and making sure it’s derived from CCLayer. Add the InputLayer class instance to the GameLayer class. Listing 7-8 updates thescene method to add the new InputLayer to it, and both layers get a tag just in case you need to identify them later.

Listing 7-8.  Adding the InputLayer to the GameScene

#import "InputLayer.h"
...
 
+(id) scene
{
    CCScene* scene = [CCScene node];
    GameScene* layer = [GameScene node];
    [scene addChild:layer z:0 tag:GameSceneLayerTagGame];
    InputLayer* inputLayer = [InputLayer node];
    [scene addChild:inputLayer z:1 tag:GameSceneLayerTagInput];
    return scene;
}

The new tags are defined in the GameLayer header file just above the existing GameSceneNodeTags enum as follows:

typedef enum
{
    GameSceneLayerTagGame = 1,
    GameSceneLayerTagInput,
} GameSceneLayerTags;

With the InputLayer in place, the next step is to add the header files for the SneakyInput files you want to use to the InputLayer.h header file. I’m not picky, and you’re probably going to use most of the classes, so just add all the SneakyInput header files:

#import <Foundation/Foundation.h>
#import "cocos2d.h"
#import "GameLayer.h"
#import "Ship.h"
 
// SneakyInput headers
#import "SneakyButton.h"
#import "SneakyButtonSkinnedBase.h"
#import "SneakyJoystick.h"
#import "SneakyJoystickSkinnedBase.h"
 
@interface InputLayer : CCLayer
{
    SneakyButton* fireButton;
    SneakyJoystick* joystick;
 
    ccTime totalTime;
    ccTime nextShotTime;
}
@end

In addition, you want to add a SneakyButton member variable for easier access to the button you’re going to create now. The addFireButton method does this in Listing 7-9. I explain the totalTime and nextShotTime variables as well as the SneakyJoystick later.

Listing 7-9.  Creating a SneakyButton

-(id) init
{
    if ((self = [super init]))
    {
       [self addFireButton];
       [self scheduleUpdate];
    }
    return self;
}
 
-(void) addFireButton
{
    float buttonRadius = 80;
    CGSize screenSize = [CCDirector sharedDirector].winSize;
 
    fireButton = [[SneakyButton alloc] initWithRect:CGRectZero];
    fireButton.radius = buttonRadius;
    fireButton.position = CGPointMake(screenSize.width - buttonRadius, buttonRadius);
    [self addChild:fireButton];
}

SneakyButton doesn’t use the CGRect parameter of the button’s initWithRect method, which is why you’re simply passing CGRectZero. The actual touch code uses the radius property to determine whether the button should react to the touch. The button in this case should be neatly tucked into the lower right-hand corner. Subtracting the buttonRadiusfrom the screen width and setting its height to buttonRadius places it exactly at the desired location.

Tip  The use of the buttonRadius variable lets you change the radius of the button in one place. Otherwise, you’d have to update several values in several places. This is not only extra work for a value you might want to tweak several times before you get it exactly the way you want, but it can also introduce subtle bugs, because you’re a human and you tend to forget things, such as changing that one value over there. Suddenly the button is offset—or worse, the input doesn’t match the button’s location.

The InputLayer class also schedules the update method. For now, the update method is used only to log whether the fire button was touched:

-(void) update:(ccTime)delta
{
    if (fireButton.active)
    {
     CCLOG(@"FIRE!!!");
    }
}

Instead of shooting a bullet, keep things simple for now and simply log a successful button press. If you try the ScrollingWithJoy03 project now, you’ll notice that there isn’t any button drawn. Yet when you touch the screen at the lower right-hand corner, you’ll see the “FIRE!!!” message appear in the Debugger Console window. So, all is well and right, except that the button can’t be seen—which you need to fix.

Skinning the Button

Eeeww! No, it’s not what you think. Skinning in computer graphics refers to giving an otherwise featureless object a texture or simply a different look. In this case, you want to actually see your button, so you need an image for that.

I created four button images that are 100 × 100 pixels in size—twice the final button radius of 50. The button images come in four variations: Default, Pressed, Activated, and Disabled. The default state is what the button looks like when it isn’t pressed, which should make it obvious what the Pressed state is. The Activated state comes into play only for toggle buttons, meaning the toggle button is active, or on. The Disabled image is used if the button currently has no function. For example, when the ship’s weapons are overheated and you can’t shoot for a few seconds, you could disable the button, and it would show the Disabled image.

For the shoot button, you only need the Default and Pressed images. Listing 7-11 shows the updated addFireButton method of the InputLayer class.

Listing 7-11.  Replacing Listing 7-9 with a Skinned Button

float buttonRadius = 50;
CGSize screenSize = [CCDirector sharedDirector].winSize;
 
CCSprite* idle = [CCSprite spriteWithSpriteFrameName:@"fire-button-idle.png"];
CCSprite* press = [CCSprite spriteWithSpriteFrameName:@"fire-button-pressed.png"];
 
fireButton = [[SneakyButton alloc] initWithRect:CGRectZero];
fireButton.isHoldable = YES;

SneakyButtonSkinnedBase* skinFireButton = [[SneakyButtonSkinnedBase alloc] init];
skinFireButton.button = fireButton;
skinFireButton.defaultSprite = idle;
skinFireButton.pressSprite = press;
skinFireButton.position = CGPointMake(screenSize.width - buttonRadius, buttonRadius);
[self addChild:skinFireButton];

The code initializes thefireButton as usual except that I made it holdable, which means you can keep it pressed down for a continuous stream of bullets. It also doesn’t set the radius property anymore, because the images of the SneakyButtonSkinnedBase class determine the radius now. The fireButton is later assigned to the skinFireButton.button property, so that the two work together.

Instead of positioning the fireButton, the skinned button is now determining the position of the button on the screen; the actual fireButton is updated accordingly by the SneakyButtonSkinnedBase class.

At this point, it makes sense to also write the firing code; Listing 7-12 shows theupdate method now sending the fire message to the GameScene class. This is also where the totalTime and nextShotTime variables come into play.

Listing 7-12.  Shooting Bullets Whenever the Fire Button Is Active

-(void) update:(ccTime)delta
{
    totalTime + = delta;
 
    GameLayer* game = [GameLayer sharedGameLayer];
    Ship* ship = [game defaultShip];
 
    if (fireButton.active && totalTime > nextShotTime)
    {
        nextShotTime = totalTime + 0.5f;
        [game shootBulletFromShip:ship];
    }
 
    // Allow faster shooting by quickly tapping the fire button
    if (fireButton.active == NO)
    {
        nextShotTime = 0;
    }
}

The two ccTime variables,totalTime and nextShotTime, are used to limit the number of bullets the ship will emit to two per second. If the fire button isn’t active (meaning it isn’t pressed), the nextShotTime is set to 0 so that the next time you press the button a shot is guaranteed to be fired. Tap the button quickly, and you should be able to shoot more bullets than with continuous fire.

You have to make a few minor changes in order to have the ship shoot bullets when pressing the fire button. Add the declaration of the defaultShip method to the GameLayer interface as well as the GameSceneNodeTagShip enum:

typedef enum
{
        GameSceneNodeTagBullet = 1,
        GameSceneNodeTagBulletSpriteBatch,
        GameSceneNodeTagShip,

} GameSceneNodeTags;
 
@interface GameLayer : CCLayer
{
    NSUInteger nextInactiveBullet;
}
 
+(id) scene;
+(GameLayer*) sharedGameLayer;
-(CCSpriteBatchNode*) bulletSpriteBatch;
-(void) shootBulletFromShip:(Ship*)ship;
-(Ship*) defaultShip;
@end

In the implementation file of GameLayer, find the lines where the ship class is created in the init method and assign the ship the GameSceneNodeTagShip tag so that it’s easily accessed later on. Alternatively you can also use an instance variable to store the reference to the ship object.

// add the ship
Ship* ship = [Ship ship];
ship.position = CGPointMake(80, screenSize.height / 2);
ship.tag = GameSceneNodeTagShip;
[self addChild:ship z:10];

Then add the new defaultShip method anywhere in the @implementation section of the GameLayer class.

-(Ship*) defaultShip
{
    CCNode* node = [self getChildByTag:GameSceneNodeTagShip];
    NSAssert([node isKindOfClass:[Ship class]], @"node is not a Ship!");
    return (Ship*)node;
}

What’s left is to stop the ship from automatically shooting bullets all the time. To do that, open the Ship implementation file and comment out or remove the shootBulletFromShip line in the update method:

-(void) update:(ccTime)delta
{
    // Shooting is relayed to the game scene
    //[[GameLayer sharedGameLayer] shootBulletFromShip:self];
}

Now you can shoot bullets on demand whenever you press or hold the fire button. If you tap the button repeatedly in quick succession, you should be able to shoot bullets more rapidly.

Controlling the Action

You can’t fly a ship without some form of input. This is where SneakyJoystick lends a helping hand, I mean, a helping virtual thumbstick. For the joystick, go right ahead and create a skinned one in the addJoystick method in Listing 7-13. You should add this method to the InputLayer class.

Listing 7-13.  Adding a Skinned Joystick

-(void) addJoystick
{
    float stickRadius = 50;
 
    joystick = [[SneakyJoystick alloc] initWithRect: ←
        CGRectMake(0, 0, stickRadius, stickRadius)];
    joystick.autoCenter = YES;
    joystick.hasDeadzone = YES;
    joystick.deadRadius = 10;
 
    CCSprite* back = [CCSprite spriteWithSpriteFrameName:@"joystick-back.png"];
    CCSprite* thumb = [CCSprite spriteWithSpriteFrameName:@"joystick-stick.png"];
 
    SneakyJoystickSkinnedBase* skinStick = [[SneakyJoystickSkinnedBase alloc] init];
    skinStick.joystick = joystick;
    skinStick.backgroundSprite.color = ccYELLOW;
    skinStick.backgroundSprite = back;
    skinStick.thumbSprite = thumb;
    skinStick.position = CGPointMake(stickRadius * 1.5f, stickRadius * 1.5f);
    [self addChild:skinStick];
}

The SneakyJoystick is initialized with a CGRect, and contrary to the SneakyButton, the CGRect is actually used to determine the joystick’s radius. I set the joystick to autoCenter so that the thumb controller jumps back to the neutral position, like most real-world game controllers. The dead zone is also enabled; this is a small area defined by the deadRadius in which you can move the thumb controller without any effect. This gives users a certain radius where they can keep the thumb controller centered. Without the dead zone, it would be almost impossible to center the thumb controller manually.

The SneakyJoystickSkinnedBase is positioned a small distance away from the edge of the screen. The button’s position and size may not be ideal for the game, but it demonstrates the controls better. If you align the thumb controller with the screen edges, it’s too easy to inadvertently move your finger off the touchscreen and thus lose control of the ship.

Before you can test the joystick you also have to send the addJoystick message to the InputLayer class. Just add this next to the addFireButton in the init method:

-(id) init
{
    if ((self = [super init]))
    {
       [self addFireButton];
       [self addJoystick];
       [self scheduleUpdate];
    }
    return self;
}

Tip  Gray areas are useful! I mean gray images like joystick-back.png. By using just grayscale colors, you can colorize (tint) the image using the color property of the sprite. You can create red, green, yellow, blue, magenta, and other colored versions of the same image, saving both download size and in-game memory. The only drawback is that its’s a flat color, so instead of shades of gray, your image uses shades of red. This trick works best with images that are supposed to be shades of a single color.

You want the thumbstick on the screen to be used to control the ship, of course. As usual, theupdate method processes the input, as shown in Listing 7-14. You can add the changes at the end of the method.

Listing 7-14.  Moving the Ship Based on Joystick Input

-(void) update:(ccTime)delta
{
    ...
    // Velocity must be scaled up by a factor that feels right
    CGPoint velocity = ccpMult(joystick.velocity, 7000 * delta);
    ship.position = CGPointMake(ship.position.x + velocity.x * delta,←
        ship.position.y + velocity.y * delta);
}

You use the velocity property of the joystick to change the ship’s position, but not without scaling it up. The velocity values are a tiny fraction of a pixel, so you need to multiply the velocity using cocos2d’s ccpMult method, which takes a CGPoint and a float factor, for the joypad velocity to have a noticeable effect. The scale factor is arbitrary; it’s just a value that feels good for this game.

To ensure smooth movement even if the update method is called at uneven intervals, factor in the update method’s delta parameter as well. The delta parameter is passed by cocos2d and contains the time elapsed since the update method was last called. This isn’t strictly necessary, but it’s good practice. If you don’t do it, you run the risk that the ship will move more slowly whenever the framerate drops below 60 fps. Tiny things like these can be pretty annoying to players, and as a game developer your goal is the exact opposite of annoying players.

At this point, moving the ship outside the screen area is still possible. I bet you’d like for the ship to stay on the screen as much as I do. And you may be tempted to add this code directly to the InputLayer where the ship’s position is updated. That brings up a question: do you want to prevent the joystick input from moving the ship outside the screen, or do you want to prevent the ship from ever being able to move outside the screen, regardless of who or what changes its position? The latter is the more general solution and preferable in this case. To do so, you only need to override the setPosition method in the Ship class by adding the code shown in Listing 7-15 to the Ship class.

Listing 7-15.  Overriding theShip’s setPosition Method

// Override setPosition to keep the ship within bounds
-(void) setPosition:(CGPoint)pos
{
    CGSize screenSize = [CCDirector sharedDirector].winSize;
    float halfWidth = contentSize_.width * 0.5f;
    float halfHeight = contentSize_.height * 0.5f;
 
    // Cap the position so the ship's sprite stays on the screen
    if (pos.x < halfWidth)
    {   pos.x = halfWidth;
    }
    else if (pos.x > (screenSize.width - halfWidth))
    {
        pos.x = screenSize.width - halfWidth;
    }
 
    if (pos.y < halfHeight)
    {
        pos.y = halfHeight;
    }
    else if (pos.y > (screenSize.height - halfHeight))
    {
        pos.y = screenSize.height - halfHeight;
    }
 
    // Must call super with the new position
    [super setPosition:pos];
}

Every time the ship’s position property changes, the preceding code performs the check to see whether the ship’s sprite is still inside the screen boundaries. If not, then the x or y coordinate is set to a distance of half the contentSize away from the respective screen border. This prevents any part of the player ship’s sprite from leaving the screen.

Because position is a property, the setPosition method gets called by this code:

ship.position = CGPointMake(200, 100);

You can override other base class methods in this way to change the behavior of your game objects. For example, if an object is allowed to be rotated only between 0 and 180 degrees, you’d override the setRotation:(float)rotation method and add the code to limit the rotation.

Digital Controls

You can turn the SneakyJoystick class into a digital controller as well, often referred to as a D-pad. The necessary code changes are minimal:

joystick = [SneakyJoystick joystickWithRect:CGRectMake(0, 0, stickRadius, stickRadius)];
joystick.autoCenter = YES;
 
// Now with fewer directions
joystick.isDPad = YES;
joystick.numberOfDirections = 8;

The dead zone properties can be removed—they’re not needed for a digital controller. The joystick is set to digital controls by setting the isDPad property to YES.

You can also define the number of directions. While D-pads regularly have four directions, in many games you can keep two directions pressed at the same time to have the character move in a diagonal direction. To achieve the same effect, set the numberOfDirections property to 8. SneakyJoystick automatically ensures that these directions are evenly divided onto the thumbpad controller. Of course, you’ll get strange results if you set the number of directions to 6, but then again, maybe that’s exactly what you need to travel across a hexagonal tile map.

Summary

In this chapter you learned several tricks to make an effective parallax scrolling background. Not only can you scroll the background infinitely and without flickering edges, you also learned how to properly separate the parallax layers so TexturePacker can cut down on unnecessary transparent areas while keeping the image’s offset so you don’t have to fumble with positioning the parallax layers.

The second half of this chapter introduced you to SneakyInput, an open source project to add virtual thumbsticks and buttons to any cocos2d game. It may not be perfect, as it needs a few tweaks to work with ARC and the latest cocos2d 2.0 version—unless you’re using Kobold2D, of course—but it’s good enough for most games, and it definitely beats writing your own virtual thumbstick code.

The ship is now controllable with a virtual joypad and stays within the screen’s boundaries while moving, and it’s able to shoot with the press of a button—but the game is still lacking something. A shoot-’em-up isn’t a shoot-’em-up without something to shoot down, is it? The next chapter addresses that issue.

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

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