[Unity]TextMeshProの文字をバウンドさせるサンプル

自分メモ用
UnityでTextMeshProのテキストをバウンドさせるためのスクリプトのサンプル

# 出来上がったもの
![Point.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492164/9d512d74-93db-3794-c01f-72ce9b0c6a5e.gif)

<details>
<summary>サンプルコード</summary>

```CS:BoundText.cs
using System;
using TMPro;
using UnityEngine;

[RequireComponent(typeof(TextMeshProUGUI)), DisallowMultipleComponent]
public class BoundText : MonoBehaviour
{
    private TextMeshProUGUI textMeshPro = null;

    private float elapseTime = 0;

    [SerializeField]
    private float boundTime = 0.2f;

    [SerializeField]
    private float boundHeight = 20.0f;

    [SerializeField]
    private float afterTime = 0.5f;

    private Func<float, float> CalcFunc = null;

    // Awake is called when an instance of the script is loaded
    private void Awake()
    {
        this.textMeshPro = this.GetComponent<TextMeshProUGUI>();
    }

    // Start is called before the first frame update
    private void Start()
    {
        elapseTime = 0.0f;
        CalcFunc = GetBoundFunc(boundHeight, boundTime);
    }

    // Update is called once per frame
    private void Update()
    {
        if (Time.deltaTime < 1)
        {
            Animation(elapseTime, boundTime);
            float deltaTime = Time.deltaTime;
            elapseTime += deltaTime;
        }

        if (elapseTime > (boundTime + afterTime))
        {
            Destroy(this.gameObject);
        }
    }

    private void Animation(float time, float maxTime)
    {
        textMeshPro.ForceMeshUpdate(true);
        TMP_TextInfo textInfo = textMeshPro.textInfo;

        float[] preHeights = new float[4];

        for (int i = 0; i < textInfo.characterInfo.Length; i++)
        {
            TMP_CharacterInfo charInfo = textInfo.characterInfo[i];
            if (!charInfo.isVisible) continue;

            int mIndex = charInfo.materialReferenceIndex;
            int vIndex = charInfo.vertexIndex;

            Vector3[] vectors = textInfo.meshInfo[mIndex].vertices;

            float height = CalcFunc(time - i * boundTime * 0.5f);
            vectors[vIndex + 0].y += height;
            vectors[vIndex + 1].y += height;
            vectors[vIndex + 2].y += height;
            vectors[vIndex + 3].y += height;
        }

        for (int i = 0; i < textInfo.materialCount; i++)
        {
            if (textInfo.meshInfo[i].mesh == null) continue;

            textInfo.meshInfo[i].mesh.vertices = textInfo.meshInfo[i].vertices;
            textMeshPro.UpdateGeometry(textInfo.meshInfo[i].mesh, i);
        }
    }

    private Func<float, float> GetBoundFunc(float maxHeight, float maxTime)
    {
        float v0 = 4 * maxHeight / maxTime;
        float g = 2 * v0 / maxTime;

        return GetBoundFuncFromInitSpeed(g, v0);
    }

    private Func<float, float> GetBoundFuncFromInitSpeed(float g, float v0)
    {
        return (float t) =>
        {
            float h = v0 * t - (g * Mathf.Pow(t, 2.0f) * 0.5f);
            return h < 0 ? 0 : h;
        };
    }
}
```
</details>

# コード解説

## 概要
このスクリプトをアタッチしたTextMeshProは、オブジェクト生成後、１文字ずつ文字がバウンドしていき、全部バウンドを終えると自信を削除(Destroy)する。

## 詳細
#### クラス属性
```CS
[RequireComponent(typeof(TextMeshProUGUI)), DisallowMultipleComponent]
public class BoundText : MonoBehaviour
```
* RequireComponent(typeof(TextMeshProUGUI))
TextMeshProUGUIコンポーネントを持つGameObjectにのみアタッチできる。

* DisallowMultipleComponent
多重にアタッチはできない

#### パラメタ
```CS
    [SerializeField]
    private float boundTime = 0.2f;

    [SerializeField]
    private float boundHeight = 20.0f;

    [SerializeField]
    private float afterTime = 0.5f;
```
* boundTime
１文字が跳ねて落ちるまでの時間
* boundHeight
１文字が跳ねる高さ
* afterTime
全部跳ね終わった後しばらく表示している時間
 
#### Awake
```CS
    private void Awake()
    {
        this.textMeshPro = this.GetComponent<TextMeshProUGUI>();
    }
```
スクリプト内で参照するTextMeshProコンポーネントの事前取得

