How to create a typing effect with UI Toolkit

Hello Everyone

I’m trying to create a typewriter effect using the UI Toolkit (ie displaying characters one by one over time). Surprisingly I couldn’t find any example of this being done already and I couldn’t find any good way to do it.

With the old UI and TextMeshPro this was really easy. TextMeshPro objects had an int property indicating the number of displayed character. That mean you could set the entire text and just increase the number of displayed character over time. UI Toolkit doesn’t seem to have a similar property.

I know I could just use a StringBuilder and build the displayed text string over time adding characters one by one. But this will look ugly with multi line text. The words will start appearing on the line and then wrap to the next line as more characters are added. Not acceptable.

Animating the width property of the Label element will also not work for multi line text.

So what did I miss? Any idea?

Thanks.

Maybe this tutorial can help

1 Like

@aditya from what I can tell this solution builds the string character by character, which is what I want to avoid.

So far the least bad solution I found is to insert the tag <alpha=#00> into the string and move its position over time.

1 Like

Why?! Would you rather build the geometry one quad at a time somewhere deep inside the text element?! That doesn’t sound like an improvement, but by all means, be my guest.

Attached is my typewriter script (for uGUI) but you could trivially repurpose it to send to a UIElement. Its product is just a string output obviously. The script handles rich text correctly, eg, chops apart the tags and reassembles it.

8936637–1225557–TypewriterWithHTMLStrings.unitypackage (5.2 KB)

If you just need the code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

// @kurtdekker - inspired from Unity forum at:
// https://forum.unity.com/threads/text-typewriter-without-displaying-richtext-tag.891742/

public class TypewriterWithHTMLStrings : MonoBehaviour
{
	const string initialString = "Leave this blank and it will pull up what is in the UI.Text field already.";
	[Tooltip(initialString)]
	[Multiline]
	public string originalString;

	public string cursor;

	public float interval;

	// If you prefer to use TMPro, then do two things:
	//	add using TMPro; at the top of this script
	//	replace this Text declaration with TextMeshProUGUI
	public Text output;

	void Reset()
	{
		originalString = initialString;
		cursor = "_";
		interval = 0.05f;
	}

	int GetPayloadLength( string s)
	{
		int count = 0;
		bool payload = true;

		for (int i = 0; i < s.Length; i++)
		{
			char c = s[i];   // one character

			if (payload)
			{
				if (c == '<')
				{
					payload = false;  // we just entered a formatting block
				}
				else
				{
					count++;   // tally payload character
				}
			}
			else
			{
				if (c == '>') payload = true;  // done with this formatting block, back to payload
			}
		}

		return count;
	}

	string  GetPartialPayload( string s, int typedSoFar)
	{
		int count = 0;
		bool payload = true;
		string tempString = "";

		for (int i = 0; i < s.Length; i++)
		{
			char c = s[i];   // one character

			if (payload)
			{
				if (c == '<')
				{
					tempString = tempString + c.ToString();
					payload = false;  // we just entered a formatting block
				}
				else
				{
					if (count < typedSoFar) tempString = tempString + c.ToString();
					count++;   // tally payload character
				}
			}
			else
			{
				tempString = tempString + c.ToString();
				if (c == '>')
				{
					payload = true;  // done with this formatting block, back to payload
				}
			}
		}

		return tempString;
	}

	IEnumerator Start ()
	{
		// if blank, pick up what's in the UI.Text field to start with
		if (string.IsNullOrEmpty(originalString))
		{
			originalString = output.text;
		}

		int i = 0;
		while( true)
		{
			output.text = GetPartialPayload( originalString, i) + cursor;

			yield return new WaitForSeconds( interval);

			i++;

			if (i > GetPayloadLength(originalString))
			{
				break;
			}
		}

		// remove cursor
		output.text = originalString;
	}
}

Because if I build the string by adding one character at a time then long words will start appearing on one line and then be wrapped on the next one when more character are added. That’s ugly.

Don’t combine problems into one to try and solve them.

Run the text through whatever you want to use for your line breaking, insert linebreaks, then feed it through the typewriting script.

1 Like

Yeah, or just have the entire text written at once and reveal it letter by letter, like pretty much any game would, instead of having to go through multiple processing steps. Your script simply doesn’t provide a solution to the actual issue, and just saying “yeah just bring it into the right format first” does not help. This is exactly what he doesn’t know how to do, or simply doesn’t work for this game, and why he’s asking here.

Also, in my opinion he’s not combining issues. Revealing letters one by one is way easier than figuring out line breaks beforehand. This needs to be handled automatically. You’re separating a simple issue into two individual ones for no reason.

The necessity for inserting line breaks beforehand is an issue that you introduced by not implementing the feature according to the specifications. There’s no reason not to handle the entire thing in one go - namely by revealing the characters one by one instead. Which is easily possible in TMPro. Unfortunately I also have no idea how to do this in UIToolkit. ChiwTheNeko’s suggestion to move an alpha-tag to reveal individual characters actually seems pretty okay.

1 Like

I succeeded with the moving alpha tag method. It’s a bit convoluted because you have to handle the and tags that are in the text already. Also, the memory allocation with StringBuilder.ToString() kinds of hurt my backside. But it works.

I still wish there was an easier way though.

Hi @ChiwTheNeko hope you doing well.
Would you mind showing a working snippet of your solution?
Thanks

I now understand how the tags work in UI Toolkit.

Here is an example for those in the future:


using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;

public class LabelTypewriterEffect : MonoBehaviour
{
    Label label;
    [SerializeField] float typewriterDelay = 0.05f;
    [SerializeField] string debugString;
    [SerializeField] string fullText = "Hello, this is a typewriter effect using Unity UI Toolkit!";
    [SerializeField] UIDocument UIDocument;

    void Start()
    {
        /* Make sure to target the desired label by name: UIDocument.rootVisualElement.Q<Label>("YourLabelName") */
        /* Now it is taking the first label in the hierarchy. */
        label = UIDocument.rootVisualElement.Q<Label>();
        StartCoroutine(WriteText());
    }

    IEnumerator WriteText()
    {
        for (var i = 0; i < fullText.Length; i++)
        {
            var visibleText = fullText[..(i + 1)];
            var invisibleText = fullText[(i + 1)..];
            label.text = $"{visibleText}<alpha=#00>{invisibleText}";
            debugString = label.text;
        
            yield return new WaitForSeconds(typewriterDelay);
        }
    }
}

You can check the tag moving through the label in the editor by looking the debugString field.

1 Like

I’m still working on it. Handling rich text proved to be troublesome.

The last problem I have it that <color> and <alpha> rich text tags don’t play well with text shadows. If I make half of the text transparent the shadows are still visible. I think that’s a bug in Unity.

For example this is what happen when the text string is Lorem ipsum dolor sit amet, <color=#FFFFFF00>consectetur adipiscing elit.</color>.

1 Like

Did you manage to solve the type writer effect with rich text tags? In a personal project I managed to get the moving alpha tag working but it would always break when using the color tag to highlight certain things. If you have a working solution I’d be very grateful to know what you did

My typewriter solution already posted above already handles most simple HTML tags.