Beautifying Our Game - Sparrow iOS Game Framework Beginner's Guide (2014)

Sparrow iOS Game Framework Beginner's Guide (2014)

Chapter 5. Beautifying Our Game

In the previous chapter, we learned about cross-device compatibility and what we need to do if we want to target iPhones and iPads simultaneously. We then set up the base for our game. In this chapter, we will begin to add animations to our game.

Working with tweens

Let's say we want to move our ship to an edge of the screen. How would we go about achieving this? The following are two options to achieve this:

· Move the ship each frame in the direction we want it to move

· Define two states for our ship and let the processor calculate all the required steps for animation

At first glance, the second option seems to be more attractive. We first need to know the initial position of the ship and the position where the ship should be after the animation is complete. Sparrow provides the SPTween class, which does exactly this.

We take two values, also called key frames, and interpolate all values in between. The name "tween" comes from its in-between states.

While in this example, we are talking about moving a position explicitly, in general, a tween is not confined to animating the position of an entity, but could be used to animate its color or any of its other properties.

In Sparrow, specifically, any numeric property of an object can be animated. So every property that is available on an SPDisplayObject is available for the SPTween class and its animation abilities.

If we want to implement a fade-out or fade-in effect, all we need to do is to animate the alpha property of a display object from its maximum to its minimum value or vice versa.

Let's try this by actually moving the pirate ship.

Time for action – moving the pirate ship

Let's follow these steps to move the ship:

1. Open our game project file if it's not already open.

2. Add an instance variable called _pirateShip of the type SPImage, as shown in following line of code:

SPImage* _pirateShip;

3. Update the references from pirateShip to _pirateShip in Battlefield.m:

4. _pirateShip = [SPImage imageWithTexture:[Assets texture:@"ship_pirate.png"]];

5. _pirateShip.x = (Sparrow.stage.width - _pirateShip.width) / 2;

_pirateShip.y = (Sparrow.stage.height - _pirateShip.height) / 2;

6. Add a method called onBackgroundTouch in the Battlefield.m file, as shown in the following line of code:

-(void) onBackgroundTouch: (SPTouchEvent*) event

7. Within this method, get the touch itself:

SPTouch* touch = [[event touchesWithTarget:self andPhase:SPTouchPhaseBegan] anyObject];

8. Complete the onBackgroundTouch method with the following piece of code:

9. if (touch) {

10. SPTween* tweenX = [SPTween tweenWithTarget:_pirateShip time:2.0f];

11. SPTween* tweenY = [SPTween tweenWithTarget:_pirateShip time:2.0f];

12.

13.

14. [tweenX animateProperty:@"x" targetValue:touch.globalX - (_pirateShip.width / 2)];

15. [tweenY animateProperty:@"y" targetValue:touch.globalY - (_pirateShip.height / 2)];

16.

17. [Sparrow.juggler addObject:tweenX];

18. [Sparrow.juggler addObject:tweenY];

}

19. Register the event listener to the background image as shown in the following line of code:

[background addEventListener:@selector(onBackgroundTouch:) atObject:self forType:SP_EVENT_TYPE_TOUCH];

20. Switch to the Game.m file.

21. Update the scene director to show the battlefield scene.

22. Run the example and you will get the following output:

Time for action – moving the pirate ship

What just happened?

In step 1, we opened our Xcode template from where we left off in the previous chapter. In order to use a pirate ship in the entirety of our battlefield source file, we should move it into an instance variable for the Battlefield class, which is what we did in step 2.

Now, we need to update the references to the pirate ship which was the task for step 3.

After this, we defined the method where we declared what happens if we were to touch the background (in our case, the water on the screen). In step 5, we got the current touch.

In step 6, we implemented the actual tween. As soon as we were sure that we have the current touch object (as in not a false value such as nil), we began to animate the pirate ship.

We created two tweens: the first for the x position of the pirate ship and the second one for its y position. As long as the target and the duration of tween are the same, we could actually use a single tween, as shown in the following code:

if (touch) {

SPTween* tween = [SPTween tweenWithTarget:_pirateShip time:2.0f];

[tween animateProperty:@"x" targetValue:touch.globalX - (_pirateShip.width / 2)];

[tween animateProperty:@"y" targetValue:touch.globalY - (_pirateShip.height / 2)];

[Sparrow.juggler addObject:tween];

}

