Working with Sprites

Sprites are the basic building blocks used to create the majority of your scene’s content, so understanding sprites is useful before moving on to other node classes in SpriteKit. Sprites are represented by SKSpriteNode objects. An SKSpriteNode object can be drawn either as a rectangle with a texture mapped onto it or as a colored, untextured rectangle. Textured sprites are more common, because they represent the primary way that you bring custom artwork into a scene. This custom artwork might represent characters in your game, background elements, or even user interface elements, but the basic strategy is the same. An artist creates the images, and your game loads them as textures. Then you create sprites with those textures and add them to the scene.

Creating a Textured Sprite

The simplest way to create a textured sprite is to have SpriteKit create both the texture and the sprite for you. You store the artwork in the app bundle, and then load it at runtime. Listing 2-1 shows how simple this code can be.

Listing 2-1  Creating a textured sprite from an image stored in the bundle

SKSpriteNode *spaceship = [SKSpriteNode spriteNodeWithImageNamed:@"rocket.png"];
spaceship.position = CGPointMake(100,100);
[self addChild: spaceship];

When you create a sprite in this fashion, you get a lot of default behavior for free:

The default behavior gives you a useful foundation for creating a sprite-based game. You already know enough to add artwork to your game, create sprites, and run actions on those sprites to do interesting things. As sprites move onscreen and offscreen, SpriteKit does its best to efficiently manage textures and draw frames of animation. If that is enough for you, take some time to explore what you can do with sprites. Or, keep reading for a deeper dive into the SKSpriteNode class. Along the way, you'll gain a thorough understanding of its capabilities and how to communicate those capabilities to your artists and designers. And you will learn more advanced ways to work with textures and how to improve performance with texture-based sprites.

Customizing a Textured Sprite

You can use each sprite’s properties to independently configure four distinct rendering stages:

Often, configuring a sprite to perform these four steps—positioning, sizing, colorizing, and blending—is based on the artwork used to create the sprite’s texture. This means that you rarely set property values in isolation from the artwork. You work with your artist to ensure that your game is configuring the sprites to match the artwork.

Here are some of the possible strategies you can follow:

Using the Anchor Point to Move the Sprite’s Frame

By default, the sprite’s frame—and thus its texture—is centered on the sprite’s position. However, you might want a different part of the texture to appear at the node’s position. You usually do this when the game element depicted in the texture is not centered in the texture image.

A sprite node’s anchorPoint property determines which point in the frame is positioned at the sprite’s position. Anchor points are specified in the unit coordinate system, shown in Figure 2-1. The unit coordinate system places the origin at the bottom left corner of the frame and (1,1) at the top right corner of the frame. A sprite’s anchor point defaults to (0.5,0.5), which corresponds to the center of the frame.

Figure 2-1  The unit coordinate system

Although you are moving the frame, you do this because you want the corresponding portion of the texture to be centered on the position. Figure 2-2 shows a pair of texture images. In the first, the default anchor point centers the texture on the position. In the second, a point at the top of the image is selected instead. You can see that when the sprite is rotated, the texture image rotates around this point.

Figure 2-2  Changing a sprite’s anchor point

Listing 2-2 shows how to place the anchor point on the rocket’s nose cone. Usually, you set the anchor point when the sprite is initialized, because it corresponds to the artwork. However, you can set this property at any time. The frame is immediately updated, and the sprite onscreen is updated the next time the scene is rendered.

Listing 2-2  Setting a sprite’s anchor point

rocket.anchorPoint = CGPointMake(0.5,1.0);

Resizing a Sprite

The size of the sprite’s frame property is determined by the values of three other properties:

  • The sprite’s size property holds the base (unscaled) size of the sprite. When a sprite is initialized using the code in Listing 2-1, the value of this property is initialized to be equal to the size of the sprite’s texture.

  • The base size is then scaled by the sprite’s xScale and yScale properties inherited from the SKNode class.

For example, if the sprite’s base size is 32 x 32 pixels and it has an xScale value of 1.0 and a yScale value of 2.0, the size of the sprite’s frame is 32 x 64 pixels.

When a sprite’s frame is larger than its texture, the texture is stretched to cover the frame. Normally, the texture is stretched uniformly across the frame, as shown in Figure 2-3.

