SpriteKit TextureAtlases with Swift

2014-12-18 Joachim Grill Get Sourcecode from 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:

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 select the SpriteKit (Swift) framework :

Add your sprites to TexturePacker

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

In the Settings panel you can enter a path to which the atlas bundle should be written. Additionally TexturePacker can generate a .swift file which contains a class providing useful methods for easy SKTexture creation.

To add scaled sprites for non-retina displays automatically, open the Scaling variants dialog by clicking on the cog icon. There you can select the SpriteKit @2x/@1x preset, which defines two scaling variants: @2x with the original sprites, and a variant without suffix containing the sprites scaled by 0.5.

With the Publish sprite sheet button in the toolbar the Atlas bundle and the Swift file is written to disk.

Select SpriteKit as Data Format

To use the published sprite sheet in Xcode, drag and drop the .atlasc bundle and the generated .swift file 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:

let texture = SKTexture(imageNamed: "Background")
let sprite = SKSpriteNode(texture: 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.

If you know on which sprite sheet the texture is stored, it is more efficient to fetch the texture directly from the sheet:

let atlas = SKTextureAtlas(named: "CapGuy")
let texture = atlas.textureNamed("Background")
let sprite = SKSpriteNode(texture: texture)

Adding compile-time checks to your SpriteKit project

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 Swift class file together with your atlas. You can simply add it to your Xcode project and create an object of this class:

Dummy graphic replacing missing sprite
let sheet = CapGuy()

The class contains all sprite names used in the atlas as properties. It also defines a method for each texture image to creates the corresponding `SKTexture` object.

let BACKGROUND       = "Background"
let CAPGUY_WALK_0001 = "capguy/walk/0001"
let CAPGUY_WALK_0002 = "capguy/walk/0002"
...
func Background() -> SKTexture       { return textureAtlas.textureNamed(BACKGROUND) }
func capguy_walk_0001() -> SKTexture { return textureAtlas.textureNamed(CAPGUY_WALK_0001) }
func capguy_walk_0002() -> SKTexture { return textureAtlas.textureNamed(CAPGUY_WALK_0002) }

Using these methods, creating a sprite is a 1-liner:

let sprite = SKSpriteNode(texture: sheet.Background());

If you now rename the sprite and publish the sprite atlas from TexturePacker, the method 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 a method returning an array with all textures of the animation is defined.

SpriteKit animation phases
func capguy_walk() -> [SKTexture] {
    return [
        capguy_walk_0001(),
        capguy_walk_0002(),
        capguy_walk_0003(),
        ...

This makes it extremely simple to animate sprites:

let walk = SKAction.animateWithTextures(sheet.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.

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:

let walk = SKAction.animateWithTextures(sheet.capguy_walk(), timePerFrame: 0.033)
let turn = SKAction.animateWithTextures(sheet.capguy_turn(), timePerFrame: 0.033)

To walk over the complete iPad display, we have to repeat the animation

let walkAnim = SKAction.repeatAction(walk, count: 6)

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:

let moveRight = SKAction.moveToX(900, duration: walkAnim.duration)
let 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:

let mirrorDirection = SKAction.scaleXTo(-1, y:1, duration:0.0)
let 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:

let walkAndMoveRight = SKAction.group([resetDirection,  walkAnim, moveRight]);
let walkAndMoveLeft  = SKAction.group([mirrorDirection, walkAnim, moveLeft]);

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

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:

override func touchesBegan(touches: NSSet, withEvent event: UIEvent)
{
    let sprite = SKSpriteNode(texture: sheet.capguy_walk_0001())
    sprite.position = CGPointMake(100.0, CGFloat(rand() % 100) + 200.0)

    sprite.runAction(sequence)
    addChild(sprite)
}

The complete SpriteKit sample

iPhone screenshot complete sample

Did you like the tutorial? Please share!

Source code available for download

The source code is available on GitHub. Clone it using git:

git clone https://github.com/CodeAndWeb/TexturePacker-SpriteKit-Swift.git

or download one of the archives:
TexturePacker-SpriteKit-Swift.zip TexturePacker-SpriteKit-Swift.tar.gz