In this post I’m going to describe some technical problems one is likely to stumble upon when implementing a typewriter effect (meaning text apearing letter by letter, as often seen with written dialoge in games). I’ll then propose a solution to these problems.

If you were to naively implement a typewriter effect in unity, you might come up with something like this:

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Text))]
public class TypewriterEffect : MonoBehaviour
{
    private float _charDelay = 0.1f;
    
    private Text _uiText;
    private string _fullText;
    private float _elapsed = 0f;
    private int _numCharsToShow;

    void Awake()
    {
        _uiText = GetComponent<Text>();
        _fullText = _uiText.text;
        _uiText.text = "";
    }
    
    void Update()
    {
        _elapsed += Time.deltaTime;

        var changed = false;
        
        while (_elapsed >= _charDelay && _numCharsToShow < _fullText.Length) {
            _elapsed -= _charDelay;
            _numCharsToShow++;
            changed = true;
        }

        if (changed) {
            _uiText.text = _fullText.Substring(0, _numCharsToShow);
        }
    }
}

TLDR: We start by caching the text value of the ui Text component. Then we reset the displayed value to an empty string. Every frame, we determine if we must show more characters depending on the timing. If we do, we assign a truncated version of the original text to the ui Text component.

This approach seems to work quite well at first, but it has a some serious problems.

Words wrapping

It’s common when displaying long text to have the line wrap when it’s too long to be contained in the ui rect. When this happens, a really ugly problem of our solution shows up. Whenever a word is too long fit on the line, it gets wrapped. Unfortunately it doesn’t happen immediately, so the word gets drawn on the same line at first as the characters appear, then when it gets too long, it’s kicked on the next line. It’s quite displeasing to look at:

Memory allocations

You may already have heard about unity’s infamous garbage collector, and how it’s advisable to avoid allocation during gameplay. Do we allocate? A look at the profiler tells us that we’re in fact allocating each time we update our ui text.

Indeed, every time we call Substring, a brand new string is returned. There’s no (sane) way to get around this as strings in C# are immutable. That means anytime we need to change a string we have to create a new one and throw the other away. We could solve this cleanly if unity had a method to feed a char array and a length to a Text component (as this guy suggests). In that case we would be able to build a char array with the whole text, and change only the length parameter.

In our case, the amount of memory allocated is not huge (it directly depends on the length of the string, for me it was around 150 bytes), but remember that we’re doing the operation multiple time per second; it adds up quickly. Eventually, the heap will be full and the garbage collector will fire, causing one of these noticeable hiccups we’re used to as unity users…

Here you can see the allocations (blue line) growing as we allocate bigger and bigger Substring (tested on an iPhone 5S). When there's a GC collection, the memory usage is going down (light pink line). In this test, the garbage collection spikes are very small, but that's only because we have very few c# to deal with, so the garbage collector has not much to do; on real projects, the tree of objects the GC has to work with will be much bigger.

Rich text

Unity allows us to style the text using html-style markup (like <b> for bold and <i> for italic). Unfortunately, with our current implementation, the markup characters are animated with the rest of the text, resulting in markup popping in and out as the text appears.

If we wanted to fix this with our current implementation, we would have to parse the markup and generate an adjusted string on each change (e.g dynamically adding the correct closing tag(s) at the end of the string). Doesn’t sound too fun, right?

A better solution: a ui mesh effect

We can solve these three issues using a simple trick. We’ll implement our typewriter as a UI mesh effect (vertex effect in older versions of unity). What’s that, you ask? It’s a special type component that can be added alongside a Graphic component (Image, Text, …). When rendering the canvas, the UI system passes the mesh of each graphic to their mesh effects so that they can make modifications. Built-in mesh effects are Shadow and Outline (they actually duplicate and offset geometry behind the scenes).