Figure 2-3  A texture is stretched to cover the sprite’s frame

However, sometimes you want to use sprites to build user interface elements, such as buttons or health indicators. Often, these elements contain fixed-size elements, such as end caps, that should not be stretched. In this case, use a portion of the texture without stretching, and then stretch the remaining part of the texture over the rest of the frame.

The sprite’s centerRect property, which is specified in unit coordinates of the texture, controls the scaling behavior. The default value is a rectangle that covers the entire texture, which is why the entire texture is stretched across the frame. If you specify a rectangle that only covers a portion of the texture, you create a 3 x 3 grid. Each box in the grid has its own scaling behavior:

  • The portions of the texture in the four corners of the grid are drawn without any scaling.

  • The center of the grid is scaled in both dimensions.

  • The upper- and lower-middle parts are only scaled horizontally.

  • The left- and right-middle parts are only scaled vertically.

Figure 2-4 shows a close-up view of a texture you might use to draw a user interface button. The complete texture is 28 x 28 pixels. The corner pieces are each 12 x 12 pixels and the center is 4 X 4 pixels.

Figure 2-4  A stretchable button texture

Listing 2-3 shows how this button sprite would be initialized. The centerRect property is computed based on the design of the texture.

Listing 2-3  Setting the sprite’s center rect to adjust the stretching behavior

SKSpriteNode *button = [SKSpriteNode spriteNodeWithImageNamed:@"stretchable_button.png"];
button.centerRect = CGRectMake(12.0/28.0,12.0/28.0,4.0/28.0,4.0/28.0);

Figure 2-5 shows that the corners remain the same, even when the button is drawn at different sizes.

Figure 2-5  Applying the button texture to buttons of different sizes

Colorizing a Sprite

You can use the color and colorBlendFactor properties to colorize the texture before applying it to the sprite. The color blend factor defaults to 0.0, which indicates that the texture should be used unmodified. As you increase this number, more of the texture color is replaced with the blended color. For example, when a monster in your game takes damage, you might want to add a red tint to the character. Listing 2-4 shows how you would apply a tint to the sprite.

Listing 2-4  Tinting the color of the sprite

monsterSprite.color = [SKColor redColor];
monsterSprite.colorBlendFactor = 0.5;
Figure 2-6  Colorizing adjusts the color of the texture

You can also animate the color and color blend factors using actions. Listing 2-5 shows how to briefly tint the sprite and then return it to normal.

Listing 2-5  Animating a color change

SKAction *pulseRed = [SKAction sequence:@[
                        [SKAction colorizeWithColor:[SKColor redColor] colorBlendFactor:1.0 duration:0.15],
                        [SKAction waitForDuration:0.1],
                        [SKAction colorizeWithColorBlendFactor:0.0 duration:0.15]]];     [monsterSprite runAction: pulseRed];

Blending the Sprite into the Framebuffer

The final stage of rendering is to blend the sprite’s texture into its destination framebuffer. The default behavior uses the alpha values of the texture to blend the texture with the destination pixels. However, you can use other blend modes when you want to add other special effects to a scene.

You control the sprite’s blending behavior using the blendMode property. For example, an additive blend mode is useful to combine multiple sprites together, such as for fire or lighting. Listing 2-6 shows how to change the blend mode to use an additive blend.

Listing 2-6  Using an additive blend mode to simulate a light

lightFlareSprite.blendMode = SKBlendModeAdd;

Working with Texture Objects

Although SpriteKit can create textures for you automatically when a sprite is created, in more complex games you need more control over textures. For example, you might want to do any of the following:

You do all of these things by working directly with SKTexture objects. You create an SKTexture object and then use it to create new sprites or change the texture of an existing sprite.

Creating a Texture from an Image Stored in the App Bundle

Listing 2-7 shows an example similar to Listing 2-1, but now the code explicitly creates a texture object. The code then creates multiple rockets from the same texture.

Listing 2-7  Loading a texture from the bundle

SKTexture *rocketTexture = [SKTexture textureWithImageNamed:@"rocket.png"];
for (int i= 0; i< 10; i++)
{
    SKSpriteNode *rocket = [SKSpriteNode spriteNodeWithTexture:rocketTexture];
    rocket.position = [self randomRocketLocation];
    [self addChild: rocket];
}

