Pages

Sunday, November 13, 2011

A Simple Arcade Game With Sparrow: Part 2


The Sparrow Framework on iOS: Part II
Last month I showed you how to start using the Sparrow Framework, and make a very basic game iOS game. This month we are going to expand that same game by adding in some of Sparrow’s more advanced features, primarily graphics. The source code of this demo has been updated and is available for download from Github; https://github.com/matt62king/SparrowDemo.
I think we could all agree that graphics are the most important part of a game;  wether us coder’s want to admit it or not. I am going to start this tutorial with a graphics update to the game. Last month the focus was using Sparrow, so the graphics were just simple OpenGL renderings. As luck has it, Sparrow has great solutions for handling images in your game. 
OpenGL creates images by rendering textures. Basically it draws a rectangle and lays the image onto it. The SPImage class takes care of this work for you.  
SPImage has several options for using images, the preferred method is by using a texture atlas, a.k.a sprite sheets. Texture atlas are a group of images combined together to make one large image. The idea is that you load the large image once and crop out sections of it to display the individual images you need. It is not as difficult as it sounds, and it will greatly increase the performance of the game.
Sparrow has built in methods that make using a texture atlas’s very simple. The first step is create an atlas. Code‘n’Web has free Mac App called Texture Packer, that will create texture atlases, and give a Sparrow compatible output. Texture Packer can be downloaded from here: http://www.texturepacker.com/. This app has great GUI, all you have to do is add the images you want, select Sparrow as the output format, and click Publish. 

The output will give and image file, and an xml file that is used for cropping the atlas. The xml file for Sparrow will look like this:
<?xml version="1.0" encoding="UTF-8"?>
<TextureAtlas imagePath="Textures.png">
    <!-- Created with TexturePacker -->
    <!--  -->
    <SubTexture name="Background" x="0" y="0" height="480" width="320"/>
    <SubTexture name="Invader" x="320" y="0" height="30" width="30"/>
    <SubTexture name="SpaceShip" x="350" y="0" height="30" width="30"/>
</TextureAtlas>
And the image file will look like this:

Now that we have a texture atlas we can start adding the images to the game. OpenGL can support a texture atlas that is 1024x1024, so for most iOS games you will not need more than one atlas. Remember the idea is that you load the atlas one time and then crop images from it. It is easiest to do this by creating a singleton instance of the SPTextureAtlas class. 
A singleton can be created by adding a category of the SPStage class. In the Game.h file define a category and some methods to set and use an instance of SPTextureAtlas. Add the following after the @end marker:
@interface SPStage (Game)
+ (void)setStageTextureAtlas:(SPTextureAtlas *)texture;
+ (SPTextureAtlas *)atlas;
+ (void)removeTextureAtlas;
@end
The implementation of this category is pretty simple. The +setStageTextureAtlas: method retains an instance, +atlas returns the instance, and +removeTextureAtlas releases the instance. Go to the Game.m file and add the following after the @end marker:
@implementation SPStage (Game)
SPTextureAtlas *mAtlas = nil;
+ (void)setStageTextureAtlas:(SPTextureAtlas *)texture {
    mAtlas = [texture retain];
}
+ (SPTextureAtlas *)atlas {
    return mAtlas;
}
+ (void)removeTextureAtlas {
    mAtlas = nil;
    [mAtlas release];
}
Now that the set-up is complete we are ready to start adding images to the game. We can start with the background image. This background image is a little to dark so we will need to lighten it up a bit. We will do this by putting a white background behind it and making the image semi transparent. In Game.m go to the top of the -initWithWidth:height: method and add a white square the covers the whole screen:
SPQuad *whiteQuad = [SPQuad quadWithWidth:320.0 height:480.0];        whiteQuad.x = 0.0;
whiteQuad.y = 0.0;
whiteQuad.color = 0xffffff;
        
