SpriteKit From Scratch: Advanced Techniques and Optimizations

Introduction

In this tutorial, the fifth and final installment of the SpriteKit From Scratch series, we look at some advanced techniques you can use to optimize your SpriteKit-based games to improve performance and user experience.

This tutorial requires that you are running Xcode 7.3 or higher, which includes Swift 2.2 and the iOS 9.3, tvOS 9.2, and OS X 10.11.4 SDKs. To follow along, you can either use the project you created in the previous tutorial or download a fresh copy from GitHub.

The graphics used for the game in this series can be found on GraphicRiver. GraphicRiver is a great source for finding artwork and graphics for your games.

1. Texture Atlases

In order to optimize the memory usage of your game, SpriteKit provides the functionality of texture atlases in the form of the SKTextureAtlas class. These atlases effectively combine the textures you specify into a single, large texture that takes up less memory than the individual textures on their own. 

Luckily, Xcode can create texture atlases very easily for you. This is done in the same asset catalogs that are used for other images and resources in your games. Open your project and navigate to the Assets.xcassets asset catalog. At the bottom of the left sidebar, click the + button and select the New Sprite Atlas option.

Create a New Sprite Atlas

As a result, a new folder is added to the asset catalog. Click the folder once to select it and click again to rename it. Name it Obstacles. Next, drag the Obstacle 1 and Obstacle 2 resources into this folder. You can also delete the blank Sprite asset that Xcode generates if you want to, but that is not required. When completed, your expanded Obstacles texture atlas should look like this:

Obstacles Texture Atlas

It's now time to use the texture atlas in code. Open MainScene.swift and add the following property to the MainScene class. We initialize a texture atlas using the name we entered in our asset catalog.

While not required, you can preload the data of a texture atlas into memory before it is used. This allows your game to eliminate any lag that might occur when loading the texture atlas and retrieving the first texture from it. Preloading a texture atlas is done with a single method and you can also run a custom code block once the loading has completed.

In the MainScene class, add the following code at the end of the didMoveToView(_:) method:

To retrieve a texture from a texture atlas, you use the textureNamed(_:) method with the name you specified in the asset catalog as a parameter. Let's update the spawnObstacle(_:) method in the MainScene class to use the texture atlas we created a moment ago. We fetch the texture from the texture atlas and use it to create a sprite node.

Note that, if your game takes advantage of On-Demand Resources (ODR), you can easily specify one or more tags for each texture atlas. Once you have successfully accessed the correct resource tag(s) with the ODR APIs, you can then use your texture atlas just like we did in the spawnObstacle(_:) method. You can read more about On-Demand Resources in another tutorial of mine.

2. Saving and Loading Scenes

SpriteKit also offers you the ability to easily save and load scenes to and from persistent storage. This allows players to quit your game, have it relaunched at a later time, and still be up to the same point in your game as they were before.

The saving and loading of your game is handled by the NSCoding protocol, which the SKScene class already conforms to. SpriteKit's implementation of the methods required by this protocol automatically allow for all of the details in your scene to be saved and loaded very easily. If you want to, you can also override these methods to save some custom data along with your scene.

Because our game is very basic, we are going to use a simple Bool value to indicate whether the car has crashed. This shows you how to save and load custom data that is tied to a scene. Add the following two methods of the NSCoding protocol to the MainScene class.

If you are unfamiliar with the NSCoding protocol, the encodeWithCoder(_:) method handles the saving of your scene and the initializer with a single NSCoder parameter handles the loading.

Next, add the following method to the MainScene class. The saveScene() method creates an NSData representation of the scene, using the NSKeyedArchiver class. To keep things simple, we store the data in NSUserDefaults.

Next, replace the implementation of didBeginContactMethod(_:) in the MainScene class with the following:

The first change made to this method is editing the player node's categoryBitMask rather than removing it from the scene entirely. This ensures that upon reloading the scene, the player node is still there, even though it is not visible, but that duplicate collisions are not detected. The other change made is calling the saveScene() method we defined earlier once the custom explosion logic has been run.

Finally, open ViewController.swift and replace the viewDidLoad() method with the following implementation:

When loading the scene, we first check to see if there is saved data in the standard NSUserDefaults. If so, we retrieve this data and recreate the MainScene object using the NSKeyedUnarchiver class. If not, we get the URL for the scene file we created in Xcode and load the data from it in a similar manner.

Run your app and run into an obstacle with your car. At this stage, you don't see a difference. Run your app again, though, and you should see that your scene has been restored to exactly how it was when you just crashed the car.

3. The Animation Loop

Before each frame of your game renders, SpriteKit executes a series of processes in a particular order. This group of processes is referred to as the animation loop. These processes account for the actions, physics properties, and constraints that you have added to your scene.

If, for whatever reason, you need to run custom code in between any of these processes, you can either override some specific methods in your SKScene subclass or specify a delegate that conforms to the SKSceneDelegate protocol. Note that, if you assign a delegate to your scene, the class' implementations of the following methods are not invoked.

The animation loop processes are as follows:

Step 1

The scene calls its update(_:) method. This method has a single NSTimeInterval parameter, which gives you the current system time. This time interval can be useful as it allows you to calculate the time it took for your previous frame to render.

