Working with Other Node Types

Although sprites are the most important element you use when building a game, Sprite Kit provides many other node classes. Many of these node classes provide visual content, similar to the SKSpriteNode class. Others do not directly draw content of their own, but instead modify the behavior of their descendants in the node tree. Table 6-1 lists all the node classes provided by Sprite Kit, including the SKScene and SKSpriteNode classes you are already familiar with.

Table 6-1  Sprite Kit node classes

Class

Description

SKNode

The class from which all node classes are derived. It does not draw anything.

SKScene

A scene is the root node in the node tree. It handles animation and action processing.

SKSpriteNode

A node that draws a textured sprite.

SKLabelNode

A node that renders a text string.

SKShapeNode

A node that renders a shape based on a Core Graphics path.

SKVideoNode

A node that plays video content.

SKEmitterNode

A node that creates and renders particles.

SKCropNode

A node that crops its child nodes using a mask.

SKEffectNode

A node that applies a Core Image filter to its child nodes.

Almost all of the techniques that work with sprite nodes can be applied to other node types as well. For example, you can use actions to animate other node objects onscreen, manipulate the order in which they are rendered, and use them inside the physics simulation. Read on to learn about how to use these other node classes in your game. After you become familiar with these classes, you will understand all of the visual capabilities of Sprite Kit. You can then start designing your game’s appearance.

Basic Nodes

The SKNode class doesn’t draw any visual content. Its primary role is to provide baseline behavior that the other node classes use. However, this doesn’t mean you can’t find useful ways to use SKNode objects in your game. Here are some of the ways you might use basic nodes inside your game engine:

There are advantages to having a node in the tree to represent these concepts:

Subclassing the SKNode class is a useful way to build up more complex behaviors in your game. See “Use Subclassing to Create Your Own Node Behaviors.”

Display Text with Label Nodes

Just about every game needs to display text at some point, even if it is just to display “Game Over” to the player. If you had to implement this yourself in OpenGL, it takes a fair amount of work to get it correct. But Sprite Kit makes it easy! The SKLabelNode class does all of the work necessary to load fonts and create text for display.

Listing 6-1 demonstrates how to create a new text label.

Listing 6-1  Adding a text label

        SKLabelNode *winner = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        winner.text = @"You Win!";
        winner.fontSize = 65;
        winner.fontColor = [SKColor greenColor];
        winner.position = CGPointMake(CGRectGetMidX(self.bounds),
                                     CGRectGetMidY(self.bounds));
        [self addChild:winner];

Whenever you change the label node’s properties, the label node is automatically updated the next time the scene is rendered.

Shape Nodes Draw Path-Based Shapes

The SKShapeNode class draws a standard Core Graphics path. The graphics path is a collection of straight lines and curves that can define either open or closed subpaths. The shape node includes separate properties to specify the color of the lines and the color to fill the interior.

Shape nodes are useful for content that cannot be easily decomposed into textured sprites. Shape nodes are also very useful for building and displaying debugging information on top of your game content. However, textured sprites offer higher performance than shape nodes, so use shape nodes sparingly.

Listing 6-2 shows an example of how to create a shape node. The example creates a circle with a blue interior and a white outline. The path is created and attached to the shape node’s path property.

Listing 6-2  Creating a shape node from a path

SKShapeNode *ball = [[SKShapeNode alloc] init];
 
CGMutablePathRef myPath = CGPathCreateMutable();
CGPathAddArc(myPath, NULL, 0,0, 15, 0, M_PI*2, YES);
ball.path = myPath;
 
ball.lineWidth = 1.0;
ball.fillColor = [SKColor blueColor];
ball.strokeColor = [SKColor whiteColor];
ball.glowWidth = 0.5;

You can see from the code that the shape has three essential elements:

You can disable any of these elements by setting its color to [SKColor clearColor].

The shape node provides properties that let you control how the shape is blended into the framebuffer. You use these properties the same way as the properties of the SKSpriteNode class. See “Blending the Sprite into the Framebuffer.”

A Video Node Plays a Movie

The SKVideoNode class uses the AV Foundation framework to display movie content. Like any other node, you can put the movie node anywhere inside the node tree and Sprite Kit will render it properly. For example, you might use a video node to animate some visual behaviors that would be expensive to define using actions.

A video node is similar to a sprite node, but offers only a subset of the features:

However, the following limitations apply:

Like most of the node classes, creating a video node is very simple. Listing 6-3 shows a typical usage pattern. It initializes the video node using a video stored in the app bundle and then adds the node to the scene. It calls the node’s play method to start the video playback.

Listing 6-3  Displaying a video in a scene

