skrilmps
skrilmps

Reputation: 695

Unity fill Text Box until full

I have a text box in Unity of a set size that I would like to populate with text until it is full, then wait for the user to turn the page before re-populating it with the remaining text, repeating until all the text has been viewed. The idea is to create an iBooks / Kindle type app. I would like it to work regardless of which font size the user has selected (so that the vision-impaired can choose a larger font size, for instance). Similar functionality exists in iBooks and Kindle.

The challenge I'm running into is: how do I know when the text box is full?

I've read quite a few posts on stack overflow and answers.unity3d.com and came up with the following method, which doesn't work very well.

Basically the idea is to use a Text Mesh object (which is invisible to the user) and put one word in the Text Mesh at a time, allowing Unity to render it, and then query the bounding box to find out how wide and how tall the text is. Using that information, I can know whether adding a word to my text box would cause me to exceed the width of my text box (and thus it's time for a new line) and/or exceed the height of my text box (and thus it's time to wait for the user to turn the page).

There are a few problems with this method, however.

1) My Text Mesh does not create on-screen text that is the same size as my Text Box, even if I make sure they both have the same position (relative to the camera), same scaling (1, 1, 1), same font size, and same font type. The Text Mesh has a property called Character Size that I have to manually fine-tune through trial and error until the two appear to be the same size.

2) Looking at the bounding box of the Text Mesh gives me the size in dimensions of Unity world units. I have to convert these units to pixels, which requires knowing about the camera's position and orientation. I did this using WorldToScreenPoint, but I don't think it's very accurate.

3) As I'm adding lines to the text box I'm accumulating the total height of the lines that I've added by looking at the Text Mesh size. However, this does not incorporate the amount of space included between lines, and so my accounting of the height of the block of text is off. I tried using mesh.font.lineheight but this doesn't seem to give me an accurate height, or perhaps I don't know what the units are that it's using.

Does anyone have a better idea for achieving what I'm trying to do here? It seems like it should be a fairly simple matter to fill a text box until it is full but I've found answers on this topic to be sparse.

Here is the code I have so far. In this example I came up with a text string for testing purposes. However, in typical usage, this text will not be known at compile time but rather is determined at runtime as it reads text from a file.

private string myText = 
    "A-tisket, a-tasket\nA green and yellow basket\n" +
    "I wrote a letter to my love\nAnd on the way I dropped it\n" +
    "I dropped it\nI dropped it\nYes, on the way I dropped it\n" +
    "A little boy he picked it up \nand put it in his pocket.\n\n" +
    "Baa, baa, black sheep\nHave you any wool?\nYes sir, yes sir\n" +
    "Three bags full.\nOne for my master\nAnd one for the dame\n" +
    "One for the little boy\nWho lives down the lane.";

public Text textBox;
public TextMesh mesh;
public Button nextButton;

private int wordnum;
private List<string> words;
private Camera myCamera;

void Start ()
{
    // divide the text up into words
    words = new List<string> (
        myText.Split (new string[] { " ", "\n", "\t" }, 
            System.StringSplitOptions.RemoveEmptyEntries));
    // store a reference to the camera
    myCamera = GetComponent<Camera>();
    // don't allow the user to click Next Page button unless there's more text to view
    nextButton.enabled = false;
    // turn off rendering of the text mesh because the user shouldn't see it
    mesh.GetComponent<Renderer>().enabled = false;
    wordnum = 0;
    setContents ();
}

void setContents ()
{
    float heightSoFar = 0;
    float linewidth = 0;
    float maxheight = 0;
    float currheight = 0;
    int wordCount = 0;

    float unitsPerPixel = Vector3.Distance(myCamera.ScreenToWorldPoint(Vector3.zero), myCamera.ScreenToWorldPoint(Vector3.right));
    float pixelsPerUnit = 1 / unitsPerPixel;

    textBox.text = "";

    while (heightSoFar < textBox.rectTransform.rect.height && wordnum < words.Count) {

        mesh.text = words[wordnum] + " ";

        Bounds bounds = mesh.GetComponent<Renderer>().bounds;
        float width = bounds.size.x * pixelsPerUnit;

        currheight = bounds.size.y * pixelsPerUnit + mesh.font.lineHeight;
        if (currheight > maxheight) maxheight = currheight;

        if (linewidth + width > textBox.rectTransform.rect.width) {
            // time for new line
            textBox.text += "\n" + words [wordnum];
            heightSoFar += maxheight;
            linewidth = width;
        } else {
            // add it to this line
            if (wordCount == 0) textBox.text += words[wordnum];
            else textBox.text += " " + words [wordnum];
            linewidth += width;
            wordCount++;
        }
        wordnum++;
    }

    if (wordnum < words.Count) 
    {
        nextButton.enabled = true;
    } else {
        nextButton.enabled = false;
    }
 }

