How to Enable Auto Word Wrapping in Mixed Reality Apps
Want to learn how to enable automatic word wrapping on your Mixed Reality app? Check out this tutorial to learn how to enable auto word wrapping.
Join the DZone community and get the full member experience.
Join For FreeIntroduction
As Mixed Reality and HoloLens apps, in particular, become more mainstream, they become even more complex, and this is a reflection of their interfaces. And if in those user interfaces, we need to communicate something more complex to a user, the text is, in most cases, still the most efficient way of doing that.
Now, a floating text by itself may not be very readable. You might want to use some kind of backdrop. I usually take a kind of blueish background with white text, as that turns out to be the most readable. I know, it feels a bit 2D Windows-ish — but until someone comes up with a better paradigm, it will have to do.
A TextMesh in Unity does not have a clue about word wrapping or fitting into a specific 'box' — you need to do that yourself. There is some of that in the Mixed Reality Toolkit dialog system – but that wraps based on the maximum of characters. If you change the font size or the size of the backdrop, you will need to start testing again to see if your message fits. And, there is no need for that here.
Actual Text Wrapping
public static class TextUtilities
{
public static void WordWrap(this TextMesh mesh, float maxLength)
{
var oldQ = mesh.gameObject.transform.rotation;
mesh.gameObject.transform.rotation = Quaternion.identity;
var renderer = mesh.GetComponent<Renderer>();
var parts = mesh.text.Split(' ');
mesh.text = "";
foreach (var t in parts)
{
var builder = new StringBuilder(mesh.text);
mesh.text = string.Concat(mesh.text, t, " ");
if (renderer.bounds.size.x > maxLength)
{
builder.AppendFormat("{0}{1} ", Environment.NewLine, t);
mesh.text = builder.ToString();
}
}
mesh.text = mesh.text.TrimEnd();
mesh.gameObject.transform.rotation = oldQ;
}
}
This sits in an extension method of the TextUtilities
class. It assumes that the text has already been applied to the text mesh. What is basically does is:
- Rotate the text mesh to Identity so it can measure the width in one plane
- Split the text into words
- For each word:
- Make a
StringBuilder
for the text so far - Add the word to the mesh
- Calculate the width of the resulting mesh
- If the mesh is wider than allowed:
- Add the word to the
StringBuilder
with a newline prepending it - Set mesh to the
StringBuilder’s
contents
Now, I did not make that up myself; I nicked it from here in the Unity Forums, but I kind of simplified and optimized it a bit.
Calculating the Size
As I have written in a previous post, you can calculate an object's size by getting the Render's size. But, I also show what you get is the size after rotation. So, what you need to do is to calculate the unrotated size. I put the same trick as used in the way to measure the text width in an extension method:
public static class GameObjectExtensions
{
public static Vector3 GetRenderedSize( this GameObject obj)
{
var oldQ = obj.transform.rotation;
obj.transform.rotation = Quaternion.identity;
var result = obj.GetComponent<Renderer>().bounds.size;
obj.transform.rotation = oldQ;
return result;
}
}
Rotate the object to identify, get it's render's size, return the object back, then return the result. While this is a rather crude way to get to the size, it seems to work. I stuck this method into my GameObjectExtensions
class.
Connecting All the Dots
Now, the only thing missing is a behavior that will use all of this:
public class SimpleTextDialogController : MonoBehaviour
{
private TextMesh _textMesh;
[SerializeField]
public GameObject _backPlate;
[SerializeField]
public float _margin = 0.05f;
void Start()
{
_textMesh = GetComponentInChildren<TextMesh>();
gameObject.SetActive(false);
}
public void ShowDialog(string text)
{
gameObject.SetActive(true);
StartCoroutine(SetTextDelayed(text));
}
private IEnumerator SetTextDelayed(string msg)
{
yield return new WaitForSeconds(0.05f);
_textMesh.text = msg;
var sizeBackplate = _backPlate.GetRenderedSize();
var textWidth = sizeBackplate.x - _margin * 2f;
_textMesh.WordWrap(textWidth);
_textMesh.GetComponent<Transform>().position -= new Vector3(textWidth/2f, 0,0);
}
}
The start method immediately renders the game object invisible. Calling the ShowDialog
makes the dialog visible and actually sets the text by calling the ShowTextDelayed
coroutine, where the stuff is actually happening. First, we get the size of the backplate. Then, we calculate the desired width of the text. After that, the text is word-wrapped, and then it's moved half the calculated width to the left.
So, why the delay? This is because the complete dialog looks like this:
I reused the AdvancedKeepInViewController
from my previous post. But, if you use the AppearInView
property (as I do), that places the dialog's center exactly on the gaze cursor when it's activated. And, you will see that it tends to appear on the left of the center, then quickly moves to the center.
That is because when the text is rendered without the word wrap, it looks like this:
Because of this, Unity calculates the horizontal component of the center of all the objects in the combined game object that end up a little left of the center of the text. But, what we want to see is this:
The text is nicely wrapped and fits inside the box. Hence, there is a little delay to allow the AdvancedKeepInViewController
to calculate and place the object in the center before we start messing with the text.
Finally, there's a simple behavior called DialogLauncher,
but, basically, all that does is call the ShowDialog
method with some text I got from "Startupsum."
The fun thing is that when you make the dialog bigger and run the app again, it will automatically word wrap to the new size:
And, when you increase the font size, it will look like this:
Requirements and Limitations
There are four requirements to the text:
- The text needs to be placed in the horizontal center of the backdrop plate.
- Vertically, it needs to be in the position where you want the text to start to flow down from:
- Needs to have its anchor in the upper left
- The alignment needs to be left
Limitations
- If you have a single word in your text that's so long it takes up more space than available, you are out of luck.
- As you can see a little in the last image, if the text is so long it takes up more vertical space than available, Unity will render the rest "in thin air" under the backdrop.
It's actually possible to fix that too, but you might wonder how efficiently you are communicating if you need to write large texts in what is, in essence, an immersive experience. How many people read an entire six-paragraph text in a zoo or a museum? "Brevity is the soul of wit," as the Bard once said, so keep texts short and concise!
Conclusion
Thanks to Shopguy on the Unity Forums for the original word wrapping code. The code for this article can be found here.
Published at DZone with permission of Joost van Schaik, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments