Apple, naughty Apple. You made SpriteKit and SKLabelNode, but not make multiline labelnode. This is supposed to be expected!! OMG!! UILabel has multiline function, why not SKLabelNode?
Ok, lets not get crazy. Relax I am here to solve your woes.
As normal, I make this simple method to handle multiline SKLabelNode. Before we go on,
I'd like to give you alternatives (because some people already make some solution).
https://github.com/downrightsimple/DSMultilineLabelNode
The above isn't really a labelnode object. What it does is write down all the text with labelnode on a dummy space and capture it as image, and use that image as labels. Basically in your app it becomes SKSpritenode. This maybe is ok if you want a quick implementation. But capturing things into an image has lots of problems - namely memory usage, sizes (for example you need to cater for bigger screens making sure the image is not pixelated at high res, etc, etc).
https://github.com/nickfalk/NORLabelNode
The second method above is probably what you want if you prefer to have SKLabelNode that can use the "\n" (linebreaks). This is OK-ish. But I really don't like manually putting the line breaks. Especially if I change the wording then the line breaks need to be redone again, which SUCKSKKKKSK... but this solution is actually not bad.
OKAY! My method (which is obviously the best and smartest method XD) is to use SKLabelNodes, auto creating them through for loops depending on the length of string you have. You specify the rough character width per line, and this method autowraps it. It's like MAGIC. I know you will like this 10million times. XD
This is how the app will look like when you run it. Those are SKLabelNodes!
Ok now for the code. Read the comments to understand what each line does.
// For the sake of testing, I just use a dummy NSString and hardcode the string I want to display.
// This string can be anything - even a string of paragraph that you retrieve from the web or pdf or whatever.
NSString *tmp = @"This is a long string where you do not need to bother about linebreaks and my method breaks it up into multiple SKLabelNode to roughly fit a customized width. Yep it works."; // long string - just type whatever in here
// parse through the string and put each words into an array.
NSCharacterSet *separators = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSArray *words = [tmp componentsSeparatedByCharactersInSet:separators];
int len = [tmp length];
int width = 20; // specify your own width to fit the device screen
// get the number of labelnode we need.
int totLines = len/width + 1;
int cnt = 0; // used to parse through the words array
// here is the for loop that create all the SKLabelNode that we need to
// display the string.
for (int i=0; i<totlines; i++) {
int lenPerLine = 0;
NSString *lineStr = @"";
while (lenPerLine<width) {
if (cnt>[words count]-1) break; // failsafe - avoid overflow error
lineStr = [NSString stringWithFormat:@"%@ %@", lineStr, words[cnt]];
lenPerLine = [lineStr length];
cnt ++;
// NSLog(@"%@", lineStr);
}
// creation of the SKLabelNode itself
SKLabelNode *_multiLineLabel = [SKLabelNode labelNodeWithFontNamed:@"Oxygen Light"];
_multiLineLabel.text = lineStr;
// name each label node so you can animate it if u wish
// the rest of the code should be self-explanatory
_multiLineLabel.name = [NSString stringWithFormat:@"line%d",i];
_multiLineLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeCenter;
_multiLineLabel.fontSize = 20;
_multiLineLabel.fontColor = [SKColor colorWithRed:1 green:1 blue:1.0 alpha:1.0];
_multiLineLabel.position = CGPointMake(size.width/2, size.height/2+100-20*i);
[self addChild:_multiLineLabel];
}
So that's all. Easy right? Feel free to improve it more. This method is ideal for games in SpriteKits.
For example when you want to make credits info, or a short storyline, or some instructions. It makes
creating and positioning the multiline text easy.
Have fun!
Edit: Shaun Hirst made a Swift Version (thanks mate)
func addMultilineText(Val: CGFloat )-> CGFloat
{
var Returnval = Val
let tmp = "This is a long string where you do not need to bother about linebreaks and my method breaks it up into multiple SKLabelNode to roughly fit a customized width. Yep it works."; // long string - just type whatever in here
// parse through the string and put each words into an array.
let separators = NSCharacterSet.whitespaceAndNewlineCharacterSet()
let words = tmp.componentsSeparatedByCharactersInSet(separators)
let len = countElements(tmp)
let width = 40; // specify your own width to fit the device screen
// get the number of labelnode we need.
let totLines = len/width+1
var cnt = 0; // used to parse through the words array
// here is the for loop that create all the SKLabelNode that we need to
// display the string.
for ( var i = 0; i < totLines; ++i )
{
var lenPerLine = 0
var lineStr = ""
while lenPerLine < width
{
if cnt > words.count-1
{
break
}
else
{
lineStr = NSString(format: "%@ %@", lineStr, words[cnt])
lenPerLine = countElements(lineStr)
cnt++
}
}
// creation of the SKLabelNode itself
var _multiLineLabel = SKLabelNode(fontNamed: "Oxygen Light")
_multiLineLabel.text = lineStr;
// name each label node so you can animate it if u wish
// the rest of the code should be self-explanatory
_multiLineLabel.name = NSString(format: "line%d", i)
_multiLineLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center
_multiLineLabel.fontSize = 16;
_multiLineLabel.fontColor = UIColor.whiteColor()
let Top = Val-20*CGFloat(i)
_multiLineLabel.position = CGPointMake( self.size.width/2 , Top )
self.sharedInstance.addChildFadeIn(_multiLineLabel, target: self)
Returnval = Top;
}
// return last y pos sp we can add stuff under it
return Returnval
}
ReplyDeleteRewritten it in swift, not sure if its 100% correct but it works.
func addMultilineText(Val: CGFloat )-> CGFloat
{
var Returnval = Val
let tmp = "This is a long string where you do not need to bother about linebreaks and my method breaks it up into multiple SKLabelNode to roughly fit a customized width. Yep it works."; // long string - just type whatever in here
// parse through the string and put each words into an array.
let separators = NSCharacterSet.whitespaceAndNewlineCharacterSet()
let words = tmp.componentsSeparatedByCharactersInSet(separators)
let len = countElements(tmp)
let width = 40; // specify your own width to fit the device screen
// get the number of labelnode we need.
let totLines = len/width+1
var cnt = 0; // used to parse through the words array
// here is the for loop that create all the SKLabelNode that we need to
// display the string.
for ( var i = 0; i < totLines; ++i )
{
var lenPerLine = 0
var lineStr = ""
while lenPerLine < width
{
if cnt > words.count-1
{
break
}
else
{
lineStr = NSString(format: "%@ %@", lineStr, words[cnt])
lenPerLine = countElements(lineStr)
cnt++
}
}
// creation of the SKLabelNode itself
var _multiLineLabel = SKLabelNode(fontNamed: "Oxygen Light")
_multiLineLabel.text = lineStr;
// name each label node so you can animate it if u wish
// the rest of the code should be self-explanatory
_multiLineLabel.name = NSString(format: "line%d", i)
_multiLineLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Center
_multiLineLabel.fontSize = 16;
_multiLineLabel.fontColor = UIColor.whiteColor()
let Top = Val-20*CGFloat(i)
_multiLineLabel.position = CGPointMake( self.size.width/2 , Top )
self.sharedInstance.addChildFadeIn(_multiLineLabel, target: self)
Returnval = Top;
}
// return last y pos sp we can add stuff under it
return Returnval
}
I guess the problem I've run into is removeFromParent(), so I can then change the paragraph each time the user taps the screen. I'll work on it as I go along, but if you have any tips, please let me know. Thank you!
ReplyDeleteI should also note that I made mine as a func within my GameScene.swift file.
ReplyDeleteThis code is a lifesaver. Thank you!!
ReplyDeleteThanks to Shaun Hirst for making the swift version! And thanks all for nice comments. Always appreciate it.
ReplyDeleteHello!
ReplyDeleteI'm really noob and I'm trying to "convert" your code to be used with swift 3. The problem I'm facing is that depending on the number of words per line and the number of words in the text, parts of the text are not shown. Lets say I choose for wordsPerLine the number 6 and my text has 20 words, in this case the last two words stay out.
I think I can add more lines, but some times I'll end up with empty lines and I need centralize this text inside another node…
Any idea about how guarantee that all text will be shown and that I'll be able to centralize this text block in the middle of another node?
_________________________
GameScene.swift
_________________________
class GameScene: SKScene {
override func didMove(to view: SKView) {
let myLabel = SuperLabel()
myLabel.fontName = "Helvetica"
myLabel.fontSize = 20
myLabel.fontColor = UIColor.white
myLabel.position = CGPoint(x: 0, y:0)
myLabel.text = "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty."
myLabel.zPosition = 100
myLabel.createMultilineLabel(wordsPerLine: 6)
addChild(myLabel)
} // END override func didMove
} // END class GameScene:
_________________________
DeleteSuperLabel.swift
_________________________
class SuperLabel: SKLabelNode {
func createMultilineLabel(wordsPerLine: Int) {
if text != nil {
let tmp = text
text = "" // This avoid the text been duplicated
let myFontName = fontName
let myFontSize = fontSize
let myFontColor = fontColor
let separators = NSCharacterSet.whitespacesAndNewlines
let words = tmp?.components(separatedBy: separators)
let lenght = words?.count
let width = wordsPerLine + 1 // number of words per line + 1
// get the number of labelnode we need.
let totLines = (lenght!/width)+1
var cnt = 0 // used to parse through the words array
// here is the for loop that create all the SKLabelNode that we need to
// display the string.
print("lenght: \(lenght) | width: \(width) = \(totLines)")
for i in (0 ..< totLines ) {
print("i: \(i)")
var lenghtPerLine = 0
var lineStr = ""
var wordCount = [String]()
while lenghtPerLine < width
{
print("\(i) lenghtPerLine: \(lenghtPerLine) < width: \(width)")
if cnt > (words?.count)!-1
{
break
}
else
{
lineStr = lineStr + " " + (words?[cnt])!
wordCount = lineStr.components(separatedBy: separators)
lenghtPerLine = wordCount.count
cnt += 1
}
} // END while
// creation of the SKLabelNode itself
let _multiLineLabel = SKLabelNode(fontNamed: myFontName)
_multiLineLabel.text = lineStr
// name each label node so you can animate it if u wish
// the rest of the code should be self-explanatory
_multiLineLabel.name = "line \(i)"
_multiLineLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.center
_multiLineLabel.fontSize = CGFloat(myFontSize)
_multiLineLabel.fontColor = myFontColor
let lineHeight = myFontSize + (myFontSize * 0.25)
let Top = ( -lineHeight * CGFloat(i) ) - myFontSize/2
_multiLineLabel.position = CGPoint(x: 0, y: ( myFontSize * CGFloat(totLines) ) / 2 + Top)
addChild(_multiLineLabel)
} // END for loop
} // END if text != nil
} // END func createMultilineLabel
} // END class SuperLabel