public void handleClick()
{
    setContents();
}

UPDATE: Here is an alternative method for finding the word width that I found here: http://answers.unity3d.com/questions/898770/how-to-get-the-width-of-ui-text-with-horizontal-ov.html

Instead of using a text mesh, this approach queries the font for the CharacterInfo struct which has the size of a character, and then adds the sizes of the characters to get the size of the word. This seems to be an improvement over the above technique because it fixes issues #1 and #2 above. #1 is fixed because, without the Text Mesh, I no longer have to fine-tune Character Size. #2 is fixed because in this approach everything is done in units of pixels instead of world units. Issue #3 still remains, though. Although CharacterInfo can give me the glyph height, it doesn't give me any info on the spacing between lines. I did some googling and found that typical line-spacing is 120-145% of the character height. So I'm multiplying by 1.45 to get an upper bound on the height of each line (including line spacing). But perhaps there's a better way?

Here's the code using that approach:

private string myText = 
        "A-tisket, a-tasket\nA green and yellow basket\n" +
        "I wrote a letter to my love\nAnd on the way I dropped it\n" +
        "I dropped it\nI dropped it\nYes, on the way I dropped it\n" +
        "A little boy he picked it up \nand put it in his pocket.\n\n" +
        "Baa, baa, black sheep\nHave you any wool?\nYes sir, yes sir\n" +
        "Three bags full.\nOne for my master\nAnd one for the dame\n" +
        "One for the little boy\nWho lives down the lane.";
public Text textBox;
public Button nextButton;

private int wordnum;
private List<string> words;
private Font myFont;
private int myFontSize;

void Start ()
{
    words = new List<string> (
        myText.Split (new string[] { " ", "\n", "\t" }, 
            System.StringSplitOptions.RemoveEmptyEntries));
    nextButton.enabled = false;
    wordnum = 0;
    setContents ();
}

void setContents ()
{
    float heightSoFar = 0;
    float linewidth = 0;
    float maxheight = 0;
    CharacterInfo characterInfo = new CharacterInfo();

    textBox.text = "";
    myFont = textBox.font;
    myFontSize = textBox.fontSize;

    while (heightSoFar < textBox.rectTransform.rect.height && wordnum < words.Count) {


        char[] chars = (words[wordnum] + " ").ToCharArray();
        float width = 0;

        foreach(char c in chars) {
            myFont.GetCharacterInfo(c, out characterInfo, myFontSize);
            width += characterInfo.advance;
            if (characterInfo.glyphHeight * 1.45f > maxheight) maxheight = characterInfo.glyphHeight * 1.45f;
        }

        if (linewidth + width > textBox.rectTransform.rect.width) {
            // time for new line
            heightSoFar += maxheight;
            // are we still within vertical bounds?  if so, add a new line
            if (heightSoFar < textBox.rectTransform.rect.height) {
                textBox.text += "\n" + words [wordnum] + " ";
                wordnum++;
                linewidth = width;
            }
        } else {
            // add it to this line
            textBox.text += words [wordnum] + " ";
            wordnum++;
            linewidth += width;
        }
    }

    if (wordnum < words.Count) {
        nextButton.enabled = true;
    } else {
        nextButton.enabled = false;
    }
}

public void handleClick()
{
    setContents();
}

Upvotes: 2

Views: 1528

Answers (1)

Adam
Adam

Reputation: 33146

CharacterInfo is the way to do it.

But you have to know the "Super Secret Unity Staff Hate You", undocumented, MagicValues(TM) of font layout in Unity: Lines are spaced at "the maximum of the heights of all glyphs AND the font-line-height", where the "font-line-height" is defined as "110% of the font-size, in pixels".

... i.e. some version of "Unity is hardcoded to always use a 110% linespacing".

UPDATE: also be aware that it's not actually 110%, due to rounding errors and Unity's choice of forcing all font-sizes to be integer values. It appears (via trial-and-error) to be "Mathf.Floor( 1.1f * fontSize )" - i.e. 110% but rounding-down any decimals.

Upvotes: 1

Related Questions