[self addChild:whiteQuad];
Next we can create and store our texture atlas:
SPTextureAtlas *atlas = [[SPTextureAtlas alloc] initWithContentsOfFile:@"Textures.xml"];
[SPStage setStageTextureAtlas:atlas];
[atlas release];
SPTextureAtlas has several methods for creating textures. For this case we use the -initWithContentsOfFile: method. When using this method you need to pass the xml file that was generated from the Texture Packer app. It is important to make sure that the png file that holds your textures is given the same name, if it does not have the same name Sparrow will not be able to find it. 
After the instance is created it can be set with the +setStageTextureAtlas category method we made earlier.  Then the instance is stored in a singleton, so the one that we created can be released. Make sure you call [SPStage removeTextureAtlas]; in the  dealloc method to make sure there are no leaks. 
Next we can add an background image to the game using SPImage. For this case we will use the initWithTexture: method. This method needs an instance of SPTexture for a parameter. First create an instance of SPTexture from our texture atlas:
SPTexture *backgroundTexture = [[SPStage atlas] textureByName:@"Background"];
Calling -textureByName on our instance of SPTextureAtlas will clip the image of the texture atlas for us. You can find the name of the texture by looking at the xml file that was created by Texture Packer. Now we can use the initWithTexture: method of SPImage to create the image. Other than the creation, SPImage works just like any other display object. Add the following lines to make the image:
SPImage *background = [[SPImage alloc] initWithTexture:backgroundTexture];
background.x = 0.0;
background.y = 0.0;
background.height = 480.0;
background.width = 320.0;
background.alpha = 0.75;
[self addChild:background];
[background release];
If you build and run the project now, you will see the planets on the background and the two SPQuad from before. There is one little fix to make. If you double tap the ship to shoot a laser you will notice that you do not see anything happen. That is because we set the laser to be behind all the other objects. To fix this go to the -onShoot: in the Game.m file. Find [self addChild:laser atIndex:0]; and replace it with [self addChild:laser atIndex:2];. This will put the laser on top of the two background objects that we added. 
Use the same methods to create images for the ship and enemy sprites. Make sure you import Game.h into the Ship and Enemy classes so the SPStage category is usable. In the Ship.m’s init method replace the SPQuad creation, setting, and display with:
SPTexture *ship = [[SPStage atlas] textureByName:@"SpaceShip"]; 
         
SPImage *shipImage = [[SPImage alloc] initWithTexture:ship]; shipImage.x = -shipImage.width/2.0;
shipImage.y = -shipImage.height/2.0;
shipImage.height = 30.0;
shipImage.width = 30.0;
[self addChild:shipImage];
        
[shipImage release];
Do the same for the enemy in Enemy.m. Notice for the enemyImage instance, the color property is set. This is because the image used is semi transparent so setting the color property darkens it. 
SPTexture *enemyTexture = [[SPStage atlas] textureByName:@"Invader"];
        
SPImage *enemyImage = [[SPImage alloc] initWithTexture:enemyTexture];
enemyImage.color = 0x0000ff;
enemyImage.x = -enemyImage.width/2.0;
enemyImage.y = -enemyImage.height/2.0;
[self addChild:enemyImage];
        
[enemyImage release];
Remember in the last version we rotated the ship 45 degrees to make a diamond shape instead of a square. Now that the ship is an image we need to fix that. Go into the Game.m file, in the -initWithWidth:height: method find the ship.rotation = SP_D2R(45); and delete it. Build and run the game and you should see this:

  Now that we have a better looking game we can make it more interesting to play.
One thing we can do is add more enemies to give the game some challenge. To accomplish this we will have to make some modifications to the Game and Enemy classes. Starting in the Game.h file, add the following instance variables:
NSMutableSet *mEnemies;
float mTimePassed;
We will use a NSMutableSet to hold instances of our enemies, and the float will be used to count the time for new enemies to be added. There is a bit of fixes to make in the Game.m file. Starting from the top add a variable above the -initWithWidth:height: method, and remove the BOOL variable that is already there.
static float kEnemyDispatchTime = 1.5;
Inside the -initWithWidth:height: method insert the following lines at the top to initialize the NSMutableSet and set the mTimePassed to zero. Do not forget to release the mEnemies in the dealloc method.
mEnemies = [[NSMutableSet alloc] initWithCapacity:1];
mTimePassed = 0.0;
Next remove the creation and adding of the Enemy instance. Then go to the -advancedTime: method and remove everything after [self testCollisions];. Add the following to the advancedTime: method:
[mEnemies makeObjectsPerformSelector:@selector(advance)];
    
    if (mTimePassed < kEnemyDispatchTime) {
        mTimePassed += seconds;
    }
    else {
        mTimePassed = 0.0;
        
        Enemy *enemy = [[Enemy alloc] init];
        enemy.x = 150.0;
        enemy.y = 50.0;
        
        [mEnemies addObject:enemy];
        
        [self addChild:enemy];
        [enemy release];
    }