The texture object itself is just a placeholder for the actual texture data. The texture data is more resource intensive, so SpriteKit loads it into memory only when needed.

Using Texture Atlases to Collect Related Art Assets

Art assets stored in your app bundle aren’t always unrelated images. Sometimes they are collections of images that are being used together for the same sprite. For example, here are a few common collections of art assets:

  • Animation frames for a character

  • Terrain tiles used to create a game level or puzzle

  • Images used for user interface controls, such as buttons, switches, and sliders

If each texture is treated as a separate object, then SpriteKit and the graphics hardware must work harder to render scenes—and your game’s performance might suffer. Specifically, SpriteKit must make at least one drawing pass per texture. To avoid making multiple drawing passes, SpriteKit uses texture atlases to collect related images together. You specify which assets should be collected together, and Xcode builds a texture atlas automatically. Then, when your game loads the texture atlas, SpriteKit manages all the images inside the atlas as if they were a single texture. You continue to use SKTexture objects to access the elements contained in the atlas.

Creating a Texture Atlas

Xcode can automatically build texture atlases for you from a collection of images. The process is described in detail in Texture Atlas Help.

When you create a texture atlas, you want to strike a balance between collecting too many textures or too few. If you use too few images, SpriteKit may still need many drawing passes to render a frame. If you include too many images, then large amounts of texture data may need to be loaded into memory at once. Because Xcode builds the atlases for you, you can switch between different atlas configurations with relative ease. So experiment with different configurations of your texture atlases and choose the combination that gives you the best performance.

Loading Textures from a Texture Atlas

The code in Listing 2-7 is also used to load textures from a texture atlas. SpriteKit searches first for an image file with the specified filename. If it doesn’t find one, it searches inside any texture atlases built into the app bundle. This means that you don’t have to make any coding changes to support this in your game. This design also offers your artists the ability to experiment with new textures without requiring that your game be rebuilt. The artists drop the textures into the app bundle. When the app is relaunched, SpriteKit automatically discovers the textures (overriding any previous versions built into the texture atlases). When the artists are satisfied with the textures, you then add those textures to the project and bake them into your texture atlases.

If you want to explicitly work with texture atlases, use the SKTextureAtlas class. First, create a texture atlas object using the name of the atlas. Next, use the names of the image files stored in the atlas to look up the individual textures. Listing 2-8 shows an example of this. It uses a texture atlas that holds multiple frames of animation for a monster. The code loads those frames and stores them in an array. In the actual project, you would add a monster.atlas folder with the four image files.

Listing 2-8  Loading textures for a walk animation

SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:@"monster"];
SKTexture *f1 = [atlas textureNamed:@"monster-walk1.png"];
SKTexture *f2 = [atlas textureNamed:@"monster-walk2.png"];
SKTexture *f3 = [atlas textureNamed:@"monster-walk3.png"];
SKTexture *f4 = [atlas textureNamed:@"monster-walk4.png"];
NSArray *monsterWalkTextures = @[f1,f2,f3,f4];

Creating a Texture from a Subsection of a Texture

If you already have an SKTexture object, you can create new textures that reference a portion of it. This approach is efficient because the new texture objects reference the same texture data in memory. (This behavior is similar to that of the texture atlas.) Typically, you use this approach if your game already has its own custom texture atlas format. In this case, you are responsible for storing the coordinates for the individual images stored in the custom texture atlas.

Listing 2-9 shows how to extract a portion of a texture. The coordinates for the rectangle are in the unit coordinate space.

Listing 2-9  Using part of a texture

SKTexture *bottomLeftTexture = [SKTexture textureWithRect:CGRectMake(0.0,0.0,0.5,0.5) inTexture:cornerTextures];

Other Ways to Create Textures

In addition to loading textures from the app bundle, you can create textures from other sources:

  • Use the SKTexture initializer methods to create textures from properly formatted pixel data in memory, from core graphics images, or by applying a Core Image filter to an existing texture.

  • The SKView class’s textureFromNode: method can render a node tree’s content into a texture. The texture is sized so that it holds the contents of the node and all of its visible descendants.

Changing a Sprite’s Texture

