【Unity】TextMeshProでリンクをタップした際の色を変える

# 概要
以下のようなことをやる方法です。
ソースコード書いてるだけで解説はしてません。

![Jul-29-2023 15-11-22.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163629/6ba29d84-0492-d722-4ccf-ab3946d9ce80.gif)

## 動作環境

* Unity 2021.3.19f1
    * 多分もっと古くても動くと思う

# 内容

TextMeshProではリッチテキストでタグを扱うことができ、その中の &lt;Link&gt; タグを使うことでリンクを扱うことができます。

タグの一覧
https://www.midnightunity.net/textmeshpro-richtext-tags/

&lt;Link&gt;タグはあくまでもリンクの情報をもたせるだけになるため、リンクの色やアンダーラインを設定する際には、 &lt;color&gt;タグと&lt;u&gt;タグを組み合わせて表現します。
上記の例の場合、以下のようなリッチテキストの文章を設定してます。

```
槍ボックルは他の味方コロボックルが3体以上存在
する場合<link="KYOKA"><u><color=#f86051>[ 強化召喚 ]</color></u></link> +1される
```

今回のコードは、上記のように &lt;link&gt;タグ内に&lt;color&gt;、&lt;u&gt;タグが設定されていること前提になります。(uタグはあってもなくてもOK)

# ソースコード
以下のクラスを作ります。

* SingleTouch.cs : UnityEditorとスマホのTouchイベントの差分を吸収するクラス
* TextMeshProExt.cs : TextMeshProUGUIの拡張クラス
* TextLinkGUI.cs : テキストの色をタップ中返るクラス

リンクの色を変える処理自体は、 `TextMeshProExt#ChangeLinkColor` と `TextMeshProExt#ResetLinkColor` でやってます。なのでそこだけコピれば使えます。

```csharp: SingleTouch.cs
using UnityEngine;
using UnityEngine.EventSystems;

public class SingleTouch {
    private Vector2 mLatest = Vector2.zero;
    private static SingleTouch sInstance = new();
    public static SingleTouch Default => sInstance;

    public Vector2 Position {
        get {
            if (HasTouched()) {
                #if UNITY_EDITOR
                // TODO: PCもこっちだから判定方法は変えた方がよさげ
                mLatest = Input.mousePosition;
                #else
                mLatest = Input.GetTouch(0).position;
                #endif
            }

            return mLatest;
        }
    }
}
```