The idea is that we don’t change the string of the text component at any point. Instead, we’ll take the geometry generated by the ui system, and trash everything we don’t want (all the chars after a given offset in our data). Each char is drawn with a quad by the UI system. Since these quads are ordered, we just have to count how many vertex are needed (to triangles = 6 vertices; charsToShow * 6) and merrily delete the rest!

Here is the code:

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

[RequireComponent(typeof(Text))]
public class BetterTypewriterEffect : BaseMeshEffect
{
    public float _charDelay = 0.1f;

    private int _textLenght;
    private float _elapsed = 0f;
    private int _numCharsToShow;
    private List<UIVertex> _uiVertexList;
    
    void Awake()
    {
        _textLenght = GetComponent<Text>().text.Length;
    }

    void Update()
    {
        // Our base class has a [ExecuteInEditMode] attribute. We don't want to
        // execute in the editor, so we do a early return here if it's the case.
        if (!Application.isPlaying) {
            return;
        }
        
        _elapsed += Time.deltaTime;

        var changed = false;
    
        while (_elapsed >= _charDelay && _charDelay > 0.001f) {
            _elapsed -= _charDelay;
            _numCharsToShow++;
            changed = true;
        }

        if (changed) {
            // SetVerticesDirty tells the UI system that this graphic needs to be redrawn
            this.graphic.SetVerticesDirty();
        }
    }
    
    public override void ModifyMesh(VertexHelper vh)
    {
        if (!this.IsActive() || !Application.isPlaying) {
            return;

        // we create the list once and cache it to avoid allocation on subsequent frames
        if (_uiVertexList == null) {
            _uiVertexList = new List<UIVertex>();
        }
        
        vh.GetUIVertexStream(_uiVertexList); // the vertex helper will fill our list with vertices

        var vertsToShow = _numCharsToShow * 6;
        _uiVertexList.RemoveRange(vertsToShow, _uiVertexList.Count - vertsToShow);
        
        vh.Clear();
        vh.AddUIVertexTriangleStream(_uiVertexList); // put back the trucated list in the helper
    }
}   

Let’s see how it solves our problems.

Words wrapping: since the layout system executes before our effect is applied on the text mesh, all the calculations are made assuming the whole text will be displayed. You can even center the text horizontally and verticaly without it moving while it’s appearing. Great!

Memory allocations: this allocates zero bytes per execution. Feels good, doesn’t it?

Rich text: no popping of markup tags anymore! The text is parsed in it’s integrity, we act only on the geometry. But there’s a small catch… as I was writing this post and making tests, I realized the typing was stopping a little longer before and after the marked-up words. Turns out unity does what looks like an ugly hack to me: it actually generates quads for the tag characters… with a width of 0. Yeah… Anyway, I say it’s not a bug, it’s a feature. Words in bolds are supposed to be important, we might as well pause for a little longer before drawing them!

…Or just use TextMesh Pro

If you’re starting a new project, a better option might be to use TextMesh Pro. It’s a tool to render high quality text that was initially sold by it’s developer, and was so good that it has been acquired by unity. It’s supposed to replace the unity Text component at some point, but as of this day it’s not tightly integrated with unity.

TextMesh Pro has a maxVisibleCharacters property that you can use just like _numCharsToShow in our implementation (I never tried it though).

Making it feel good

We’ve covered the technical issues we had, but this is still an very basic effect. To make it an enjoyable experience in an actual game, we’ll need to tune the speed exactly right. I found that waiting longer on punctuation characters helps a ton with the rythm. I’ve also been experimenting with progressively fading the characters in which makes for a smoother, less stressful excperience.

Then of course there is sound. Be sure to choose a sound that doesn’t feel to repetitive or anoying, and consider variing the pitch or choosing randomly between a few sounds. You could also play a different sound for space or return characters. Sky is the limit!

Thanks for reading!

If you enjoyed this read and have any comment, or if you want to share your own tricks to make this kind of effects, you can contact me directly at seb.degraff@gmail.com or respond to the tweet below.