A sprite’s texture property points to its current texture. You can change this property to point to a new texture. The next time the scene renders a new frame, it renders with the new texture. Whenever you change the texture, you may also need to change other sprite properties—such as size, anchorPoint, and centerRect—to be consistent with the new texture. Usually, it is better to ensure that all the artwork is consistent so that the same values can be used for all of the textures. That is, the textures should have a consistent size and anchor point placement so that your game does not need to update anything other than the texture.

Because animation is a common task, you can use actions to animate a series of textures on a sprite. The code in Listing 2-10 shows how to use the array of animation frames created in Listing 2-8 to animate a sprite’s texture.

Listing 2-10  Animating through a series of textures

SKAction *walkAnimation = [SKAction animateWithTextures:monsterWalkTextures timePerFrame:0.1]
[monster runAction:walkAnimation];
// insert other code here to move the monster.

SpriteKit provides the plumbing that allows you to animate or change a sprite’s texture. It doesn’t impose a specific design on your animation system. This means you need to determine what kinds of animations that a sprite may need and then design your own animation system to switch between those animations at runtime. For example, a monster might have walk, fight, idle, and death animation sequences—and it’s up to you to decide when to switch between these sequences.

Preloading Textures Into Memory

A major advantage to SpriteKit is that it performs a lot of memory management for you automatically. When rendering a new frame of animation, SpriteKit determines whether a texture is needed to render the current frame. If a texture is needed but is not prepared for rendering, SpriteKit loads the texture data from the file, transforms the data into a format that the graphics hardware can use, and uploads it to the graphics hardware. This process happens automatically in the background, but it isn’t free. If too many unloaded textures are needed at once, it may be impossible to load all the textures in a single frame of animation, causing the frame rate to stutter. To avoid this problem, you need to preload textures into memory, particularly in larger or complex games.

Listing 2-11 shows how to preload an array of SKTexture objects. The preloadTextures:withCompletionHandler: method calls the completion handler after all of the textures are loaded into memory. In this example, all of the textures for a particular level of the game are preloaded in a single operation. When the textures are all in memory, the completion handler is called. It creates the scene and presents it. (You need to add code to provide these texture objects to the scene; that code isn’t shown here).

Listing 2-11  Preloading a texture

[SKTexture preloadTextures:textureArrayForLevel1 withCompletionHandler:^
    {
        // The textures are loaded into memory. Start the level.
        GamePlayScene* gameScene = [[GamePlayScene alloc] initWithSize:CGSizeMake(768,1024)];
        SKView *spriteView = (SKView *) self.view;
        [spriteView presentScene: gameScene];
    }];

Because you are intimately familiar with the design of your game, you are the best person to know when new textures are needed. The exact design of your preloading code is going to depend on your game engine. Here are a few possible designs to consider:

  • For a small game, you may be able to preload all of its textures when the app is launched, and then keep them in memory forever.

  • For a larger game, you may need to split the textures into levels or themes. Each level or theme’s textures are designed to fit in a specific amount of memory. When the player starts a new level, you preload all of that level’s texture objects. When the player finishes playing the level, the textures not needed for the next level are discarded. By preloading the levels, the load time is all up front, before gameplay starts.

  • If a game needs more textures than can fit into memory, you need to preload textures dynamically as the game is being played. Typically, you preload some textures at the start of a level, and then load other textures when you think they will be needed soon. For example, in a racing game, the player is always moving in the same direction, so for each frame you might fetch a new texture for content the player is about to see. The textures are loaded in the background, displacing the oldest textures for the track. In an adventure game that allows for player-controlled movement, you might have to provisionally load textures when a player is moving in a particular direction.

Removing a Texture From Memory

After a texture is loaded into the graphics hardware’s memory, it stays in memory until the referencing SKTexture object is deleted. This means that between levels (or in a dynamic game), you may need to make sure a texture object is deleted. Delete a SKTexture object object by removing any strong references to it, including:

  • All texture references from SKSpriteNode and SKEffectNode objects in your game

  • Any strong references to the texture in your own code

  • An SKTextureAtlas object that was used to create the texture object

Creating Untextured Sprites

Although textured sprites are the most common way to use the SKSpriteNode class, you can also create sprite nodes without a texture. The behavior of the class changes when the sprite lacks a texture:

The other properties (size, anchorPoint, and blendMode) work the same.

Try This!

Now that you know more about sprites, try some of the following activities:

You can find useful code in the Sprite Tour sample.