SKVideoNode *sample = [SKVideoNode videoNodeWithVideoFileNamed:@"sample.m4v"];
sample.position = CGPointMake(CGRectGetMidX(self.frame),
                                 CGRectGetMidY(self.frame));
[self addChild: sample];
[sample play];

You control playback using the node’s play and pause methods.

If you need more precise control over the video playback behavior, you can use AV Foundation to create an AVPlayer object from your video content, and then use this object to initialize the video node. Then, instead of using the node’s playback methods, you use the AVPlayer object to control playback. The video content is automatically displayed in the video node. For more information, see AV Foundation Programming Guide.

Emitter Nodes Create Particle Effects

When an SKEmitterNode object is placed in a scene, it automatically creates and renders new particles. You use emitter nodes to automatically create special effects, including rain, explosions, or fire.

A particle is similar to an SKSpriteNode object; it renders a textured or untextured image that is sized, colorized, and blended into the scene. However, particles differ from sprites in two important ways:

Particles are purely visual objects, and their behavior is entirely defined by the emitter node that created them. The emitter node contains many properties to control the behavior of the particles it spawns, including:

Use the Particle Emitter Editor to Experiment with Emitters

In most cases, you never need to configure an emitter node directly in your game. Instead, you use Xcode to configure an emitter node’s properties. As you change the behavior of the emitter node, Xcode immediately provides you an updated visual effect. When complete, Xcode archives the configured emitter. Then, at runtime, your game uses this archive to instantiate a new emitter node.

Using Xcode to create your emitter nodes has a few important advantages:

  • It is the best way to learn the capabilities of the emitter class.

  • You can more quickly experiment with new particle effects and see the results immediately.

  • You separate the task of designing a particle effect from the programming task of using it. Your artists can work on new particle effects independent of your game code.

For more information on using Xcode to create particle effects, see Particle Emitter Editor Guide.

Listing 6-4 shows how to load a particle effect that was created by Xcode. All particle effects are saved using Cocoa’s standard archiving mechanisms, so the code first creates a path to the smoke effect, and then loads the archive.

Listing 6-4  Loading a particle effect from a file

- (SKEmitterNode *) newSmokeEmitter
{
    NSString *smokePath = [[NSBundle mainBundle] pathForResource:@"smoke" ofType:@"sks"];
    SKEmitterNode *smoke = [NSKeyedUnarchiver unarchiveObjectWithFile:smokePath];
    return smoke;
}

Manually Configuring Particle Creation

The SKEmitterNode class provides many properties for configuring an emitter node’s behavior. The Xcode inspector sets the same property values. You can also create and configure your own emitter, or you can take an emitter node created in the Particle Emitter Editor, load it, and change its property values. For example, assume for a moment that you are using the smoke effect in Listing 6-4 to show damage to a rocket ship. As the ship takes more damage, you could increase the birth rate of the emitter to add more smoke.

When the emitter node is in a scene, it emits new particles. You use the following properties to define how many particles it creates:

  • The particleBirthRate property specifies the number of particles that the emitter creates every second.

  • The numParticlesToEmit property specifies how many particles are created before the emitter turns itself off. You can also configure the node to emit an unlimited number of particles.

When a particle is created, its initial property values are determined by the properties of the emitter. For each of the particle’s properties, the emitter class declares up to four properties:

  • The average starting value for the property.

  • A random range for values of the property. Each time a new particle is emitted, a new random value is calculated within that range.

  • The rate at which the value changes over time, also known as the property’s speed. Not all properties have a speed property.

  • An optional keyframe sequence.

The complete list of properties used to configure an emitter node is described in SKEmitterNode Class Reference.

Listing 6-6 shows how you might configure an emitter’s scale property. This is a simplified version of a node’s xScale and yScale properties, and determines how large the particle is.

Listing 6-5  Configuring a particle’s scale properties

myEmitter.particleScale = 0.3;
myEmitter.particleScaleRange = 0.2;
myEmitter.particleScaleSpeed = -0.1;

When a new particle is created, its scale value is a random number from 0.2 to 0.4. The scale value then decreases at a rate of 0.1 per second. So, if a particular particle started at the average value, 0.3, it would decrease from 0.3 to 0 over a period of 3 seconds.

Using Keyframe Sequences to Configure Custom Ramps for a Particle Property

Keyframe sequences provide more sophisticated behaviors for a particle property. A keyframe sequence specifies multiple points in a particle’s lifetime and specifies the value for a property at each point. The keyframe sequence then interpolates values between those points and uses them to simulate the particle’s property value.