Since we are going to change these properties in a bit, we better leave it at being two separate tweens.

A tween always needs a target which we are setting to the _pirateShip instance variable. Another value we must specify is how long the tween will animate, which is set by the time parameter. The amount of time the tween takes is available as a property on an instance of SPTween. The time parameter is of the type double and is measured in seconds.

The tweenX instance is being bound to the x property. We need to access the property through its NSString identifier. So, if we want to animate the alpha property, we would need to access it through @"alpha". Internally, Sparrow uses the runtime type information (also referred to as reflection) to change properties at runtime.

We set the target value to the current touch position, the x coordinate of that touch to be precise. Now, if we touch the background, the ship's top-left corner would be at the touch position. To feel more natural, we should change it so that the ship is at the center of the touch. This is why we subtracted half of the ship's width from the touch position.

Implicitly, the initial value is automatically set to the current value of the property, which is to be animated.

Then, we did the same for tweenY and the y positions, respectively.

To actually animate the properties, we added the tweens to an object called the juggler, which is available through Sparrow.juggler. We will take a look at how jugglers work later in the chapter.

For the touch event to fire, we registered the onBackgroundTouch method with the background image.

In step 8, we opened the Game.m file and updated the show call to use the battlefield scene instead of the pirate cove scene that happens in step 9.

Then, we ran the example. If we touch anywhere on the screen, the ship will move to the position we just touched.

Let's take a look at our source files.

The following is the code for the Battlefield.h file:

#import "Scene.h"

@interface Battlefield : Scene {

SPImage *_pirateShip;

}

@end

Here's the corresponding Battlefield.m file:

#import "Battlefield.h"

#import "Assets.h"

@implementation Battlefield

-(void) onBackgroundTouch: (SPTouchEvent*) event

{

SPTouch *touch = [[event touchesWithTarget:self andPhase:SPTouchPhaseBegan] anyObject];

if (touch) {

SPTween *tweenX = [SPTween tweenWithTarget:_pirateShip time:2.0f];

SPTween *tweenY = [SPTween tweenWithTarget:_pirateShip time:2.0f];

[tweenX animateProperty:@"x" targetValue:touch.globalX - (_pirateShip.width / 2)];

[tweenY animateProperty:@"y" targetValue:touch.globalY - (_pirateShip.height / 2)];

[Sparrow.juggler addObject:tweenX];

[Sparrow.juggler addObject:tweenY];

}

}

-(id) init

{

if ((self = [super init])) {

SPImage *background = [SPImage imageWithTexture:[Assets texture:@"water.png"]];

background.x = (Sparrow.stage.width - background.width) / 2;

background.y = (Sparrow.stage.height - background.height) / 2;

_pirateShip = [SPImage imageWithTexture:[Assets texture:@"ship_pirate.png"]];

_pirateShip.x = (Sparrow.stage.width - _pirateShip.width) / 2;

_pirateShip.y = (Sparrow.stage.height - _pirateShip.height) / 2;

SPImage *ship = [SPImage imageWithTexture:[Assets texture:@"ship.png"]];

ship.x = 100;

ship.y = 100;

[background addEventListener:@selector(onBackgroundTouch:) atObject:self forType:SP_EVENT_TYPE_TOUCH];

[self addChild:background];

[self addChild:_pirateShip];

[self addChild:ship];

}

return self;

}

@end

Understanding transitions

Let's take a closer look at the animation we just implemented. When we moved our pirate ship, it moves at a constant speed. This is a linear transition, which is the default behavior for each newly created SPTween instance if the transition value is not explicitly set when creating the instance.

The standard way to create a tween with the default transition is as follows:

SPTween *myTween = [SPTween tweenWithTarget:_pirateShip time:2.0f];

To use a tween with a nonlinear transition, just specify it as a parameter:

SPTween *myTween = [SPTween tweenWithTarget:_pirateShip time:2.0f transition:SP_TRANSITION_EASE_IN_OUT];

In this piece of code, we are using a transition behavior called "ease-in-out", in which case the ship wouldn't move right away but would take its time to start, and shortly before the animation is over, it slows down a bit.

Note

