Friday, December 5, 2014

Multiline SKLabelNode? Hell Yes Please XD

Woah. Another Noobies Tutorial yayyyyyy....

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  
 }  

7 comments:


  1. Rewritten 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
    }

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

    ReplyDelete
  3. I should also note that I made mine as a func within my GameScene.swift file.

    ReplyDelete
  4. This code is a lifesaver. Thank you!!

    ReplyDelete
  5. Thanks to Shaun Hirst for making the swift version! And thanks all for nice comments. Always appreciate it.

    ReplyDelete
  6. Hello!
    I'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:


    ReplyDelete
    Replies
    1. _________________________
      SuperLabel.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

      Delete