You can use keyframe sequences to implement many custom behaviors, including:

  • Changing a property value until it reaches a specific value.

  • Using multiple different property values over the lifetime of a particle. For example, you might increase the value of the property in one part of the sequence and decrease it in another. Or, when specifying colors, you might specify multiple colors that the particle cycles through during its lifetime.

  • Changing a property value using a nonlinear curve or a stepping function.

Listing 6-6 shows how you might replace the code in Listing 6-5 to use a sequence. When you use a sequence, the values are not randomized. Instead, the sequence specifies all of the values of the property. Each keyframe value includes a value object and a timestamp. The timestamps are specified in a range from 0 to 1.0, where 0 represents the birth of the particle and 1.0 represents its death. So, for this sequence, the particle starts out with a scale of 0.2 and increases to 0.7 one quarter of the way through the sequence. Three quarters of the way through the sequence, it reaches its minimum size, 0.1. It remains at this size until it dies.

Listing 6-6  Using a sequence to change a particle’s scale property

SKKeyframeSequence  *scaleSequence = [[SKKeyframeSequence alloc] initWithKeyframeValues:@[@0.2,@0.7,@0.1] times:@[@0.0,@0.250,@0.75]];
myEmitter.particleScaleSequence = scaleSequence;

Adding Actions to Particles

Although you do not have direct access to the particles created by Sprite Kit, you can specify an action that all particles execute. Whenever a new particle is created, the emitter tells the particle to run that action. You can use actions to create very sophisticated behaviors.

For the purpose of using actions on particles, you can treat the particle as if it were a sprite. This means you can perform other interesting tricks, such as animating the particle’s textures.

Using Target Nodes to Change the Destination of Particles

When the emitter creates particles, they are rendered as children of the emitter node. This means that they inherit the characteristics of the emitter node, just like nodes do. For example, if you rotate the emitter node, the positions of all of the spawned particles are rotated also. Depending on what effect you are simulating with the emitter, this may not be the correct behavior. For example, assume that you are using the emitter node to create the exhaust from a rocket. When the engines are at full burn, a cone of flame should come out the back of the ship. This is easily simulated using particles. But if the particles are rendered relative to the ship, when the ship turns, the exhaust is going to rotate as well. That doesn’t look right. What you really want is for the particles to be spawned, but thereafter be independent of the emitter node. When the emitter node is rotated, new particles get the new orientation, and old particles maintain their old orientation. You make particles independent of the emitter by specifying a target node.

Listing 6-7 shows how to configure a rocket exhaust effect to use a target node. When the custom sprite node class instantiates the exhaust node, it makes the node its child. However, it redirects the particles to the scene.

Listing 6-7  Using a target node to redirect where particles are spawned

- (void) newExhaustNode
{
    SKEmitterNode *emitter =  [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:@"exhaust" ofType:@"sks"]];
 
// Place the emitter at the rear of the ship.
   emitter.position = CGPointMake(0,-40);
   emitter.name = @"exhaust";
// Send the particles to the scene.
   emitter.targetNode = self.scene;
 
   [self addChild:emitter];
}

When an emitter has a target node, it calculates the position, velocity, and orientation of the particle, exactly as if it were a child of the sprite node. This means that if the ship sprite is rotated, the exhaust orientation is automatically rotated also. However, at the moment a new particle’s starting values are calculated, the values are transformed into the target node’s coordinate system. Thereafter, they would only be affected by changes to the target node.

Particle Emitter Tips

Particle emitters in Sprite Kit are one of the most powerful tools for building visual effects. However, used incorrectly, particle emitters can be a bottleneck in the design and implementation of your app. Consider the following tips:

  • Use Xcode to create and test your particle effects, then load the archives in your game.

  • Adjust emitter properties sparingly inside your game code. Typically, you do this to specify properties that cannot be specified in the Xcode inspector or to control properties inside your game logic.

  • Particles are cheaper than a sprite node, but they still have overhead! Try to keep the number of particles onscreen to a minimum by creating particle emitters with a low birth rate, and specifying a short lifetime for particles. For example, instead of creating hundreds or thousands of particles per second, reduce the birth rate and increase the size of the particles slightly. Often, you can create effects with fewer particles but the same net visual appearance.

  • Use actions on particles only when there isn’t another solution. Executing actions on individual particles is potentially very expensive, especially if the particle emitter also has a high birth rate.

  • Assign a target node whenever the particles should be independent of the emitter node after they are spawned. For examples, particles should be independent if the emitter node moves or rotates in the scene.

  • Consider removing a particle emitter from the scene when it is not visible onscreen. Add it just before it becomes visible.

Crop Nodes Mask Portions of the Scene