For a complete list of all available transitions and their graphical representations, take a look at the Sparrow manual at http://wiki.sparrow-framework.org/_detail/manual/transitions.png?id=manual%3Aanimation.

Explaining jugglers

The purpose of a juggler is to animate other objects. It does this by holding them in a list, and calling an update method every frame. The update method (advanceTime) passes through the number of milliseconds that have been passed since the last frame. Every object we want to animate needs to be added to an instance of SPJuggler.

The default juggler can be accessed through Sparrow.juggler and is the easiest way to animate objects on the screen.

As Sparrow.juggler is just an instance of SPJuggler, it is also possible to separate jugglers for each of the main components of our game. For now, using the default juggler is enough for our needs.

Updating the movement and canceling tweens

It's time for our first gameplay decisions. Right now, the pirate ship's animation is always 2 seconds long which would provide a serious advantage if the player touched one of the edges of the screen instead of just moving a few points on the screen.

What we need to introduce is some kind of penalty if we move to an edge of the screen, like taking more time for the ship to advance.

It's also a good idea to add the possibility of canceling the animation when the ship is currently moving. So when things get heated, we have a option to retreat from the current battle.

Now, how would we go about implementing the cancelation of the current animation? Let's see the following options for doing so:

· By adding a button on the screen

· By touching the ship itself

We should try to avoid onscreen controls as long as we can, so let's add this functionality to the touch event (when we touch the pirate ship).

Time for action – updating the movement

To update the movement of our ship, follow these steps:

1. Inside the initializer, add a tween for the enemy ship. We want the enemy ship to move on its own. We should also rename the ship instance to enemyShip:

2. SPImage *enemyShip = [SPImage imageWithTexture:[Assets texture:@"ship.png"]];

3. enemyShip.x = 100;

4. enemyShip.y = 100;

5.

6. SPTween *shipTween = [SPTween tweenWithTarget:enemyShip time:4.0f transition:SP_TRANSITION_EASE_IN_OUT];

7. [shipTween animateProperty:@"y" targetValue:250];

8. shipTween.repeatCount = 5;

9. shipTween.reverse = YES;

10.shipTween.delay = 2.0f;

11.

12.[Sparrow.juggler addObject:shipTween];

13. Update the onBackgroundTouch method to resemble the following piece of code:

14.SPTouch *touch = [[event touchesWithTarget:self] anyObject];

15.

16.if (touch) {

17. [Sparrow.juggler removeObjectsWithTarget:_pirateShip];

18.

19. float targetX = touch.globalX - (_pirateShip.width / 2);

20. float targetY = touch.globalY - (_pirateShip.height / 2);

21.

22. float distanceX = fabsf(_pirateShip.x - targetX);

23. float distanceY = fabsf(_pirateShip.y - targetY);

24. float penalty = (distanceX + distanceY) / 80.0f;

25.

26. float shipInitial = 0.25f + penalty;

27.

28. float speedX = shipInitial + (distanceX / Sparrow.stage.width) * penalty * penalty;

29. float speedY = shipInitial + (distanceY / Sparrow.stage.height) * penalty * penalty;

30.

31. SPTween *tweenX = [SPTween tweenWithTarget:_pirateShip time:speedX];

32. SPTween *tweenY = [SPTween tweenWithTarget:_pirateShip time:speedY];

33.

34.

35. [tweenX animateProperty:@"x" targetValue:targetX];

36. [tweenY animateProperty:@"y" targetValue:targetY];

37.

38. [Sparrow.juggler addObject:tweenX];

39. [Sparrow.juggler addObject:tweenY];

}

40. Add a new method called onShipStop as shown in the following line of code:

-(void) onShipStop:(SPTouchEvent*) event

41. Implement this method with all of the touch boilerplate code and stop all animations:

42.SPTouch *touch = [[event touchesWithTarget:self andPhase:SPTouchPhaseBegan] anyObject];

43.

44.if (touch) {

45. [Sparrow.juggler removeObjectsWithTarget:_pirateShip];

}

46. Register the onShipStop selector to the pirate ship:

[_pirateShip addEventListener:@selector(onShipStop:) atObject:self forType:SP_EVENT_TYPE_TOUCH];

47. When we add the ships to the battlefield scene, switch the enemy ship with the pirate ship.

