Raid Encounters 2 Dev Log //Optimization

Mike Twomey
7 min readMar 17, 2018

--

For about a month now I’ve been hard at work juggling art and coding duties on Raid Encounters 2. Over this time, I’ve learned a lot about Swift, but much more about SpirteKit. As expected, I’ve started running into little things I was feeling ready for, so it hasn’t been too big of a deal.

The first issue has been how SpriteKit will randomly drop the FPS to 40 from a smooth 60. This typically happens much more when hooked into Xcode for debugging, but it also occurs fairly frequently in the event of a slide-down notification from something such as a text message or another app’s alert. This can be fixed by swiping out of the game and back into it, but it’s a little worrying. I haven’t had much time to test off the debugger, so I’m not overly worried about it now. On the plus side, it pushed me to start doing some optimizing a little early in the development cycle.

SpriteKit Optimizations So Far

Too Much Alpha Variance

My first optimization was eliminating fade transitions, colorizations, and assorted alpha values for sprites of the same texture. When SpriteKit makes a draw call on the texture of a sprite, it does so in batch based on the zPosition. Even if the sprites have the same texture and zPosition, mainly changing these two properties between them all will result in that many more draw calls. Initially, I had monster sprites spawning in from alpha: 0 to alpha: 1 over TimeInterval(0.5) in 1 second intervals. Having them scale in to 1 from 0 instead eliminated the extra draw call or two during hectic environments, which is not much on an iPhone X but undoubtedly gives a performance boost of some sort. I have some other colorization effects on SKLabelNodes which I’m thinking of re-tooling because of the leaks caused (rapid colorization effect that fades between 4 colors).

Pool and Re-use Everything

While not so much of an optimization rather than a better coding practice, I created pools for all text popups and monster sprites. The game has several things going on in the scene at once, and each time a qualifying event occurred (such as an XP gain, damage dealt, or health received indication) a new SKLabelNode was created and then removed. I haven’t taken a second glance after the creation of the pool, but SpriteKit appeared to have trouble removing SKLabelNodes from memory even after a .removeFromParent(). I was able to create an array of all of my SKLabelNodes and draw from them as needed. I created the size of the pool based on thinking how much carnage could be going on and they not be noticed if they have to be pulled for a new event before their current animation event was complete.

Creating the monster pool was a little different and required creating two pools: one for live monsters, and one for dead monsters. It was different mainly because of the size of the pool and how I drew new monsters out. Looking back, I suppose I could have always grabbed at an array index of 0, but instead it cycles through the pool as well. When elite monsters are implemented (random monsters which have increased attributes) it will make the shuffling a little more useful.

While not too much of a concern, creating the monster pool has stopped the stuttering between each new monster load. I was able to properly remove them from memory when required, but overall it’s a better solution. Making use of SpriteKit’s .copy() function allows all monsters to be painlessly created at runtime so there are essentially no more nodes created once gameplay begins except the Raid Boss, for now.

Don’t Accidentally Recreate Textures

When trying to limit memory usage, I noticed that new sprites were taking up additional amounts of memory based on their texture atlases. I was able to create a Singleton “cache” for all textures used as a sharedInstance. Right now it just houses monsters which are all on separate atlases during development, but memory usage has gone down substantially.

The first implementation I tried relied on the native .preload() function, but this caused completely random crashes at boot time, so the preloading was scrapped in favor of just the let references, which have proven to be sufficient to this point.

Really, Really Remove Emitters

Again, maybe not an optimization, but rather a better solution to handle SKEmitterNodes. If you’ve played Raid Encounters, you’ll know that effects are a huge part of the wild, visual spectacle, and I want a lot of them in Raid Encouners 2. Sometimes SKEmitterNodes have problems removing themselves from the scene, even after a .removeFromParent() when it gets really chaotic and multiple instances must be on screen at the same time. These maybe something I pool and reposition if I can get them to fire on command which should be possible based on the nature of how the particles are handled, but for now I found a technique from Apple’s examples which drop the particleBirthRate to 0 before removal. Ahead of time, the numParticlesToEmit is already set in the emitter file. The combination of these two seemed to resolve most of the ghost emitters from remaining in memory, but creating a single emitter for each effect instead of creating new ones from each instance of an event is definitely the next step forward in that regard if the particles will react how I want them to and fire on command, perhaps with .resetSimulation() ?

