1. サイトトップ
  2. ブログ
  3. Unity
  4. 【Unity】TextMeshProでテキストアニメーションを実装する

【Unity】TextMeshProでテキストアニメーションを実装する

はじめに

こんにちは、情熱開発部プログラム1課の辻野です。

皆さんいかがお過ごしでしょうか。
秋を迎えて気温も下がり、過ごしやすい日々が続いています。
季節の変わり目といえば、Steam等のタイムセールを思い浮かべる方は私のほかにもいらっしゃるのではないでしょうか。
読書の秋ということで、本ではなく、セールでノベルゲームを購入してみるのもよさそうですね。

本日はそのノベルゲームのほとんどで使われている、テキストアニメーションをUnityのTextMeshProで実装していきたいと思います。

※Unity 2021.3.27f1を使用

シンプルなテキストアニメーションの実装

初めにフェードインなしのテキストアニメーションを実装していきます。
その前にテキストアニメーションが何を指しているのかわからない方もいると思います。

このように一文字ずつ表示されるアニメーションのことです。
文字送り演出とも言われます。

これからこの実装を解説していきますが、実はTextMeshProを使えば一瞬で実現できてしまいます。

using System.Collections;
using TMPro;
using UnityEngine;

public class TextAnimation : MonoBehaviour
{
    [SerializeField] private TMP_Text tmpText;

    void Start()
    {
        StartCoroutine(Simple());
    }

    private IEnumerator Simple()
    {
        // 文字の表示数を0に(テキストが表示されなくなる)
        tmpText.maxVisibleCharacters = 0;

        // テキストの文字数分ループ
        for (var i = 0; i < tmpText.text.Length; i++)
        {
            // 一文字ごとに0.2秒待機
            yield return new WaitForSeconds(0.2f);

            // 文字の表示数を増やしていく
            tmpText.maxVisibleCharacters = i + 1;
        }
    }
}

maxVisibleCharactersが表示される文字数になっています。
上記のスクリプトを文字入力済みのText(TMP)にアタッチしてください。

実行すると表示される文字が0.2秒ごとに増えていきます。

Logicalbeatが一文字づつ表示されました。

文字の透明度を利用したテキストアニメーションの実装

TextMeshProを利用してお手軽に実装できましたが、キャラクターのカットインで技名やセリフを表示する場面など、より滑らかに文字を表示したい場合があります。
そういった際には、一文字ごとにフェードインさせてあげるといいでしょう。

private IEnumerator FadeIn()
    {
        // script上でテキストを更新した場合、TMPの更新が終わっていない場合があるので再生成
        tmpText.ForceMeshUpdate(true);
        TMP_TextInfo textInfo = tmpText.textInfo;
        TMP_CharacterInfo[] charInfos = textInfo.characterInfo;

        // 全ての文字を一度非表示にする(特殊文字の兼ね合いで要素と文字の数が一致しない場合がある)
        for (var i = 0; i < charInfos.Length; i++)
        {
            SetTextAlpha(tmpText, i, 0);
        }

        // charInfosの要素数分ループ
        for (var i = 0; i < charInfos.Length; i++)
        {
            // 空白または改行文字の場合は無視
            if (char.IsWhiteSpace(charInfos[i].character)) continue;

            // 一文字ごとに0.2秒待機
            yield return new WaitForSeconds(0.2f);

            byte alpha = 0;

            while (true)
            {
                // FixedUpdateのタイミングまで待つ
                yield return new WaitForFixedUpdate();

                // 一文字の不透明度を増加させていく
                alpha = (byte)Mathf.Min(alpha + 10, 255);
                SetTextAlpha(tmpText, i, alpha);

                // 不透明度が255を超えたら次の文字に移る
                if (alpha >= 255) break;
            }

        }
    }

    // charIndexで指定した文字の透明度を変更
    private void SetTextAlpha(TMP_Text text, int charIndex, byte alpha)
    {
        // charIndex番目の文字のデータ構造体を取得
        TMP_TextInfo textInfo = text.textInfo;
        TMP_CharacterInfo charInfo = textInfo.characterInfo[charIndex];

        // 文字を構成するメッシュ(矩形)を取得
        TMP_MeshInfo meshInfo = textInfo.meshInfo[charInfo.materialReferenceIndex];

        // 矩形なので4頂点
        var rectVerticesNum = 4;
        for (var i = 0; i < rectVerticesNum; ++i)
        {
            // 一文字を構成する矩形の頂点の透明度を変更
            meshInfo.colors32[charInfo.vertexIndex + i].a = alpha;
        }
        
        // 頂点カラーを変更したことを通知
        text.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32);
    }

少し長めのコードになりましたが、やってることは難しくはないと思います。
TextMeshProでは一文字ごとにMeshを操作できるので、今回はそれ利用し、頂点カラーでフェードインを実現しています。