48. Run the example and you'll see the following result:

Time for action – updating the movement

What just happened?

In step 1, we added a tween for the enemy ship right below the code where we load its image.

When creating the instance, we set the time the animation should take to 4 seconds and we used the ease-in-out transition to see the difference when we directly compare it with the default linear transition.

This tween will move the enemy ship by its y property/position. We set the target value to 250, which is more or less the bottom of the screen.

When setting the repeatCount property—which takes an int as its value—we want to repeat the animation for exactly as many times as we set the property to.

Tweens can be reversed by setting the reverse property to YES or NO, as it takes a BOOL value. If we had not set the reverse property in this example, the tween would start at its initial value when repeating the animation. When set to YES, the animation alternates between its initial and target values. We should keep in mind that a reverse animation counts as an animation cycle.

Tweens can be delayed by using their delay property. This property needs a double type as well and is measured in seconds just like the time property.

Now, we need to add the animation to the default juggler.

In step 2, we updated the touch event and the animation. First of all, we removed the andPhase parameter. Previously, we could only move the ship by tapping on the screen. Now, we can either tap the screen or touch-and-drag on the screen to move the ship around.

After we know that a touch was made, we removed all the previously bound tweens from the juggler. Here, we are just making sure that we always have a fresh tween and the pirate ship animation might produce any random side effects such as multiple tweens setting different target values at the same time.

In the next line, we declared and assigned variables for the new position our ship should move to. Then, we got the absolute values between the ship's position and the position of our touch.

The penalty is calculated by the sum of the distances divided by 80, which is conveniently the size of our ship in points. So, the closer the touch is to the ship, the lower this value is, and the further away the touch is from the ship, the higher this value will be.

The speed of the ship, that is, the duration of the animation, is calculated by the relative distance with regard to the screen size multiplied by the square penalty. We also have an initial value of 250 milliseconds, which is the shortest amount the animation could be.

Instead of the animateProperty method, we can also use the shorthand method moveToX:y: which does the same as calling animateProperty on the x and y properties.

In step 3, we added the onShipStop method to the source file, which we implemented in the next step. We also removed all tweens with the _pirateShip target. So, if currently a tween is being executed, it will be removed.

In step 5, we registered the onShipStop event to the pirate ship.

Currently, if we were to move over the enemy ship, the enemy ship would be displayed on the top of our ship. For our ship to be displayed on top of the enemy ship, we need to switch the two around when we add them to the display tree.

After this example, our Battlefield.m file should look like the following code:

#import "Battlefield.h"

#import "Assets.h"

@implementation Battlefield

-(id) init

{

if ((self = [super init])) {

SPImage *background = [SPImage imageWithTexture:[Assets texture:@"water.png"]];

background.x = (Sparrow.stage.width - background.width) / 2;

background.y = (Sparrow.stage.height - background.height) / 2;

_pirateShip = [SPImage imageWithTexture:[Assets texture:@"ship_pirate.png"]];

_pirateShip.x = (Sparrow.stage.width - _pirateShip.width) / 2;

_pirateShip.y = (Sparrow.stage.height - _pirateShip.height) / 2;

SPImage *enemyShip = [SPImage imageWithTexture:[Assets texture:@"ship.png"]];

enemyShip.x = 100;

enemyShip.y = 100;

SPTween *shipTween = [SPTween tweenWithTarget:enemyShip time:4.0f transition:SP_TRANSITION_EASE_IN_OUT];

[shipTween animateProperty:@"y" targetValue:250];

shipTween.repeatCount = 5;

shipTween.reverse = YES;

shipTween.delay = 2.0f;

[Sparrow.juggler addObject:shipTween];

[background addEventListener:@selector(onBackgroundTouch:) atObject:self forType:SP_EVENT_TYPE_TOUCH];

[_pirateShip addEventListener:@selector(onShipStop:) atObject:self forType:SP_EVENT_TYPE_TOUCH];

[self addChild:background];

[self addChild:enemyShip];

[self addChild:_pirateShip];

}

return self;

}

-(void) onBackgroundTouch:(SPTouchEvent*) event