An SKCropNode object does not directly render content, like a sprite node. Instead, it alters the behavior of its children when they are rendered. A crop node crops out portions of the content rendered by the children. This makes a crop node useful for implementing cockpit views, controls, and other game indicators, as well as any effect where the children should not draw outside of a specific region of the scene. Figure 6-1 uses the rocket ship art as a mask for a child sprite drawn by the crop node.

Figure 6-1  A crop node performs a masking operation

The cropped area is specified using a mask. The mask is not a fixed image. It is rendered from a node, just like any other content in Sprite Kit. This means a crop node can create simple masks, but it can also implement more sophisticated behaviors. For example, here are some ways you might specify a mask:

Listing 6-8 shows a simple use of a mask. This code loads a mask image from a texture in the app bundle. A portion of the scene’s content is then rendered, using the mask to prevent it from overdrawing the portion of the screen that the game uses to show controls.

Listing 6-8  Creating a crop node

SKCropNode *cropNode = [[SKCropNode alloc] init];
myCropNode.position = CGPointMake(CGRectGetMidX(self.bounds),
                                  CGRectGetMidY(self.bounds));
 
cropNode.maskNode = [[SKSpriteNode alloc] initWithImageNamed:@"cockpitMask"];
[cropNode addChild: gamePlayNode];
[self addChild:cropNode];
[self addChild:gameControlNodes];

When the crop node is rendered, the mask is rendered before the descendants are drawn. Only the alpha component of the resulting mask is relevant. Any pixel in the mask with an alpha value of 0.05 or higher is rendered. All other pixels are cropped.

Effect Nodes Apply Special Effects to Their Descendants

An SKEffectNode object does not draw content of its own. Instead, each time a new frame is rendered using the effect node, the effect node follows these steps:

  1. Draws its children into a private framebuffer.

  2. Applies a Core Image effect to the private framebuffer. This stage is optional.

  3. Blends the contents of its private framebuffer into its parent’s framebuffer, using one of the standard sprite blend modes.

  4. Discards its private framebuffer.

Figure 6-2 shows one possible use for an effect node. In this example, the effect node’s children are two sprites that act as light nodes. The effect node accumulates the effects of these lights, applies a blur filter to soften the resulting image, and then uses a multiply blend mode to apply this lighting to a wall texture.

Figure 6-2  Effect nodes apply special effects to a node’s children

Here’s how the scene generates the lighting effect:

  1. The scene has two children. The first is a textured sprite that represents the ground. The second is an effect node to apply lighting.

    self.lightingNode = [[SKEffectNode alloc] init];
  2. The effect node’s children are sprite nodes rendered using an additive blend mode.

    SKSpriteNode *light = [SKSpriteNode spriteNodeWithTexture:lightTexture];
    light.blendMode = SKBlendModeAdd;
    ...
    [self.lightingNode addChild: light];
  3. The effect node includes a filter effect to soften the lighting.

    - (CIFilter *)blurFilter
    {
        CIFilter *filter = [CIFilter filterWithName:@"CIBoxBlur"]; // 3
        [filter setDefaults];
        [filter setValue:[NSNumber numberWithFloat:20] forKey:@"inputRadius"];
        return filter;
    }
     
    self.lightingNode.filter = [self blurFilter];

    If you specify a Core Image filter, it must be a filter that takes a single input image and produces a single output image.

  4. The effect node uses a multiplication blend mode to apply its lighting effect to the scene’s framebuffer.

    self.lightingNode.blendMode = SKBlendModeMultiply;

Scenes Are Effect Nodes

You’ve learned a lot about the SKScene class already, but you may not have noticed that it is a subclass of SKEffectNode. This means that any scene can apply a filter to the contents. Although applying filters can be very expensive—not all filters are well designed for interactive effects—experimentation can help you find some interesting ways to use filters.

Caching May Improve Performance of Static Content

An effect node normally discards its private framebuffer after rendering is complete. Rendering the content is necessary because it typically changes every frame. However, the cost of recreating the content and applying a Core Image filter can be high. If the content is static, then this is unnecessary. It might make more sense to keep the rendered framebuffer instead of discarding it. If the content of the effect node is static, you can set the node’s shouldRasterize property to YES. Setting this property causes the following changes in behavior:

  • The framebuffer is not discarded at the end of rasterization. This also means that more memory is being used by the effect node, and rendering may take slightly longer.

  • When a new frame is rendered, the framebuffer is rendered only if the content of the effect node’s descendants have changed.

  • Changing the Core Image filter’s properties no longer causes the framebuffer to automatically be updated. You can force it to be updated by setting the shouldRasterize property to NO.