SpriteKit: CPU gains from caching enumerateChildNodesWithName
Those of you who follow me know that I’m working on a game that will have ~200 nodes on the screen updating every frame. Because of that requirement, I’m constantly looking at how I can incrementally improve performance. Slowly but surely, I’m making this game a well oiled machine. Today, I stumbled on a significant slow down, and the resulting fix that shaved off 16% CPU usage: caching enumerateChildNodesWithName
.
This discovery came from reading WWDC 2014 transcripts while discussing said function.
This is very fast, so you can do it often but we do recommend your cache results if it becomes a performance problem.
If it warranted a mention in Apple’s discussion, it deserved a closer look. So, I set up a test with 105 nodes running in the update loop. Here are the results:
- Calling enumerateChildNodesWithName each frame: 44% CPU
- Caching update callbacks in an array: 28% CPU
Huge gains, and the best part that the caching is relatively painless to implement.
Implementation
First, we’ll start with an array to hold our callback functions.
var updateListeners : Array<((CFTimeInterval) -> ())> = []
Here is an example of what one of those callbacks would look like:
func updateWithTime(currentTime:NSTimeInterval) { // do something }
With that in place, all we have to do is cache the callback functions on the first run through of enumerateChildNodesWithName<strong>
in the Scenes update loop.
Now, let’s say one of our characters dies. We don’t want the update function to keep running on that character. We’ll have to clear the cache by emptying the array. Then, it will automatically rebuild on the next update loop.
self.updateListeners = []
I’ve consolidated all of this into a small class that I call from my scene object in case it gets more complicated down the road. You can see my full code here:
Notes & Concerns
Each time you clear the cache, it fully rebuilds. If you’re pushing CPU limits already, this could cause some delayed frames. Two solutions to improve this:
- Add a removeUpdateListener() function, and a key/value store, so that you can remove a specific item on demand.
- Or, break apart your objects into smaller cache groups with the goal of minimizing the amount of objects removed from the cache at any given time.
Consider expanding updateService
to dynamically control what would be in the cache. My example is hard coded to Characters
and Heraldry
, but the situation could be different each level.
Also, this can be a useful way to begin separating out updates for different objects by different time intervals if everything doesn’t have to tick 60 times per second.
You should keep a cache of items so you can remove them via key/value this is what I do with over 2k objects and only use about 2% cpu resources while swapping in/out all the time. Granted I use sprite sheets so the reload of images, sprites etc. is no drastic hit because the actual image is already loaded into openGL. I think you will find it will make your life easier by keeping a cache and if you are like me I keep a json cache so i can add stuff like "destroyed" : true, "currentstate" : 1, etc. and so forth. It allows me to expand the object out in real time without having to reload anything.
Good stuff. Do you have 2k SKSpriteNodes actively computing each frame (i.e: performing some sort of math in the update call), and still only using 2% CPU? On iPad Air 1, with 200 active nodes, I can't come close to 2% even if I hard code the calls.