わかりやさすさを重視してゆっくりめにしたのですが、少しじれったいですね。
TMP_MeshInfoを使えば色のほかに位置も操作できるので、リッチなアニメーションをスクリプトだけで組めたりします。

演出の調整

アニメーションの実装方法について説明は終わりましたが、せっかくなのでフェード速度を調整していきたいと思います。

スクリプトの修正することで速度を調整できますが、コードを修正した後に実行し、確認する作業を繰り返すのは手間がかかるので、リスタートの実装とフェードの速度をエディタ上で調整できるようにします。

using System.Collections;
using TMPro;
using UnityEngine;

public class TextAnimation : MonoBehaviour
{
    [SerializeField] private TMP_Text tmpText;

    [SerializeField, Tooltip("1秒間における不透明度の増加量")]
    private float FadeSpeed = 2.0f;

    private Coroutine animationCoroutine;

    public void Restart()
    {
        Run();
    }

    private void Start()
    {
        Run();
    }

    private void Run()
    {
        if (animationCoroutine != null)
        {
            StopCoroutine(animationCoroutine);
        }

        animationCoroutine = StartCoroutine(Seamless());
    }

    private IEnumerator Seamless()
    {
        // script上でテキストを更新した場合、TMPの更新が終わっていない場合があるので再生成
        tmpText.ForceMeshUpdate(true);
        TMP_TextInfo textInfo = tmpText.textInfo;
        TMP_CharacterInfo[] charInfos = textInfo.characterInfo;

        // 全ての文字を一度非表示にする(特殊文字の兼ね合いで要素と文字の数が一致しない場合がある)
        for (var i = 0; i < charInfos.Length; i++)
        {
            SetTextAlpha(tmpText, i, 0);
        }

        // charInfosの要素数分ループ
        for (var i = 0; i < charInfos.Length; i++)
        {
            // 空白または改行文字の場合は無視
            if (char.IsWhiteSpace(charInfos[i].character)) continue;

            // 一文字ごとに0.2秒待機
            yield return new WaitForSeconds(0.2f);

            float alpha = 0.0f;

            while (true)
            {
                // FixedUpdateのタイミングまで待つ
                yield return new WaitForFixedUpdate();

                // 一文字の不透明度を増加させていく
                // 秒単位からフレーム単位に変換
                float alphaDelta = FadeSpeed * Time.fixedDeltaTime;
                alpha = Mathf.Min(alpha + alphaDelta, 1.0f);
                SetTextAlpha(tmpText, i, (byte)(255 * alpha));

                // 不透明度が1.0を超えたら次の文字に移る
                if (alpha >= 1.0f) break;
            }

        }
    }

    // charIndexで指定した文字の透明度を変更
    private void SetTextAlpha(TMP_Text text, int charIndex, byte alpha)
    {
        // charIndex番目の文字のデータ構造体を取得
        TMP_TextInfo textInfo = text.textInfo;
        TMP_CharacterInfo charInfo = textInfo.characterInfo[charIndex];

        // 文字を構成するメッシュ(矩形)を取得
        TMP_MeshInfo meshInfo = textInfo.meshInfo[charInfo.materialReferenceIndex];

        // 矩形なので4頂点
        var rectVerticesNum = 4;
        for (var i = 0; i < rectVerticesNum; ++i)
        {
            // 一文字を構成する矩形の頂点の透明度を変更
            meshInfo.colors32[charInfo.vertexIndex + i].a = alpha;
        }
        
        // 頂点カラーを変更したことを通知
        text.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32);
    }
}

SerializeFieldを使いフェード速度を外部から調整できるようにしました。
以前と異なり、アルファは0~1の範囲で、フレームではなく秒単位で操作するようにしています。
これはUnityの他のインターフェースに合わせるようにしています。

Restart()は演出を始めから再生する関数で、新たに追加したボタンを押すこと実行されます。

これで以前の数倍早く、確認作業を行えるようになりました。

課題

動画を確認していただくと、実行中に調整ができていることがわかると思います。
しかし、実はまだ調整はエディタ上で完結していない状態です。
実行を停止した際にSerializeFieldの値が戻ってしまいます。

CopyComponent等で実行中の値を保存してもいいのですが、調整機能の実装で話したとおり、調節をする方は実装者でないことが多いので、分かりやすくしたいですね。

TextAnimationを動的に生成する場合も考えると、Prefabを開いて値を書き換える必要が出てきてしまうので、別の設定ファイルなどから読み取るようにするのが良いのではないでしょうか。

Unityにはそういった場合に適しているScriptableObjectというものがあるので、そちらを使うといいかもしれません。
演出以外のテキストに関する設定をまとめたりもできるので、興味のある方は利用してみてください。

最後に

今回はTextMeshProを使ったテキストアニメーションの実装を解説させていただきました。
演出をスクリプトで実装する際は可読性が低くなりやすいので、丁寧な設計を心掛けたいです。


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。