How to make a Typewriter Effect in Unity with TextMeshPro

How to make a Typewriter Effect in Unity with TextMeshPro

A typewriter reveals text one character at a time and it's one of those small details that makes a big difference in games where dialogues have a main role.

0:00
/0:01

Typewriter effect final look

In Unity, the easiest way to do this with TextMeshPro (TMP) is by using its maxVisibleCharacters property.

Why use maxVisibleCharacters?

Usually the most direct way of making a typewriter is to add a new character to the text, one by one. This works fine until you start using rich text tags like <b>, <color>, or <i>.

0:00
/0:03

Typewriter problem with rich text tags

As you see in the video, this method breaks any effect while the tags are not fully appended to the text! And in Text Mesh Pro, the text box gets recalculated as well (which might make your layout shift)

TMP solves this problem with the maxVisibleCharacters property. You set the full text string up front, set maxVisibleCharacters to 0, and then increment it over time. TMP handles all parsing before anything is shown, so rich text tags are never exposed to the player and don’t break.

(This is the approach we use in Text Animator for Unity under the hood as well, with the TMP integration).

Setting up the scene

Let’s implement it!

  1. First, create a canvas
  2. Next, add a GameObject as a child and attach a TextMeshProUGUI component
  3. Also add a new script for our typewriter
TextMeshPro component and Typewriter Effect script
TextMeshPro component and Typewriter Effect script

The core implementation

Here's the full TypewriterEffect script:

using System.Collections;
using TMPro;
using UnityEngine;

[RequireComponent(typeof(TextMeshProUGUI))]
public class TypewriterEffect : MonoBehaviour
{
    [Tooltip("Characters revealed per second")]
    [SerializeField] private float charactersPerSecond = 20f;

    private TextMeshProUGUI _textComponent;
    private Coroutine _typingCoroutine;

    private void Awake()
    {
        _textComponent = GetComponent<TextMeshProUGUI>();
    }

    /// <summary>
    /// Reveals the given text with a typewriter effect.
    /// Call this to start a new line of dialogue.
    /// </summary>
    public void ShowText(string text)
    {
        // Stop any existing typing before starting a new one
        if (_typingCoroutine != null)
            StopCoroutine(_typingCoroutine);

				// set the entire text once, but make it hidden
        _textComponent.text = text;
        _textComponent.maxVisibleCharacters = 0;

        // Force TMP to parse the text and compute character count
        _textComponent.ForceMeshUpdate(); // might not be necessary in all cases

        _typingCoroutine = StartCoroutine(TypeText());
    }

    private IEnumerator TypeText()
    {
        int totalCharacters = _textComponent.textInfo.characterCount;
        float delay = 1f / charactersPerSecond;

        for (int i = 0; i < totalCharacters; i++)
        {
            _textComponent.maxVisibleCharacters = i + 1;
            yield return new WaitForSeconds(delay);
        }

        // Ensure everything is visible at the end
        _textComponent.maxVisibleCharacters = totalCharacters;
        _typingCoroutine = null;
    }

    /// <summary>
    /// Immediately shows all text (useful for a "skip" input).
    /// </summary>
    public void SkipToEnd()
    {
        if (_typingCoroutine != null)
        {
            StopCoroutine(_typingCoroutine);
            _typingCoroutine = null;
        }
        _textComponent.ForceMeshUpdate();
        _textComponent.maxVisibleCharacters = _textComponent.textInfo.characterCount;
    }

    public bool IsTyping => _typingCoroutine != null;
}

A few things worth noting:

ForceMeshUpdate()tells TMP to parse the text immediately, before the coroutine starts reading characterCount. Without this call, textInfo.characterCount may return 0 on the first frame. Calling it once before the loop is all you need. It might not be needed in all cases (e.g. if you wait some frames before showing the text), but we have found it to be more stable to be included at the beginning anyways.
textInfo.characterCount counts only visible characters, not the raw string length. This means rich text tags like <color=#ff0000>red text</color> are not counted, and the effect stops exactly when the last real character is shown.


Finally, to use our typewriter we can call ShowText

typewriter.ShowText("I am <b>very</b> tired of this <color=#ff4444>dungeon</color>.");

What’s next?

While this implementation works well for the basics, building out critical features like per-character pauses, dialogue integration, and sound (which we'll cover in a follow-up post) can require a lot of time.

We built Text Animator for Unity for handling all the heavy lifting for you! It handles all of these complexities out of the box bringing typewriter effects, per-character animations, inline effects, and rich text extensions directly to your TMP object with a simple drop-in component.

0:00
/0:13

Adding the Text Animator component

Beyond the typewriter, Text Animator adds its own tag syntax for per-character effects that TMP doesn't have natively:

"I am <shake>very</shake> tired of this <color=#ff4444>dungeon</color>."

That <shake> tag makes each character in "very" wobble independently, no extra code, and of course you have many other typewriter features ready for you, out of the box.

Anyways, back to the script!

Putting it together in a dialogue system

The TypewriterEffect we wrote earlier component works as a building block. A basic dialogue manager would:

  1. Hold a queue of dialogue lines
  2. Call typewriter.ShowText(nextLine) when advancing
  3. On player input, call typewriter.SkipToEnd() if typewriter.IsTyping, or advance to the next line if typing is already done
public class DialogueManager : MonoBehaviour
{
    [SerializeField] private TypewriterEffect typewriter;

    private Queue<string> _lines = new Queue<string>();

    public void StartDialogue(string[] lines)
    {
        _lines.Clear();
        foreach (var line in lines)
            _lines.Enqueue(line);

        ShowNextLine();
    }

    public void OnPlayerConfirm()
    {
        if (typewriter.IsTyping)
            typewriter.SkipToEnd();
        else
            ShowNextLine();
    }

    private void ShowNextLine()
    {
        if (_lines.Count == 0)
        {
            // Dialogue ended - hide the UI, etc.
            return;
        }
        typewriter.ShowText(_lines.Dequeue());
    }
}

Wrapping up

The maxVisibleCharacters approach is the right foundation for typewriter effects in Unity with TMP. It's clean, rich-text safe, and easy to extend.

Hope this helps! If you have questions or want to show what you built, let us know.

Cheers, Daniel

Daniel is a junior programmer at Febucci Team. Specialized in tech art with a Computer Engineering background. Also co-founded and managed the first student video game development team in Italy with Gabriele.

Comments

Want game-ready tools or need specific services and consulting?

Work with us