If the value greater than 1/60th of a second, your game is not running at the smooth 60 frames per second (FPS) that SpriteKit aims for. This means that you may need to change some aspects of your scene (for example, particles, number of nodes) to reduce its complexity.

Step 2

The scene executes and calculates the actions you have added to your nodes and positions them accordingly.

Step 3

The scene calls its didEvaluateActions() method. This is where you can perform any custom logic before SpriteKit continues with the animation loop.

Step 4

The scene performs its physics simulations and changes your scene accordingly.

Step 5

The scene calls its didSimulatePhysics() method, which you can override with the didEvaluateActions() method.

Step 6

The scene applies the constraints that you have added to your nodes.

Step 7

The scene calls its didApplyConstraints() method, which is available for you to override.

Step 8

The scene calls its didFinishUpdate() method, which you can also override. This is the final method you can change your scene in before its appearance for that frame is finalized.

Step 9

Finally, the scene renders its contents and updates its containing SKView accordingly.

It is important to note that, if you use a SKSceneDelegate object rather than a custom subclass, each method gains an extra parameter and changes its name slightly. The extra parameter is an SKScene object, which allows you to determine which scene the method is being run in relation to. The methods defined by the SKSceneDelegate protocol are named as follows:

  • update(_:forScene:)
  • didEvaluateActionsForScene(_:)
  • didSimulatePhysicsForScene(_:)
  • didApplyConstraintsForScene(_:)
  • didFinishUpdateForScene(_:)

Even if you don't use these methods to make any changes to your scene, they can still be very useful for debugging. If your game consistently lags and the frame rate drops at a particular moment in your game, you could override any combination of the above methods and find the time interval between each one being called. This allows you to accurately find whether it is specifically your actions, physics, constraints, or graphics that are too complex for your game to run at 60 FPS.

4. Performance Best Practices

Batch Drawing

When rendering your scene, SpriteKit, by default, runs through the nodes in your scene's children array and draws them on to the screen in the same order as they are in the array. This process is also repeated and looped for any child nodes a particular node might have.

Enumerating through child nodes individually means that SpriteKit executes a draw call for each node. While for simple scenes this method of rendering does not significantly impact performance, as your scene gains more nodes this process becomes very inefficient.

To make rendering more efficient, you can organize the nodes in your scene into distinct layers. This is done through the zPosition property of the SKNode class. The higher a node's zPosition is, the "closer" it is to the screen, which means that it is rendered on top of other nodes in your scene. Likewise, the node with the lowest zPosition in a scene appears at the very "back" and can be overlapped by any other node.

After organizing nodes into layers, you can set an SKView object's ignoreSiblingOrder property to true. This results in SpriteKit using the zPosition values to render a scene rather than the order of the children array. This process is far more efficient as any nodes with the same zPosition are batched together into a single draw call rather than having one for each node.

It is important to note that the zPosition value of a node can be negative if needed. The nodes in your scene are still rendering in order of increasing zPosition.

Avoid Custom Animations

Both the SKAction and SKConstraint classes contain a large number of rules you can add to a scene to create animations. Being part of the SpriteKit framework, they are optimized as much as they possibly can be and also fit in perfectly with SpriteKit's animation loop.

The wide array of actions and constraints provided to you allow for almost any possible animation you could want. For these reasons, it is recommended that you always utilize actions and constraints in your scenes to create animations rather than performing any custom logic elsewhere in your code.

In some cases, especially if you are needing to animate a reasonably large group of nodes, physics force fields may also even be able to produce the result you want. Force fields are even more efficient as they are calculated alongside the rest of SpriteKit's physics simulations.

Bit Masks

Your scenes can be optimized even more by using only the appropriate bit masks for nodes in your scene. In addition to being crucial for physics collision detection, bit masks also determine how regular physics simulations and lighting affect the nodes in a scene.

For any pair of nodes in a scene, regardless of whether or not they will ever collide, SpriteKit monitors where they are relative to each other. This means that, if left with the default masks with all bits enabled, SpriteKit is keeping track of where each node is in your scene compared to every other node. You can greatly simplify SpriteKit's physics simulations by defining appropriate bit masks so that only the relationships between nodes that can potentially collide are tracked.

Likewise, a light in SpriteKit only affects a node if the logical AND of their category bit masks is a non-zero value. By editing these categories, so that only the most important nodes in your scene are affected by a particular light, you can greatly reduce the complexity of a scene.

Conclusion

You should now know how you can further optimize your SpriteKit games by using more advanced techniques, such as texture atlases, batch drawing, and optimized bit masks. You should also be comfortable with saving and loading scenes to give your players a better overall experience.

Throughout this series, we have looked through a lot of the features and functionality of the SpriteKit framework in iOS, tvOS, and OS X. There are even more advanced topics beyond the scope of this series, such custom OpenGL ES and Metal shaders as well as physics fields and joints.

If you want to learn more about these topics, I recommend starting with the SpriteKit Framework Reference and read up on the relevant classes.

As always, be sure to leave your comments and feedback in the comments below.

Tags:

Comments

Related Articles