Asynchronous Dispatch

I’ve just started to get my hands dirty with asynchronous calls, and started with the in-game notification popups. While these won’t happen terribly frequently, I did notice them getting janky when multiple popups occurred during testing. The current notification would be completely erased by the new one, and the action of the first one would still be in effect so the new one would also be gone prematurely. Since we live in a multithreaded world, and Swift is ready for it, I was able to look to see if there’s a current notification up and if so to not call the new notification. Per my understanding, waiting for the previous action to complete while trying to run the same action again would result in a thread freeze, so with a dispatch queue I’m able to have the new notification fire at a different time. There may be a better way to do this, but it seemed like it’d be more efficient this way and also let me dip my toes in the pool of asynchronous functions and delays.

Efficient Variable Scoping

Coming from a JavaScript background, scoping wasn’t a terribly new concept for me, but I’ve always been told global variables are bad. Right now, there are a LOT of global variables, most of which probably don’t need to be.

It seemed a little cumbersome at first using if let within functions to create and use variables that were created in the Scene Editor, but I haven’t seen much of a difference yet although I imagine there should be some performance increases depending on how Xcode compiles.

For example, as of now I declare all the objects I created in my scene at the global scope and then assigned in the level creation process. From there, these elements can be read and manipulated from any function.

var menuButton: SKSpriteNode()func loadLevel() {
menuButton = childNode(withName: “menuButton”) as! SKSpriteNode
}
func doThings() {
menuButton.run(myAction)
}

I may be better off biting the bullet and using if let

func doThings() {
if let menuButton = childNode(withName: “menuButton”) as? SKSpriteNode {
menuButton.run(myAction)
}
}

Now that I look at it all, it may be more efficient after all, it’s more of a matter of if the difference is unreasonably negligible to the end user from the variable already being initialized and assigned, to being grabbed, casted, and assigned as needed. Initial load times aren’t major right now, but this could be because of my hardware.

SpriteKit “Bugs”

Being new at a language and SDK, I try to give the benefit of the doubt when I think there’s a bug or something not working as it should. My first experience was using the .copy() function of a node. This function copies all children, their properties, and the node itself into a new object. When I was testing different physics properties of my monsters’ SKPhysicsBody, I noticed that the copies were not behaving as the original node. It turns out that the .copy() function, when it comes to an SKPhysicsBody, will only grab that the physics body exists, and its size. It may get other things, but it definitely does not grab restitution, mass, and damping properties. I did get around this by manually settings them after copy based on the monster being copied:

let monsterCopy = generatedMonster.copy() as! SKNode
monsterCopy.physicsBody?.mass = (generatedMonster.physicsBody?.mass)!

Strange.

Oh, and if you were wondering why I’m using SKNode instead of SKSpriteNode, it’s because of the way the monsters house their health bars and level indicators. This allows me to keep everything aligned and rotated properly even in the event that the monster turns around (changing its xScale to negative) or spins and shrinks to nothing (death animation). Being able to directly affect the monster sprite and not its “meta” components has been a much better experience.

As for the .preload() function random crashing because of bad memory access, and drops to 40FPS until the scene is “reloaded,” I can’t say I know enough about Xcode to claim they’re bugs, but the 40FPS issue is at the forefront now and I’m learning how to use Instruments to find out the problem. Sometimes a physics spike and sometimes a constraint spike seem to make it happen, but it never fixes itself until any affected objects are removed from the parent node. I’ve also not yet been able to correlate any specific actions (besides top notifications from the phone) being the culprit. Sometimes it works flawlessly, then other times not so much, but it can also randomly happen on the level select screen which has no physicsWorld or constraints in place.

--

--