```csharp: TextMeshProExt.cs
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;

public static class TextMeshProExt {
    public static string GetLink(this TextMeshProUGUI text, int index) {
        if (index == -1) {
            return null;
        }
        var linkInfo = text.textInfo.linkInfo[index];
        return linkInfo.GetLinkID();
    }
    
    public static int GetLinkIndex(this TextMeshProUGUI text) {
        var pos = SingleTouch.Default.Position;
        return TMP_TextUtilities.FindIntersectingLink(text, pos, text.canvas.GetCamera());
    }

    public static void ResetLinkColor(this TextMeshProUGUI text, int index, List<Color32[]> startColors) {
        if (index == -1 || startColors == null) {
            return;
        }
        TMP_LinkInfo linkInfo = text.textInfo.linkInfo[index];
        int underlineIndex = -1;
        for (int i = 0; i < linkInfo.linkTextLength; i++) {
            int characterIndex = linkInfo.linkTextfirstCharacterIndex + i;
            var charInfo = text.textInfo.characterInfo[characterIndex];
            int meshIndex = charInfo.materialReferenceIndex;
            int vertexIndex = charInfo.vertexIndex;

            Color32[] vertexColors = text.textInfo.meshInfo[meshIndex].colors32;
            if (charInfo.isVisible) {
                vertexColors[vertexIndex + 0] = startColors[i][0];
                vertexColors[vertexIndex + 1] = startColors[i][1];
                vertexColors[vertexIndex + 2] = startColors[i][2];
                vertexColors[vertexIndex + 3] = startColors[i][3];
            }

            // undlerLineが設定されてる場合のundlerLineの色を変更
            if (charInfo.isVisible && charInfo.underlineVertexIndex > 0 && charInfo.underlineVertexIndex != underlineIndex && charInfo.underlineVertexIndex < vertexColors.Length) {
                underlineIndex = charInfo.underlineVertexIndex;
                for (int j = 0; j < 12; j++) {
                    vertexColors[underlineIndex + j] = startColors[i][0];
                }
            }
        }

        text.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
    }

    public static List<Color32[]> ChangeLinkColor(this TextMeshProUGUI text, int index, Color32 color) {
        if (index == -1) {
            return null;
        }
        var oldVertexColors = new List<Color32[]>(); 
        var linkInfo = text.textInfo.linkInfo[index];
        int underlineIndex = -1;
        for (var i = 0; i < linkInfo.linkTextLength; i++) {
            var charIndex = linkInfo.linkTextfirstCharacterIndex + i;
            var charInfo = text.textInfo.characterInfo[charIndex];
            var meshIndex = text.textInfo.characterInfo[charIndex].materialReferenceIndex;
            var vertexIndex = text.textInfo.characterInfo[charIndex].vertexIndex;
            var verts = text.textInfo.meshInfo[meshIndex].colors32;
            oldVertexColors.Add(new Color32[] { verts[vertexIndex + 0], verts[vertexIndex + 1], verts[vertexIndex + 2], verts[vertexIndex + 3] });

            if (charInfo.isVisible) {
                verts[vertexIndex + 0] = color;
                verts[vertexIndex + 1] = color;
                verts[vertexIndex + 2] = color;
                verts[vertexIndex + 3] = color;
            }

            // undlerLineが設定されてる場合のundlerLineの色を変更
            if (charInfo.isVisible && charInfo.underlineVertexIndex > 0 && charInfo.underlineVertexIndex != underlineIndex && charInfo.underlineVertexIndex < verts.Length) {
                underlineIndex = charInfo.underlineVertexIndex;
                for (int j = 0; j < 12; j++) {
                    verts[underlineIndex + j] = color;
                }
            }
        }
        text.UpdateVertexData(TMP_VertexDataUpdateFlags.All);
        return oldVertexColors;
    }

}
```

```csharp: TextLinkGUI.cs
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;

/// <summary>
/// TextMeshProのComponentをアタッチしたGameObjectに設定することで、リンクタップ時にリンクの色を変えたりリンクの内容を取得できるようになります。
/// </summary>
[RequireComponent(typeof(TextMeshProUGUI))]
public class TextLinkGUI : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler {
    private TextMeshProUGUI _text;
    /// 変更したい色
    [SerializeField] private Color32 _hoverColor;
    /// リンクがタップされた際のアクション
    [SerializeField] private UnityEvent<string> _onClick;
    /// リンクが押された(まだ手を離してない)際のアクション
    [SerializeField] private UnityEvent<string> _touchDown;
    /// 指が離された際のアクション
    [SerializeField] private UnityEvent _touchUp;

    private bool _clicked = false;
    private bool _hovered = false;
    private int _index = -1;
    private List<Color32[]> _startColors;

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

    private void Reset() {
        if (_index != -1) {
            _text.ResetLinkColor(_index, _startColors);
        }

        _clicked = false;
        _hovered = false;
        _index = -1;
        _startColors = null;
    }

    public void OnDrag(PointerEventData eventData) {
        if (!_clicked) {
            return;
        }
        var index = _text.GetLinkIndex();
        if (index == _index && !_hovered) {
            _hovered = true;
            _text.ChangeLinkColor(_index, _hoverColor);
        }

        if (index == _index || !_hovered) {
            return;
        }
        
        _hovered = false;
        _text.ResetLinkColor(_index, _startColors);
    }

    public void OnPointerDown(PointerEventData eventData) {
        Reset();
        _index = _text.GetLinkIndex();
        if (_index == -1) {
            return;
        }
        _clicked = true;
        _hovered = true;
        _startColors = _text.ChangeLinkColor(_index, _hoverColor);
        var link = _text.GetLink(_index);
        _touchDown?.Invoke(link);
    }

    public void OnPointerUp(PointerEventData eventData) {
        _touchUp?.Invoke();
        if (_hovered && _index != -1) {
            var link = _text.GetLink(_index);
            _onClick?.Invoke(link);
        }
        Reset();
    }
}

```

# 参考
TextMeshPro hyperlinks
https://forum.unity.com/threads/textmeshpro-hyperlinks.1091296/