Optimizing SKLabelNode to reduce performance issues - ios

Creating an RPG with SpriteKit has me creating SKLabelNode's for displaying all game text. I'm suffering performance issues (FPS dropping & the game lagging) every time my multi-line text box begins auto typing inputted text which uses SKLabelNode's. I also see performance hits when "summoning" UI elements, such as character stat panels (with lots of SKLabelNode's for skill levels, skills names, etc.).
Veteran developers say pre-loading data always helps, thus I did the following (which has reduced the performance hit but hasn't eliminated it & the text box still lags):
// Preloaded model label (private property that is initialized in the class init method).
_modelLabel = [[SKLabelNode alloc] initWithFontNamed:_fontName];
_modelLabel.fontColor = _fontColor;
_modelLabel.fontSize = _fontSize;
_modelLabel.text = #"T";
// Constantly created label.
SKLabelNode *rowText = [[SKLabelNode alloc] initWithFontNamed:self.modelLabel.fontName];
rowText.fontColor = self.modelLabel.fontColor;
rowText.fontSize = self.modelLabel.fontSize;
rowText.text = row;
...
[self addChild:rowText];
I would be grateful for getting some more optimizations tricks when using SKLabelNode's.
Here are the tips I have collected thus far:
Preloading is only needed when using a font that is not available via iOS.
To get the font to actually load you need to also set the text for the preload to work.

Try to preload SKLabelNode with all chars you need. Maybe, SpriteKit renders only required glyphs into texture.
// Preloaded model label (private property that is initialized in the class init method).
_modelLabel = [[SKLabelNode alloc] initWithFontNamed:_fontName];
_modelLabel.fontColor = _fontColor;
_modelLabel.fontSize = _fontSize;
_modelLabel.text = #"ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789-+=_()";
And then add your model into scene with minimal alpha (but no zero, because if node has 0 alpha, it will not drawn); Try to keep that node in scene
_modelLabel.alpha = CGFLOAT_MIN;
[self addChild:_modelLabel];
If not helps:
Organize nodes by aligning their zPosition (aka z-index). You should disable ignoresSiblingOrder for having control over zPosition.
Make your all SKLabelNode be drawn at one z level.
Switch to bitmap fonts. Writing custom bitmap font renderer is super easy. The only thing you should remember - Make all glyphs be in only texture atlas, so you can draw your text in one draw call
Use native labels. I'm not sure, how much does it help. If you're having heavy FPS drops (~5..10FPS) This may help you

It seems to me you are creating new labels every time you need text. If I were you only create the label once, and just alter the text when needed as well as just hide the lines you are not using.
Another thing you can do is fill the text in on your label one time, then assign it to an SKCropNode. Then you can use the SKCropNode to bring the font characters back in, which in turn should give you a nicer scrolling effect since you are not doing it at one character at a time.

Related

Fitting multi-line text into a dynamically size-changing node

A multiline auto typing text box class (which uses an SKNode as the parent) is created using basically 2 elements:
an SKSpriteNode that acts as text box frame & background image/texture holder.
an NSMutableArray containing a set limited amount (rows) of NSStrings that each have a set character length.
After modifying this text box class so that it can be initialized with any frame width & height, I realized I didn't program the NSMutableArray to automatically change its content in a such way that it nicely fits within the background node (with a bit of padding involved as well). So here I am wondering how to do that since NSString's can only return the character count and not the width & height of each string in points (points could have maybe helped me create character constraints in some way).
Right now, the NSMutableArray uses a hardcoded maximum character count per NSString & a maximum row count for the entire array (it's 5 rows right now and when that limit is reached, a new "page"/array is created). This forces me to manually re-adjust these parameters every time I change the background node frame size which defeats the purpose of the class allowing the background frame to change.
Thing is, I'm trying to solve this in such a way that when I post this class on github, I want the solution to take into consideration any fontName & fontSize.
What are my options for solving this problem?
I've done something similar to this. It doesn't work 100% as to what you want, but should be similar enough. It uses a root node and from there, it will build multi-line text using an array of NSString which will in turn be used to build the SKLabelNode.
I'll outline what I did. I should also say I only run this when new text is set. In other words, I do not incur the penalty of deriving the information every frame. Only once.
The generalized steps are:
You will iterate over each character in the text string. Note I do this because my code supports word wrapping as well as other alignment capabilities. So for me, I want that level of control. As this is being done only upon creation, I'm fine with the overhead. If you don't want to word wrap you could always just create an array of words and work from there.
As you iterate over each character, you'll be generating an array of lines. Where each line in the array is a line that will fit in your frame. For now let's not worry about vertical constraints. So here we are primarily worried about width. For the current line, each character you are iterating over will get added to the current line. For this potential line string, you will use NSString's sizeWithAttributes, which is configured for your font. For example in my code it is an NSDictionary which contains: NSFontAttributeName : [UIFont fontWithName:self.fontName size:self.size]. This will be used to check the width, if that width exceeds the frame width, you are overrunning the line.
So the code may look something like:
size = [line sizeWithAttributes:attributes];
if (size.width > maxTextWidth) {
needNewline = YES;
}
If you have overrun a line, you need to determine if you are word wrapping. If you are, you can just add the current line (minus one character) to the lines array. If not you have prune off the last word in the current line and then add that to the array of lines.
The tricky parts are dealing with whitespace and handling non-word wrapped overflow. I have not addressed whitespace but you need to consider this very much in your code. Additionally, you also do want to factor in leading pixels, etc.
Once you have your array of lines, you can then create your children SKLabelNodes. I add them to the root, which allows me to move the group anywhere it needs to be.
The real key here is the lines array generation.

Add custom "number" images to SKLabelNode in as a Score in SpriteKit with Swift

I basically want to add custom font numbers (with images) in Singles and add them for my score in my game with SpriteKit.
This is the font image style
They are numbers from 0 to 9
I have been trying to figure this out for a while.
I'm not sure if you can add it to SKLabelNode or if you would have to make it in a class.
SKLabelNode would not allow for bitmap fonts. If your custom font was TTF, you would be able to use the custom font with SKLabelNode. An explanation how is here: SKlabelNode custom font
In your case, since you want to use bitmap fonts, you will need to create a class to do it do this. You can either subclass an SKNode or not. But you would want to have one SKNode act as the "root" and then have each digit for your number be an SKSpriteNode (these would be children nodes of the root node representing the number).
One thing you will want to determine is how you want to handle alignment for the number (both horizontally and vertically).
I have used my own custom class to draw numbers before and typically have it configurable to be adjustable horizontally (left justified, right justified, and centered). But out of laziness, I usually always center vertically.
This may sound complicated, but is easy to do. You would just need to have
Way to assign texture atlas of bitmaps for each digit. It is easier
if you have them in progression of 0-9.
Method to set/get number value
Method which can convert
number into individual digits. You'll need to this to compute which
digit maps to which character.
Method to compute width of number. If
your fonts are monospaced, then this is easy. If they are not, then
you'll need to compute width based on digit width and letter-spacing. For monospaced font you still may want to factor in letter-spacing.
Method to compute height of number. This should be easy as 0-9 should
all be the same height.
Method to remove existing SKSpriteNode and
add each new digit. Each new digit being offset by the proper amount
based on alignment.
Your custom class should, upon the number value being set, build the correct SKSpriteNodes to represent the number.
Some updated info for the last question regarding to how one would use an array with the SKTexture. This code is not tested for compilation and is meant to just give you a better idea.
// Example of storing digits in an array. Assumes you are putting this in an atlas and your
// sub-texture names are 0, 1, etc. If not you'll have to adjust the code accordingly
var numTextures = [SKTexture]()
let numberAtlas = SKTextureAtlas(named: "NumberAtlas")
for i in 0 ..< 10 {
numTextures.append(numberAtlas.textureNamed("\(i)"))
}
// To get the texture for a digit
func textureForDigit(digit:Int) -> SKTexture {
return numTextures[digit]
}

Tiled map generation (removing & adding columns) When?

I'm making an endless runner like game for iOS, right now I'm at the map generation.
It's working when the map is moving in a fixed speed, it's endless and it's random. I'm using an array to hold the columns of tiles and I remove the first object of the array if it's out of screen, and then I generate a new column at the end of it.
My problem is when I want to increase the speed of the map, the map generation starts to slow and after a while it becomes slower the the map itself, I can't figure it out why.
I created a helper SKSpriteNode which starts at (40,0) and I'm changing its position at the same time as the maps position, and when it's less than or equal to zero I remove a column and generate an other.
The starting mapNode.mapSpeed is 2.0f and I would like to increase it with various values over time.
self.helperNode.position = CGPointMake(self.helperNode.position.x+self.mapNode.mapSpeed, self.helperNode.position.y);
self.mapNode.position = CGPointMake(self.mapNode.position.x-self.mapNode.mapSpeed, self.mapNode.position.y);
if (self.helperNode.position.x <= 0.0f) {
[self.mapNode generateNextColumn];
self.helperNode.position = CGPointMake(40.0f, 0.0f);
NSArray *array = self.mapNode.map[self.mapNode.rowCount];
SKSpriteNode *row = [self.mapNode buildNextRowFromArray:array];
row.position = CGPointMake((self.mapNode.rowCount*40)-20, 0);
[self.mapNode addChild:row];
NSString *string = [NSString stringWithFormat:#"row%li",self.mapNode.rowCount-(long)self.mapNode.children.count];
SKNode *removableRow = [self.mapNode childNodeWithName:string];
[removableRow removeFromParent];
}
Frequently calling "create/remove node" is rather inefficient. More so if you have to get each row's nodes by string, that's highly inefficient.
You have a fixed number of active sprites (tiles) at any given time, so you should preallocate does sprites and put them in an array or dictionary.
Every column of sprites that has scrolled past the screen border simply get to update their positions (and textures) so they become the next column to appear on the screen.

Animate NSLayoutManager drawing

I have an NSLayoutManager which is drawing text using the following code:
[[self textLayoutManager] drawGlyphsForGlyphRange: NSMakeRange(0, [[self text] length])
atPoint: textFrame.origin];
in my view's -drawRect:. This works wonderfully, but what I'd really like to be able to do is animate the text in, character by character, as if it were being typed.
I've tried to append characters to a "visible string" variable, then call -[self setNeedsDisplay], but when dealing with text over approximately 20 characters, it begins to lag, as it redraws all of the text every time.
Regression: How can I animate NSLayoutManager's -drawGlyphsForGlyphRange:atPoint:?
(Disclaimer: I don't have a terrible lot of experience with the new APIs, so this is mostly coming from previous experience with text rendering.)
Your major slowdown is going to come from full-on changing the text the layout manager is working with. Even if you're just appending text, replacing the text it's using is going to cause it to throw out all its layout calculations - spacing, needed glyphs, actually reading those glyphs into RAM, applying attributes, etc. - and start over, which gets very expensive very quickly. In terms of actual NSLayoutManager, this is "invalidating the layout".
I see a couple of potential solutions off the top of my head. You could subclass NSLayoutManager ("You can create a subclass of NSLayoutManager to handle additional text attributes, whether inherent or not.") and override the showCGGlyphs:positions:count:font:matrix:attributes:inContext: to progressively ignore certain glyphs (thereby leaving the original text it's using intact). Another approach would be to emulate exactly what happens when you input text in a native container - use a mutable text storage, and append the desired text character-by-character, so that text calculation is done iteratively.
If those alone still don't have great performance, consider using those techniques in tandem with some of the native text views; although this is less true now than in past SDKs, a non-editable UITextView or a UILabel (for mutable and immutable text, respectively) contain far more optimizations than our mere mortal minds could comprehend.

Reusing CCLabelTTF

I want to use the very same label in all my CCMenuItems. If I create the same CCLabelTTF once, then I can't add it to more than one CCMenuItem because it will give runtime error about label already added. But creating the same label many times also not effective, if the label is same. How to optimally solve this problem?
you cannot use the same label more than once, the label is not only what you see, for example it also contain a position, so you cannot place the same item in 2 different points.
What's the problem on creating more than one? if you have a LOT of ttf labels that change text often you can consider using bitmap fonts. they are rendered faster

Resources