SpriteKit Animations and TextureAtlases

Joachim Grill
SpriteKit Animations and TextureAtlases
Last update: More than 3 months ago

This tutorial uses Objective-C. A new version is available here: SpriteKit TextureAtlases with Swift V3

Learn how you can improve development of SpriteKit games and apps using TexturePacker.

  • Create your SpriteKit atlas with TexturePacker
  • Creating a SKSpriteNode from the texture atlas
  • Using SKActions to move the sprite
  • Applying SKAction to multiple SKSpriteNodes

Full source code is available on GitHub

In this tutorial you are going to learn how you can improve development of SpriteKit games and apps using TexturePacker.

The main advantages over the pure Xcode solution are

  • Organizing your sprites in folders

  • Importing multiple formats such as PNG, PSD, SVG, SWF

  • Compile time checks for sprite names

  • Creating animations with a single line of code

Let's start with how to easily create your atlas.

Using TexturePacker to create sprite sheets for SpriteKit

Create your SpriteKit atlas with TexturePacker

To create a new SpriteKit atlas, simply start TexturePacker and drag & drop the directories containing your sprite images to the Sprites area of TexturePacker. TexturePacker will automatically load and lay out all image files:

add your sprites to TexturePacker

Select SpriteKit in the Data Format field, and enter a path to which the atlas bundle should be written. Parallel to this .atlasc file TexturePacker generates a .h header file which contains useful macros for easy SKTexture creation. If you choose the Xcode project directory as output here, the generated header file is automatically found in the include path.

Select SpriteKit as Data Format

To use the published sprite sheet in Xcode, drag and drop the .atlasc bundle and the generated .h header file to your Xcode project:

Add atlasc bundle to your project

Xcode asks how the folder should be added. If you create a folder reference, the Xcode project is automatically updated in the future if the atlasc bundle changes (e.g. additional sprite sheets are added).

Create folder reference for any added folders

Creating a SKSpriteNode from the texture atlas

Creating a textured sprite is quite easy, just load the texture and use the SKTexture object when creating the sprite node:

texture = [SKTexture textureWithImageNamed:@"Background"];
sprite = [SKSpriteNode spriteNodeWithTexture:texture];

The first line loads the sprite—looking for a single sprite in the file system— and if not found searching all sprite sheets available to the application.

The second line creates a sprite object using the specified texture.

Adding compile-time checks to your SpriteKit project

Dummy graphic replacing missing sprite

As the texture image is referenced by its file name, typos in its name or a mismatch due to a reorganized texture atlas cannot be detected at compile-time.

SpriteKit replaces missing images with a dummy graphic which might look strange. Imagine what this would mean for you if it accidently reaches the AppStore...

TexturePacker helps you avoid this: with compile-time checks!

TexturePacker creates a header file together with your atlas. You can simply import it using:

#import "sprites.h"

The file contains all sprite names used in the atlas as a #define . It also defines a macro for each texture image which creates the corresponding SKTexture object.

#define SPRITES_SPR_BACKGROUND       @"Background"
#define SPRITES_SPR_CAPGUY_TURN_0001 @"capguy/turn/0001"
#define SPRITES_SPR_CAPGUY_TURN_0002 @"capguy/turn/0002"
...
#define SPRITES_TEX_BACKGROUND       [SKTexture textureWithImageNamed:@"Background"]
#define SPRITES_TEX_CAPGUY_TURN_0001 [SKTexture textureWithImageNamed:@"capguy/turn/0001"]
#define SPRITES_TEX_CAPGUY_TURN_0002 [SKTexture textureWithImageNamed:@"capguy/turn/0002"]

Using these defines, creating a sprite is a one-liner:

SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithTexture:SPRITES_TEX_BACKGROUND];

If you now rename the sprite and publish the sprite atlas from TexturePacker, the definition also changes its name. When compiling in Xcode you get a compiler error about a missing sprite.

Simplifying SpriteKit's animation handling

Sprites are considered as animation if they end with a number—e.g. img_01 , img_02 , etc. For these an NSArray object with all textures of the animation is defined.