{

SPTouch *touch = [[event touchesWithTarget:self] anyObject];

if (touch) {

[Sparrow.juggler removeObjectsWithTarget:_pirateShip];

float targetX = touch.globalX - (_pirateShip.width / 2);

float targetY = touch.globalY - (_pirateShip.height / 2);

float distanceX = fabsf(_pirateShip.x - targetX);

float distanceY = fabsf(_pirateShip.y - targetY);

float penalty = (distanceX + distanceY) / 80.0f;

float shipInitial = 0.25f + penalty;

float speedX = shipInitial + (distanceX / Sparrow.stage.width) * penalty * penalty;

float speedY = shipInitial + (distanceY / Sparrow.stage.height) * penalty * penalty;

SPTween *tweenX = [SPTween tweenWithTarget:_pirateShip time:speedX];

SPTween *tweenY = [SPTween tweenWithTarget:_pirateShip time:speedY];

[tweenX animateProperty:@"x" targetValue:targetX];

[tweenY animateProperty:@"y" targetValue:targetY];

[Sparrow.juggler addObject:tweenX];

[Sparrow.juggler addObject:tweenY];

}

}

-(void) onShipStop:(SPTouchEvent*) event

{

SPTouch *touch = [[event touchesWithTarget:self andPhase:SPTouchPhaseBegan] anyObject];

if (touch) {

[Sparrow.juggler removeObjectsWithTarget:_pirateShip];

}

}

@end

Working with sprite sheets

So far, we loaded every image on its own and displayed them on the screen. Sprite sheets are a way to combine all of these smaller images into one big image. When we load the image, we are able to use the textures in the same way that we are used to.

When using multiple images, something called a "texture switch" happens every time the current active texture is being swapped out by a different one. This operation is quite heavy on performance, so it should be avoided where possible. Sprite sheets allow us to achieve this by using the same image asset for numerous different images, thus avoiding the texture switch and keeping the number of draw calls to a minimum.

Sprite sheets can also be used for sprite animation, in which a series of images is displayed sequentially one frame after another, which creates the illusion of animation to the human eye—just like a flip book.

A texture atlas is a specialization of sprite sheets with regard to containing smaller images, but it also provides a file of metadata which contains the information of where exactly its subimages are. In practice though, "texture atlas" and "sprite sheet" are used as synonyms.

Note

Before we get started, let's download all the necessary graphics for this chapter at https://github.com/freezedev/pirategame-assets/releases/download/0.5/Graphics_05.zip.

Learning about texture formats

So far, we only used PNG images. However, let's see if there are any other texture formats in iOS that would better fit our purpose. Spoiler: there are. Leaving the brash remark aside, we are going to analyze which texture formats fits our purpose best.

The following table shows the pirate ship image in different file formats. Let's compare its file sizes:

Compression

File format

File size

None

BMP

257 KB

Lossless

PNG

36.6 KB

Depends

PVR

(In this case RGBA8888)

257 KB

When we load a PNG file, what happens internally? The image gets decompressed when it's being loaded—at the expense of the CPU. The same goes for other conventional image formats such as JPEG. Once the image is decompressed, it becomes a texture.

PVR is a texture format specifically optimized for iOS devices or for PowerVR GPUs used on all iOS devices, to be more precise. When loading a PVR image, for example, it will decode the image directly on the GPU instead of the CPU.

PVR includes a lot of different image formats. If we are going for lossless quality including alpha channels, we should opt for the RGBA8888 format. If we don't need the alpha channel, we should use an image format without one. The RGBA8888 image format is not compressed. So, in order to keep the application size at a minimum, we should use the pvr.gz format, which is a PVR file compressed using GZIP.

Using TexturePacker to create sprite sheets

TexturePacker is a commercial application to create sprite sheets and texture atlases and is available at http://www.codeandweb.com/texturepacker for around 30 dollars. To be able to create our very own sprite sheets, we either need the pro or the trial version of TexturePacker. The TexturePacker download window looks as follows:

Using TexturePacker to create sprite sheets

While the workflow is pretty self-explanatory, let's go through a few steps to create our own texture atlas:

1. Drag-and-drop the images 0001.png to 0032.png into the Sprites section of the application.

2. Select Sparrow/Starling as the Data Format.

3. Select GZIP compr. PVR as the Texture Format.

4. Select RGBA8888 as the Image Format.

5. Hit the AutoSD button and select corona @4x/@2x from the presets.

