Just Say No to SKLabelNode
I am currently in the process of cleaning up some elements in Raid Encounters 2 to get a better bearing of where I am right now since most of the core logic and gameplay is complete (all that’s left is more content). At this stage, I’m looking at several factors to understand how performance will be across a large range of devices.
As anyone who’s programmed with SpriteKit or any other game engine knows, draw calls are extremely important when optimizing.
My first run-in with SKLabelNode involved my entire popup system, including XP earned, damage dealt, healing received, notifications, and more. Text is probably close to 30% of the entire experience, which is a lot for a non-text driven or story-driven game.
The nature of the popups are to appear, and then disappear after a set interval (like how damage amounts pop up over enemies in World of Warcraft or Destiny). I first realized that SKLabelNodes did not like being removed from their parents, even with no strong reference cycles. I fixed this by creating a pool of all the different types I’d need and recycling them. This immediately stopped the gaping memory leak.
Further on into my development cycle, I noticed that my draw calls were getting a little too extreme in heavy action. After a quick test, I realized that each SKLabelNode required its own Draw call. And this is with ignoresSiblingOrder and consistent zPositions.
Whoa.
In the original Raid Encounters, I had a hybrid setup of actual fonts and bitmap fonts. I initially figured SpriteKit would handle text a little bit more efficiently, but that wasn’t the case. Fortunately I found bmGlyph, which allows you to create bitmap fonts and by using their Swift code, you can properly parse the XML and create the fonts.
A bitmap font is basically a texture atlas of all of the characters, and the accompanying XML file lays out where each of those characters are when composing each element into a single phrase. In SpriteKit this works well since it will render all sprites from the same texture atlas on the same draw call at the same time. Huge savings.
There are some very minor limitations with bmGlyph, most of which I’ve worked out.
The first “hack” was applying color to BMGlyphLabels via other methods besides the provided setGlyphColor function. If you use a Colorize SKAction, it does not take. Fortunately, shaders do.
To implement it, I defined very specific SKColors in one of my classes, and modified the setGlyphColor function to look for the specific colors and apply shaders to cycle colors or add effects.
In the setGlyphColor function of BMGlyphLabel.swift, just add:
if color == Colors().yourSpecificColor { (letter as! SKSpriteNode).shader = yourShader} else { (letter as! SKSpriteNode).shader = nil}
One issue down!
(If you’re wondering about shaders, that is going to be another story altogether as I try to adapt the SKShader implementation of fragment shaders.)
The next issue was how the BMGlyphLabels are implemented when it comes to registering touches. Typically, you would detect a touch and base the next action from the name of the node. Even when defining the BMGlyphLabel with a name property, it would not register on the touch delegate.
There was an issue raised on the Github for the project, but was closed as not an issue. There was a workaround given involving detecting touch coordinates, which was not ideal and would create more hassle than is necessary in the wonderful world of programming.
To fix this, I considered adding a name property to the initializer of the BMGlyphLabel, but opted for something more simple.
In the updateLabel function of BMGlyphLabel.swift, I added
letter.name = self.text
I put this in the if statement that looks to re-use letters, and also after the statement (which appears to occur as a fall-through on a freshly created label). This ensures that each letter in the BMGlyphLabel receives that name (which in my implementation is the text of the BMGlyphLabel). This means I can once again register touches based off the name, which will always be the text of the entire label.
After all is said and done, I’ve almost completely eliminated the SKLabelNode from my project and have probably dropped my draw calls by almost 75%, so all-in-all a pretty good deal.