#### Start
```CS
    private void Start()
    {
        elapseTime = 0.0f;
        CalcFunc = GetBoundFunc(boundHeight, boundTime);
    }
```
スクリプト内で使用する変数の初期化
* elapseTime
スクリプト開始（オブジェクト生成）からの起動時間をカウントする変数
経過時間に伴うバウンドの高さを計算することに使用
* CalcFunc
バウンドする高さを計算するための関数を生成
時間以外のパラメタは事前生成したほうがいいと思ったのでここで生成している

```CS:GetBoundFunc
    private Func<float, float> GetBoundFunc(float maxHeight, float maxTime)
    {
        float v0 = 4 * maxHeight / maxTime;
        float g = 2 * v0 / maxTime;

        return GetBoundFuncFromInitSpeed(g, v0);
    }

    private Func<float, float> GetBoundFuncFromInitSpeed(float g, float v0)
    {
        return (float t) =>
        {
            float h = v0 * t - (g * Mathf.Pow(t, 2.0f) * 0.5f);
            return h < 0 ? 0 : h;
        };
    }
```
* maxHeight
バウンドする高さ
* maxTime
バウンドしている時間

基本的な計算式は、重力加速度（$ y = v_0t - \frac{1}{2}gt^2 $）と同じ
maxHeightとmaxTimeから初速$ v_0 $と重力加速度$g$を算出している。
> 計算式は下記ページを参考にしている
> https://ja.wikipedia.org/wiki/%E3%83%9C%E3%83%BC%E3%83%AB%E3%81%AE%E8%B7%B3%E3%81%AD%E8%BF%94%E3%82%8A%E9%81%8B%E5%8B%95

#### Update
```CS
    private void Update()
    {
        if (Time.deltaTime < 1)
        {
            Animation(elapseTime, boundTime);
            float deltaTime = Time.deltaTime;
            elapseTime += deltaTime;
        }

        if (elapseTime > (boundTime + afterTime))
        {
            Destroy(this.gameObject);
        }
    }
```
`` if (Time.detailTIme < 1) ``は、初回Updateの時に大きな数になることがあったのでセーフティで挿入
`` Animation(elapseTime, boundTime); ``で、文字の高さを変更
`` elapseTime += deltaTime ``で、経過時間を加算

```CS:Animation
    private void Animation(float time, float maxTime)
    {
        textMeshPro.ForceMeshUpdate(true);
        TMP_TextInfo textInfo = textMeshPro.textInfo;

        for (int i = 0; i < textInfo.characterInfo.Length; i++)
        {
            TMP_CharacterInfo charInfo = textInfo.characterInfo[i];
            if (!charInfo.isVisible) continue;

            int mIndex = charInfo.materialReferenceIndex;
            int vIndex = charInfo.vertexIndex;

            Vector3[] vectors = textInfo.meshInfo[mIndex].vertices;

            float height = CalcFunc(time - i * boundTime * 0.5f);
            vectors[vIndex + 0].y += height;
            vectors[vIndex + 1].y += height;
            vectors[vIndex + 2].y += height;
            vectors[vIndex + 3].y += height;
        }

        for (int i = 0; i < textInfo.materialCount; i++)
        {
            if (textInfo.meshInfo[i].mesh == null) continue;

            textInfo.meshInfo[i].mesh.vertices = textInfo.meshInfo[i].vertices;
            textMeshPro.UpdateGeometry(textInfo.meshInfo[i].mesh, i);
        }
    }
```
>TextMeshProの文字位置変更については下記ページを参考
>https://coposuke.hateblo.jp/entry/2020/06/07/020235

メインは下記
```CS
            float height = CalcFunc(time - i * boundTime * 0.5f);
            vectors[vIndex + 0].y += height;
            vectors[vIndex + 1].y += height;
            vectors[vIndex + 2].y += height;
            vectors[vIndex + 3].y += height;
```
ここで高さを計算し各文字にいれている
`` - i * boundTime * 0.5f ``部分で、文字ごとに次々バウンドするようにしている。
（`` i ``が文字位置なので、文字位置ごとにバウンド時間の半分の時間ズラして表示する）

# 参考URL
* [Wikipedia(ボールの跳ね返り運動)](https://ja.wikipedia.org/wiki/%E3%83%9C%E3%83%BC%E3%83%AB%E3%81%AE%E8%B7%B3%E3%81%AD%E8%BF%94%E3%82%8A%E9%81%8B%E5%8B%95 "ボールの跳ね返り運動")
* [はてなブログ(【Unity】TextMesh Proをアニメーションさせる～デザイナー編)](https://coposuke.hateblo.jp/entry/2020/06/07/020235 "【Unity】TextMesh Proをアニメーションさせる～デザイナー編")