The one year game develpoment duel
Jan

5

2015

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.

Best Practices for Building SpriteKit Games

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.

2 of you did not hold your tongue!
  1. 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.

  2. 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.


Speak Freely


Thank you

Your comment will be published once it has been approved.

Click here to see the pull request you generated.