Start here to learn how to make your game faster
SpriteKit is an extraordinarily fast 2D framework, backed by Apple’s own Metal library for raw access to the GPU.
But as your games grow you might occasionally find your frame rates start to drop, and with devices such as the iPad Pro having a 120Hz display you need to work hard to keep your frame updates inside the 8 milliseconds you’re given.
If you’re experiencing low frame rates, choppy animations, or similar performance problems, I’ve put together 15 optimization tips you can work through that will help identify and resolve problems. Even better, where appropriate you’ll find code attached so you can try it yourself!
SPONSORED Debug 10x faster with Proxyman. Your ultimate tool to capture HTTPs requests/ responses, natively built for iPhone and macOS. You’d be surprised how much you can learn about any system by watching what it does over the network.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Texture atlases place multiple separate assets in the same finished graphic, so that they all get loaded at once. Drawing then happens by effectively rendering only part of an asset at a time, which allows SpriteKit to keep one texture active and merely move the window it’s drawing from.
This often leads to major performance improvements, because changing state – unloading one texture and loading another during rendering – is expensive. However, it’s common to see people add all their images to a single atlas, which actually works against them: Xcode will build your assets into atlases based on its own fitting algorithm, and because it has no idea where assets are actually used you might find entirely unrelated sprites appear in the same atlas.
In practice, what this means is that you might have one sprite from level 2, another from level 8, and another from level 52 all on the same texture atlas. That means Xcode must load two irrelevant sprites into memory just to get access to the one it actually needs to draw, which is extremely inefficient.
So, create multiple texture atlases to fit your actual content: all the animations for a player in one atlas, for example, and all the sprites for a particular world in another.
It shouldn’t surprise you when I say that there is a performance cost to loading textures from your app bundle. It might be small if the image is small, but if you try and load a full-screen background picture it might just be enough to push you over your time budget – which means dropped frames.
To fix this, you should look to preload textures in the background, effectively preheating the cache so when you need them they are available immediately. As a result, there’s much less chance of you dropping frames.
To see how this works, it’s important to understand that SKTexture
works like UIImage
: it doesn’t actually load the data until it’s needed. So, this kind of code is almost instant even for extremely large images:
let texture = SKTexture(imageNamed: "Thrash")
However, as soon as you assign that texture to a sprite node in your game scene it needs to be loaded so it can be drawn. Ideally you want that load to happen before the scene is shown – in a loading screen, perhaps – so you avoid frame problems, so you should preload it like this:
texture.preload {
print("The texture is ready!")
}
You can also load texture atlases this way, which is particularly useful if you have one texture atlas per game segment.
When you’re making simple SpriteKit games it makes sense to produce artwork in whatever way is easier for you to understand.
In practice, that usually means making the player’s spaceship point upwards by default, because that’s what many people consider it to be its natural orientation. However, SpriteKit thinks differently: for SpriteKit 90 degrees (3 o’clock) is the natural orientation of things, so you’ll often find yourself adding CGFloat.pi / 2
radians to your calculations in order to make your idea of “straight” match SpriteKit’s.
When performance becomes more critical, it’s time to rethink. Yes, you might think that straight up is the default orientation for objects, but SpriteKit doesn’t, and constantly adding extra arithmetic to convert between the two is an unnecessary overhead.
So: make sure your assets are oriented to the right and are a sensible size for your game scene.
Despite all the calculations that need to take place, drawing pixels to the screen – rendering out your finished product – is still often one of the slowest parts of making a game. This is because it’s complicated: most sprites have irregular shapes and alpha transparency, we usually have multiple layers in your scene, and often we have complex effects that bring things to life.
A lot of this is unavoidable, but there is one easy change you can often make: if you’re drawing a sprite that has no alpha transparency at all (i.e., it’s a solid shape, such as a background image), then you can tell SpriteKit to render it without alpha blending, like this:
yourSprite.blendMode = .replace
In practice, this means drawing happens simply by copying the sprite’s pixels over whatever is already there – SpriteKit doesn’t need to read the existing color value, then blend it with the new color value.
SpriteKit does a good job of automatically culling our game scenes so that things that are off screen don’t get drawn. But – and this is important! – they still get factored into physics calculations, and even not drawing something still requires SpriteKit to constantly check whether something is visible or not.
So, if something has genuinely moved off your screen and you don’t need it back in the near future, call removeFromParent()
on it. You can add it back later if needed, but in the meantime you’re saving SpriteKit a bunch of extra calculations that just aren’t needed.
Both of these node types let us customize the way other nodes are drawn, either by clipping their drawing or by applying Core Image filters. However, both work using an offscreen render buffer: the contents of the node need to be rendered into a private framebuffer, then copied back into the main scene. This extra pass is slow, so both types should be used sparingly.
While there is no way of reducing the cost of crop nodes (other than avoiding them where possible!), you can reduce the cost of your effect nodes by instructing SpriteKit to cache the resulting framebuffer:
effectNode.shouldRasterize = true
Warning: Rasterizing your effect nodes is a bad idea if they are constantly changing, because you’re constantly storing extra data in RAM then tossing it away. On the other hand, if your effect nodes are changing only once every few seconds or less, rasterizing is a good idea.
Particle systems make for relatively cheap, easy, and impressive effects, but it’s also easy to get carried away – particularly because you can click your way around Xcode’s built in editor for an hour, without feeling the actual performance impact.
The problem is that Xcode’s particle editor doesn’t model real-world game environments – your particular might work great in the test harness, but when you have of them appearing at once it might drag down your frame rate.
A particular culprit is birth rate, which is how fast SpriteKit creates particles. If you’re using a value over 500 it might look impressive, but it’s computationally expensive – try going for half that value and using a larger scale instead, so individual particles take up more space.
SKView
has a Boolean property called ignoresSiblingOrder
, which, when set to false, has serious performance implications.
Sibling drawing order refers to the way SpriteKit renders nodes in your game scene. When it’s taken into account, SpriteKit renders nodes according to both their Z position and their position as children of their parent.
This is slow and usually unnecessary because we can just use the Z position to control draw depth and eliminate the extra ordering.
Annoyingly, ignoresSiblingOrder
is set to false by default, which means you get the slow drawing behavior. If you can, set it to true by adding this to whatever view controller your scenes are rendered inside:
yourSKView.ignoresSiblingOrder = true
Now make sure you use the zPosition
property of your nodes to control where they are drawn.
Although you can create some interesting effects using textures, crops, and effect nodes, you can do significantly more using fragment shaders – and they are much faster, too.
Fragment shaders are tiny programs written in a dedicated language called GLSL, and they are run on every pixel inside your nodes. They let you create incredible effects such as water ripples, static noise, embossing, interlacing, and more, and modern computers are designed to be extraordinarily efficient at processing them – a modern GPU might have 2000-4000 dedicated shader processors that run simultaneously.
If you want to get a library of advanced effects using shaders, all written by me and tested with SpriteKit, I have just the GitHub repository for you: ShaderKit has 26 different shaders to try out, each of which is comprehensively documented so you can modify them or create your own.
However, for extra performance there are three extra things to do:
SpriteKit gives us a collection of physics bodies we can use to represent our nodes in space: circles, rectangles, composite shapes, and even pixel-perfect collision detection. Each have their uses, but broadly speaking you’re going to choose one of three:
Sometimes – used carefully – you might find that the init(bodies:)
initializer can help you overcome particularly troublesome physics. This lets you build a compound body from several other physics bodies, allowing you to join together three circles and a rectangle for example.
Regardless of what you choose, don’t enable the usesPreciseCollisionDetection
Boolean unless you absolutely need it. By default SpriteKit evaluates physics every frame to see whether things collide, but if you have small, fast-moving bodies it’s possible that a collision might be missed because too much movement happened between two frames.
In this situation, enabling usesPreciseCollisionDetection
helps because it makes SpriteKit use an iterative detection algorithm – rather than move the ball from 100 to 150, for example, it examines the movement between frames to figure out whether a collision happened. This is very slow – use it only when absolutely needed.
Objects that have physics bodies must be evaluated every frame to see how they are moving and what they are colliding with. If you set a physics body’s isDynamic
property to false
it reduces the number of calculates SpriteKit must perform – it will no longer respond to gravity, friction, forces, or impulses you apply to it, and it won’t be moved when another object collides with it.
As a result, it’s worth looking for places where you can use static physics bodies rather than dynamic ones. Static bodies will still act as walls for other things to bounce off, they just won’t themselves move.
SpriteKit physics bodies have three bitmasks:
Separating 2 and 3 is an important performance optimization, and also allows extra functionality. For example, we might say that the player and the power up don’t collide but they do have contact, which means the player won’t bounce backwards when they touch a power up, but we are told about the touch so we can respond to it.
Similarly, we might make a player and a wall collide but not have a contact, which means they player can’t walk through walls but we don’t keep getting callbacks saying they touched a wall.
So, to optimize your physics:
By default, SpriteKit will show you how many nodes are currently on screen – it should be there in the bottom-right corner of your game scene. You’ll also see how many frames per second you have, and you should be achieving 60fps on most iOS devices or 120fps on ProMotion devices.
However, if you have performance problems there’s another useful diagnostic value you can show there: draw count. To enable this, add showsDrawCount = true
to your SKView
configuration code in your view controller, then run the game again.
“Draw count” is the number of drawing passes it takes to render one frame of your game. It’s not possible for SpriteKit to draw everything simultaneously, because there are lots of layers, lots of effects, and more all taking place. So, instead it draws game scenes in multiple passes. As I said above, crop and effect nodes need their own passes because of their private framebuffers, which means they incur performance hits.
Once you enable drawing passes you have another useful value for profiling in your app: if your frame rate is stuttering and you find you have 20 or more drawing passes, see if you can get that number down as a matter of priority.
Tip: One crop node with one sprite inside it will cost you two draw passes.
update()
The update()
method (as well as other frame-cycle events such as didEvaluatelActions()
and didSimulatePhysics()
are called once every 16ms on most devices, or once every 8ms on ProMotion devices. That’s an extremely short amount of time, which means you need to actively limit how much work you’re doing in there.
So, instead of pro-actively checking things, can you re-actively check them? For example, rather than checking whether all enemies are destroyed in update()
, keep an array of active enemies and trigger functionality when you remove the last one. This kind of change removes an expensive branch operation from a time-sensitive method.
Different iOS devices run at dramatically different speeds, so the only way to be sure if your game is fast enough is to test it on the oldest, slowest device supported by your iOS deployment target.
If you deploy to iPad, the oldest device is likely to be an original iPad Air or iPad Air 2; on iPhone it’s likely to be an iPhone 5s or 6. Either way, if your game works smoothly on those old devices you’ll have no problem on newer ones!
This one won’t help your performance, but it will help you organize your game code better. It’s common to see folks dump lots of functionality right into their SKScene
subclass, but it’s really not necessary – scenes have a delegate
property that can handle frame-cycle events automatically if you want to.
To try it out, first make a new class that conforms to SKSceneDelegate
, then create an instance of that in your main view and assign it to the delegate
property. Now move into any of the frame-cycle events you want the delegate to handle:
update()
didEvaluateActions()
didSimulatePhysics()
didApplyConstraints()
didFinishUpdate()
The end result is a smarter, simpler distribution of your code.
I have a whole book on SpriteKit, and it has a unique feature I’ve never seen in other tutorial books: you choose how the games should work.
If you ever read “Choose your own adventure” books when you were younger, you’ll know exactly how it works: each project we make offers you choices that affect the rest of the game, meaning that are hundreds of possible outcomes you can make. To find out more about Dive Into SpriteKit, click here.
I also recommend you watch a video from WWDC14 called Best Practices for Building SpriteKit Games – it's a few years old now, but the principles haven't changed.
SPONSORED Debug 10x faster with Proxyman. Your ultimate tool to capture HTTPs requests/ responses, natively built for iPhone and macOS. You’d be surprised how much you can learn about any system by watching what it does over the network.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.