【Unity】1文字ずつメッセージを表示する

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/982650/7d9aa411-776f-e2e6-46b3-14643e648930.png" width="50%">

UNDERTAL●やどうぶつの●のようにキャラクターがしゃべる時のアレです。

# コピペ用
そのままコピペしてプロジェクトのどこかにおいてください。

- MessageText.cs

```MessageText.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Text))]
public partial class MessageText : MonoBehaviour
{
    /// セリフ音
    public AudioClip audioClip;
    Button _parentButton;
    AudioSource _audioSource;
    Text _text;
    float _charIntervalSec = 0f;
    bool _isWriting = false;
    int _lineIndex = 0;
    List<Line> _allLines;
    List<char> _muteChars;
    virtual public float charIntervalSec { get => 0.02f; }

    public void Start()
    {
        _audioSource = gameObject.AddComponent<AudioSource>();
        _audioSource.playOnAwake = false;
        _audioSource.clip = audioClip;
        _text = GetComponent<Text>();
        _allLines = GetAllLines();
        _muteChars = GetMuteChars();
        _parentButton = transform.parent.gameObject.GetComponent<Button>();
        _parentButton?.onClick.AddListener(OnClickParentButton);
        ClearLines();
        OnAppear();
    }
}

public partial class MessageText
{
    virtual public List<Line> GetAllLines()
    {
        return new List<Line>() { };
    }
    virtual public List<char> GetMuteChars()
    {
        return new List<char>() { };
    }
    virtual public void OnAppear()
    {
        WriteLine();
    }
    virtual public void OnComplete()
    {
        Destroy(this.gameObject);
    }
    virtual public void OnClickParentButton()
    {
        PushAction();
    }

    public void WriteLine()
    {
        _charIntervalSec = charIntervalSec;
        StartCoroutine(IEWrite(_allLines[_lineIndex].text));
    }
    void ClearLines()
    {
        _text.text = "";
    }
    void makeNewLine()
    {
        var line = _allLines[_lineIndex];
        switch (line.lineType)
        {
            case LineType.SkipOn:
                _text.text += "\n";
                break;
            case LineType.SkipOff:
                _text.text += "\n";
                break;
            case LineType.Clear:
                break;
        }
        _lineIndex += 1;
    }
    void ReadNext()
    {
        makeNewLine();
        var line = _allLines[_lineIndex];
        switch (line.lineType)
        {
            case LineType.SkipOn:
                WriteLine();
                break;
            case LineType.SkipOff:
                WriteLine();
                break;
            case LineType.Clear:
                ClearLines();
                // 再帰呼び出し
                ReadNext();
                break;
        }
    }
    public void PushAction()
    {
        if (_allLines.Count - 1 <= _lineIndex)
        {
            OnComplete();
            return;
        }
        if (_isWriting)
        {
            if (_allLines[_lineIndex].lineType == LineType.SkipOn)
            {
                _charIntervalSec = 0f;
            }
        }
        else
        {
            ReadNext();
        }
    }
    System.Collections.IEnumerator IEWrite(string lineText)
    {
        _isWriting = true;
        foreach (char c in lineText)
        {
            _text.text += c;
            if (!_muteChars.Contains(c))
            {
                _audioSource.Play();
            }
            yield return new WaitForSeconds(_charIntervalSec);
        }
        _isWriting = false;
    }
}

public partial class MessageText
{
    public enum LineType
    {
        SkipOn,
        SkipOff,
        Clear,
    }
    public interface Line
    {
        LineType lineType { get; }
        String text { get; }
    }
    public class SkipOn : Line
    {
        public LineType lineType { get => LineType.SkipOn; }
        public String text { get; }
        public SkipOn(String text)
        {
            this.text = text;
        }
    }
    public class SkipOff : Line
    {
        public LineType lineType { get => LineType.SkipOff; }
        public String text { get; }
        public SkipOff(String text)
        {
            this.text = text;
        }
    }
    public class Clear : Line
    {
        public LineType lineType { get => LineType.Clear; }
        public String text { get => null; }
    }
}

```

# 使い方

Unityの画面で ```Button``` を作り、その子として自動生成される ```Text``` に以下のサンプル ```Sample.cs``` を貼り付けます。

```Sample.cs
using System.Collections.Generic;

class Sample : MessageText
{

    // 文字と文字の間隔秒数 (短いほど早い)
    override public float charIntervalSec { get => 0.02f; }

    // 表示するメッセージ
    override public List<Line> GetAllLines()
    {
        // SkipOn: スキップできるテキスト
        // SkipOff: スキップできないテキスト
        // Clear: ここで文字を全部消す
        return new List<Line>()
        {
            new SkipOn("おはようございます、これは1番目のメッセージです"),
            new SkipOn("こんにちは、これは2番目のメッセージです"),
            new Clear(),
            new SkipOff("こんばんは、これは3番目のメッセージです"),
            new SkipOn("おやすみなさい、これは4番目のメッセージです"),
            new SkipOn("これでメッセージの表示を終わります"),
        };
    }

    // 音を出さない文字
    override public List<char> GetMuteChars()
    {
        return new List<char>()
        {
           ' ', '　', ',' , '、', '。', '「', '」', '!', '?',
        };
    }

    // 表示されたとき
    override public void OnAppear()
    {
        // 最初の1行を書き始める
        WriteLine();
    }

    // 最後のメッセージ表示した後 (クリック後)
    override public void OnComplete()
    {
        // 自分を削除
        Destroy(this.gameObject);
    }

    // 親ボタンをクリックした時
    override public void OnClickParentButton()
    {
        // アクションを進める
        PushAction();
    }
}
```

- この時点でゲームスタートすれば動きます。
- サンプルの中身は好きなように変更してください。
- Unityのインスペクタから ```Text``` を見ると音源をはめ込むところがあります。ここにセリフ音(mp3もしくはwavファイル)を設定してください。
- 画像やテキストの見た目も通常通りボタンの画像やテキストを変更すればOKです。


今回は以上です。ありがとうございました。