6. Set the filenames to ship_pirate_small_cannon{v}.xml for the data file and ship_pirate_small_cannon{v}.pvr.gz for the texture file.

7. Click on the Publish button.

Now our texture atlas is generated for each of our resolution we are supporting. Let's take a look at the result. The output of one of the generated images would look like the following screenshot:

Using TexturePacker to create sprite sheets

Here's a snippet from the corresponding XML file:

<?xml version="1.0" encoding="UTF-8"?>

<!-- Created with TexturePacker http://www.codeandweb.com/texturepacker-->

<!-- $TexturePacker:SmartUpdate:c58f88c054e0e917cc6c06d11cc04c15:0af47aa74ca5e538fac63da189c2b7ac:9e0a4549107632fbd952ab702bfc21e4$ -->

<TextureAtlas imagePath="ship_pirate_small_cannon.pvr.gz">

<SubTexture name="e_0001" x="0" y="0" width="80" height="80"/>

<SubTexture name="e_0003" x="80" y="0" width="80" height="80"/>

<SubTexture name="e_0005" x="160" y="0" width="80" height="80"/>

<SubTexture name="e_0007" x="240" y="0" width="80" height="80"/>

From this snippet, we can see the reference to the original image and its subtextures. Each subtexture has a name, its location inside the bigger image, and its dimensions.

Loading our first texture atlas

Now that we have our texture atlas, let's load and display it with Sparrow.

Time for action – loading our first texture atlas

To load our first texture atlas, we need to follow these steps:

1. Copy the necessary files (ship_pirate_small_cannon*) into the project.

2. Load the texture atlas with the following line of code:

SPTextureAtlas* atlas = [SPTextureAtlas atlasWithContentsOfFile:@"ship_pirate_small_cannon.xml"];

3. Create an array out of all textures starting with 00:

NSArray* textures = [atlas texturesStartingWith:@"00"];

4. Create a movie clip object and position it just above the original pirate ship, as shown in the following code:

5. SPMovieClip *cannonShip = [SPMovieClip movieWithFrames:textures fps:20.0f];

6. cannonShip.x = 200;

cannonShip.y = 50;

7. Play the animation with the following piece of code:

8. [cannonShip play];

[Sparrow.juggler addObject:cannonShip];

9. Add the animated pirate ship to the display tree as follows:

10.[self addChild:background];

11.[self addChild:enemyShip];

12.[self addChild:_pirateShip];

[self addChild:cannonShip];

13. Run the example to see the following result:

Time for action – loading our first texture atlas

What just happened?

To use the texture atlas, we first copied all related files into the project. Using the SPTextureAtlas class, we then loaded the XML file.

In step 3, we needed to get an array (or an NSArray to be exact) out of the texture atlas with all of the images starting with 00, which in our case means that every image in this sprite sheet will be used for the animation.

An SPMovieClip class is derived from SPDisplayObject and can be added to the display tree as well. It can play the animation from the array we made in step 3. The fps parameter is necessary as it sets the speed of the animation.

To play the animation itself, two things need to be done: first, we need to call the play method from the movie clip and second, we need to add the movie clip to the juggler. This is exactly what we did in step 5.

In the next step, we added the movie clip to the display tree and when we ran the example, we had our pirate ship, the enemy ship which moves up and down and now the second pirate ship which has the cannon firing animation.

If you want to take a look at the complete source file for this example, it is available at https://github.com/freezedev/pirategame/blob/71f42ded614c4917802dcba46a190476ff7b88c4/Classes/Battlefield.m.

Pop quiz

Q1. What are tweens?

1. A way to define animation by setting two key frames

2. Animations consisting of multiple sprites

3. A way to optimize multiple display objects on the screen

Q2. What are sprite sheets?

1. Sketches on a sheet of paper

2. An image containing several smaller ones

3. A Sparrow extension to use sprites

Q3. Transitions are used to modify the rate of animation over time.

1. True

2. False

Summary

In this chapter, we learned about tweens and sprite sheets.

Specifically, we covered how to animate display objects with tweens, create our own sprite sheets, and how to animate these sprite sheets.

We also touched upon texture formats, jugglers, and transitions.

Now that we have animations and our ship is moving around, let's add some game logic—which is the topic of the next chapter.