SpriteKit animation phases
#define SPRITES_ANIM_CAPGUY_TURN @[ \
[SKTexture textureWithImageNamed:@"capguy/turn/0001"], \
[SKTexture textureWithImageNamed:@"capguy/turn/0002"], \
[SKTexture textureWithImageNamed:@"capguy/turn/0003"], \
...

This makes it extremely simple to animate sprites:

SKAction *walk = [SKAction animateWithTextures:SPRITES_ANIM_CAPGUY_WALK timePerFrame:0.033];
[sprite runAction:walk];

No more adding single frames, no more worrying about missing animation phases!

Enhancing the animation with additional frames—or removing frames—doesn't require you change the code at all: TexturePacker always fills in the right frames. Note: SpriteKit will load the texture images as soon as the corresponding animation is started. That might cause a reduced frame rate the first time an animation is played. To avoid this the SKTextureAtlas can be loaded explicitly:

self.atlas = [SKTextureAtlas atlasNamed:SPRITES_ATLAS_NAME];

In contrast to this solution, preloading textures with [SKTexture preloadTextures:withCompletionHandler:] or [SKTextureAtlas preloadTextureAtlases:withCompletionHandler:] does not fix the problem with the frame rate drop. BTW: The manually loaded SKTextureAtlas also fixes the deficiency that [SKTexture textureWithImageNamed:@"mysprite"]] is not able to find the retina version mysprite@2x within a texture atlas.

Using SKActions to move the sprite

For our sample application we use two animations:

  • walk (left to right)
  • turn (right to left)
capguy-walk capguy-turn

These animations can be created as mentioned above:

SKAction *walk = [SKAction animateWithTextures:SPRITES_ANIM_CAPGUY_WALK timePerFrame:0.033];
SKAction *turn = [SKAction animateWithTextures:SPRITES_ANIM_CAPGUY_TURN timePerFrame:0.033];

Due to the enormous width of the iPad display we have to repeat the animation a few times:

SKAction *walkAnim = [SKAction sequence:@[walk, walk, walk, walk, walk, walk]];

Note: As SpriteKit does not allow repeat actions to be nested, we cannot use [SKAction repeatAction:count:] here, this would conflict with [SKAction repeatActionForever:] , see below. This is why we implement the action as a sequence of walk actions.

In the animation CapGuy walks without moving forward. We need a move action to move the sprite from left to right, and back. The action gets the same duration as the animation itself:

SKAction *moveRight  = [SKAction moveToX:900 duration:walkAnim.duration];
SKAction *moveLeft   = [SKAction moveToX:100 duration:walkAnim.duration];

We have only an animation with CapGuy walking from left to right, but not in the other direction. So we use a scale action with scaling factor -1 to get a mirrored animation. Another action is needed to set the scaling back to 1:

SKAction *mirrorDirection  = [SKAction scaleXTo:-1 y:1 duration:0.0];
SKAction *resetDirection   = [SKAction scaleXTo:1  y:1 duration:0.0];

All actions which are put into a group are executed in parallel. We are not only adding the walk and move actions to a group, but also the mirror / reset actions. They have a duration of 0 and are executed at the beginning of the group, so their scaling factor has direct impact on the walk / move actions:

SKAction *walkAndMoveRight = [SKAction group:@[resetDirection,  walkAnim, moveRight]];
SKAction *walkAndMoveLeft  = [SKAction group:@[mirrorDirection, walkAnim, moveLeft]];

Now we combine walk & turn actions into a sequence, and repeat this sequence forever:

self.sequence =
  [SKAction repeatActionForever:[SKAction sequence:@[walkAndMoveRight, turn, walkAndMoveLeft, turn]]];

Applying SKAction to multiple SKSpriteNodes

SKAction objects can be used for many sprites in parallel. In our example we want to create a new CapGuy sprite each time the user touches the screen. We have to create a new SKSpriteNode only, and run the action on it which we created in the section above:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithTexture:SPRITES_TEX_CAPGUY_WALK_0001];
    sprite.position = CGPointMake(100, rand() % 100 + 200);

    [sprite runAction:sequence];
    [self addChild:sprite];
}

The complete SpriteKit sample

iPhone screenshot complete sample