The first line in the block uses the NSSet instance method, makeObjectsPerformSelector: to move all of the enemy instances. Next, it checks to see if it is time to add another enemy. Notice that the comparison checks if the time passed is less than the dispatch time. We want an enemy to appear every 1.5 seconds but it  is unlikely that the time passed will equal exactly 1.5.  If enough time has not passed we add the amount that has. If enough time has passed, the time passed is reset to zero, and a new Enemy instance is created, added to the stage, and the NSMutableSet.
Since there are now several instances of the Enemy class on the game, each one needs to handle it’s own movement.  Start in the Enemy.h file and add an instance variable of: 
BOOL mIsMovingLeft;
Also define the following method:
  • (void)advance;
Move to the Enemy.m file and add mIsMovingLeft = NO; inside the -init method.   Next we need to implement the -advance method. This will be very similar to the way an enemy was moved before. The biggest difference is that when an enemy moves down the screen it moves by 40 px instead of 20 px. This is to keep the screen from being too crowded. Add the following method to Enemy.m.
- (void)advance {
    if (!mIsMovingLeft) {
        self.x = (self.x + 1);
        if (self.x == 300) {
            self.y = (self.y + 40);
            mIsMovingLeft = YES;
        }
    }
    else {
        self.x = (self.x - 1);
        if (self.x == 20) {
            self.y = (self.y + 40);
            mIsMovingLeft = NO;
        }
    }
}
If you build and run now you will see lot of enemies appear and move their way down the screen. The next issue is laser collisions are not going to be recognized. Just like each instance of an Enemy needs to handle its own movement, they each need to handle their own collisions as well. To do this, go to the Enemy.h file and import Laser.h. Then define the following method.
- (void)testCollisionWith:(Laser *)laser;
Move into the Enemy.m file for the implementation of this method. The implementation of this method is basically the same as it was when the collisions were handled by the Game.m file. Add the following to method to the Enemy.m file.
- (void)testCollisionWith:(Laser *)laser {
    SPRectangle *enemyRect = [self boundsInSpace:self.parent];
    SPRectangle *laserRect = [laser boundsInSpace:self.parent];
    
    if ([laserRect intersectsRectangle:enemyRect]) {
        [self.stage removeChild:laser];
        
        SPTween *tween = [SPTween tweenWithTarget:self time:0.5];
        [tween animateProperty:@"rotation" targetValue:(self.rotation + SP_D2R(360))];
        [tween animateProperty:@"width" targetValue:0.0];
        [tween animateProperty:@"height" targetValue:0.0];
        [self.stage.juggler addObject:tween];
        
        [self.stage performSelector:@selector(destroyedEnemy:) withObject:self afterDelay:tween.time];
    }
}
First SPRectangle objects are created for the enemy and laser instances. The rectangles represent where they are at in the enemy’s parent object. In this case the parent object is the stage. If the laser and an enemy are in the same space, the laser is removed and the enemy does a spin into its removal. Lastly, the stage is notified of the collision by performing a selector after a delay that is equal to the animation’s time.
The stage still needs to handle the collision. Start by defining the following method in Game.h: 
  • (void)destroyedEnemy:(Enemy *)enemy;
The implementation of this method is strait forward. All it needs to do is remove the  enemy from the stage and the mEnemys mutable set.
- (void)destroyedEnemy:(Enemy *)enemy {
    [self removeChild:enemy];
    [mEnemies removeObject:enemy];
}
At the end of this tutorial your game should look something like this:
This game has now gone from a wireframe prototype to a game with a graphical interface and some challenging play. All in all, utilizing this tutorial will take approximately  two to three hours of coding time. This of course depends on how familiar you are with Sparrow Framework.
All the images used in this demo were downloaded from Open Clip Art at http://www.openclipart.org/. This site is a great source for royalty free graphics, if you need quick images.  
The full source code of this and the previous demo is available on github at https://github.com/matt62king/SparrowDemo. I have tagged the sources to match the series, part I and part II.  Or download the master branch for the full source.