TextMeshProでリンクを作成する方法とそのバグの解消法

UIで文字を表示させたりするのに、最近ではTextMeshPro(テキストメッシュプロ)がデフォルトになって久しいですね。
通常のTextはLegacyになってしまいましたね。

さて、このTMP、ご存知の方も多いと思いますがHTMLみたいに、いろんなタグが使えたりします。
今回はその中でリンクを生成するLinkタグについて紹介したいと思います。
ちょっとしたハマりポイントもあるのでそちらについても触れていきたいと思います。

Unityスキルアップ、
始めるなら今

パズル、脱出、RPG...目標のゲームを完成させよう!

人気のUnity講座はこちら

Udemy講座

TMPで普通にリンクを作る

既にTMPをインストールされている前提でお話します。
まず普通にText(TMP)を作成しましょう。
こんな感じですかね。
テキストメッシュプロを作成

Linkタグを使ってみる

ではテキストにLinkタグをつけてみます。
例えばGoogleへのリンクをLinkタグで実装してみましょう。


<link="https://www.google.com/"><u>Google<u></link>

このような感じになります。
HTMLのaタグのように自動で下線は引かれませんので、下線が必要な場合はuタグで囲むか、FontStyleのUを選択する必要があります。
TMPインスペクター

イベントを付与する

これで文字をクリックすればGoogleのページが開かれるかというとそうはなりません。
スクリプトでクリックイベントを設定する必要があります。
Unityのイベントシステムを使用して、IPointerClickHandlerインターフェースを実装し、OnPointerClickメソッドを定義することで、クリックイベントを設定します。
これにより、リンクがクリックされた際に特定の処理を実行することができます。
では具体的にやってみます。

スクリプトの作成

今回はTMPLinkというC#ファイルを作りその中に処理を書いていきたいと思います。
作成したスクリプトはCanvasにアタッチしておきました。
それでは中身を書いていきましょう。

基本のスクリプト

まずクリックイベント設定する必要最低限なコードを書きます。


using UnityEngine;
using UnityEngine.EventSystems;
using TMPro;
public class TMPLink : MonoBehaviour,IPointerClickHandler
{
    [SerializeField] TextMeshProUGUI tmpText = default;

    public void OnPointerClick(PointerEventData eventData)
    {
    }
}

必要な名前空間、そしてTMPをSerializeFieldで取得しておきました。インスペクタからTextMeshProUGUIコンポーネントを簡単に割り当てることができます。

またIPointerClickHandlerインターフェースを実装します。
このインターフェースはUnityのイベントシステムの一部で、オブジェクトがクリック可能であることを示し、クリックイベントを処理する方法を定義します。
そしてインターフェースを実装することで必要となるOnPointerClickメソッドを定義しています。
現在このメソッドは空ですが、これから実装を追加していきます。
このメソッド内でクリックされた時の動作を定義することになります。

OnPointerClickの処理

ではリンク文字をクリックする実際の処理をOnPointerClickメソッド内に記述していきます。


    /* 略 */
    public void OnPointerClick(PointerEventData eventData)
    {
        int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmpText, eventData.position, eventData.pressEventCamera);
        if(linkIndex != -1)
        {
            TMP_LinkInfo linkInfo = tmpText.textInfo.linkInfo[linkIndex];
            Application.OpenURL(linkInfo.GetLinkID());
        }
    }

まずこのメソッドではPointerEventData型のeventDataを引数に取っています。
そして最初のTMP_TextUtilities.FindIntersectingLinkですが、これはTMPのライブラリの一部で、クリックされた位置にリンクがあるかどうかを判定し、そのリンクのインデックスを返すメソッドです。
引数は3つあり、意味はそれぞれ以下の通りです。

  1. 検査対象の TMP_Text オブジェクト
  2. スクリーン上のクリック位置(Vector3)
  3. 現在のカメラ(通常はイベントカメラ)

です。
今回の場合ですと、第一引数がtmpTextそのものになります。
第二引数のeventData.position は、ユーザーが入力を行ったスクリーン上の位置を示す Vector2 値です。
この位置は、スクリーン座標系(左下が原点)に基づいており、例えば、マウスクリックやタッチ入力が行われた位置を示します。
このプロパティを使用することで、ユーザーがどこでクリックやタッチを行ったかを知ることができます。
第三引数のeventData.pressEventCamera は、イベントが発生したときに使用されたカメラを示します。
このカメラを使用して、スクリーン座標をワールド座標に変換することができます。

そして処理の流れですが以下の通りです。

  1. テキスト内のリンク情報を取得
  2. クリック位置をテキストのローカル座標系に変換
  3. 各リンクの範囲とクリック位置が交差するかチェック
  4. 交差するリンクが見つかった場合、そのインデックスを返す(ない場合は-1を返す)
  5. インデックスが-1ではなければ、そのインデックスのlinkInfoを取得
  6. 取得したLinkInfoのLinkIDを取得しApplication.OpneURLでリンクを開く

という感じです。
それでは試しにクリックしたときにどのようなlinkIndexが返ってくるかDebug.Logで確認してみましょう。
TMPの中身を以下のように修正しました。


私はTanis Games です。

スクリプトの方もifブロックをコメントアウトし、リンクが開かないようにしておき、Debug.Logを追加しました。


    public void OnPointerClick(PointerEventData eventData)
    {
        int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmpText, eventData.position, eventData.pressEventCamera);
        Debug.Log($"linkIndex = {linkIndex}");
        /*if(linkIndex != -1)
        {
            TMP_LinkInfo linkInfo = tmpText.textInfo.linkInfo[linkIndex];
            Application.OpenURL(linkInfo.GetLinkID());
        }*/
    }

では実際にログを確認してみましょう。
リンクのログ
この様にリンクではない文字の部分では-1が、そしてLinkで囲まれている文字をクリックすると0が返ってくるのがわかるかと思います。
この値を使いリンク部分のクリックか、そうではないかを判断し、分岐させています。

では続いてリンク部分をクリックした時のlinkInfoの中身を確認してみましょう。
先程のコメントアウトを外し、Debug.Logを追加します。


    public void OnPointerClick(PointerEventData eventData)
    {
        int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmpText, eventData.position, eventData.pressEventCamera);
        if(linkIndex != -1)
        {
            TMP_LinkInfo linkInfo = tmpText.textInfo.linkInfo[linkIndex];
            Debug.Log($"linkId {linkInfo.GetLinkID()} / linkText {linkInfo.GetLinkText()}");
            Application.OpenURL(linkInfo.GetLinkID());
        }
    }

LinkIdとText
このような感じでGetLinkIDメソッドでlink=”◯◯”の◯◯の部分が、GetLinkTextメソッドでLinkタグで囲まれている文字列が取得できました。
最後にApplication.OpenURLメソッドを利用しlinkInfo.GetLinkID()内のURLに遷移します。
それでは実際にクリックをしたら指定したURLをブラウザで開いてくれるかテストしてみましょう。
LINKのテスト
このようにちゃんと指定したURLを開いてくれることが確認できました。

Linkタグのバグに関して

この様にLinkタグを用い、イベントを発生させることで指定したURLを開くことができましたが、実はこのLinkタグには問題があります。
これに気がついたのは、日本語を含むURLをエンコードして長いURLをLinkにしたときでした。
実はLinkタグは120文字以上のIdになると正常に機能しなくなります。
実際のその様子をお見せします。
120文字以上はエラー
いかがでしょう。文字数が増えるとダブルクォーテーションで囲まれた部分が文字として表示されてしまいました。
この様にタグが意図しない挙動を起こしてしまうことがあります。

発生する環境や解消方法も様々だと思いますが、今回は冒頭で述べたように日本語が含まれている文字をエンコードしたURLを想定して解消していきたいと思います。
今回はGoogleでUnity ◯◯ 作り方という検索をすると仮定してみましょう。

  1. パズル
  2. ロールプレイング
  3. 戦国シミュレーション

この3つのジャンルでそれぞれリンクを作成していきたいと思います。
検索するURLの雛形は以下のような感じです。
https://www.google.com/search?q=(エンコードしたキーワード)
ちなみにこのパターンでそれぞれの文字をエンコードした場合の文字数は、
パズル:109文字
ロールプレイング:154文字
戦国シミュレーション:172文字

です。
パズル以外はTMPのLinkの文字数上限からオーバーしてしまいますね。
しかしたとえオーバーしてしまっても見た目が崩れること無く正常にLinkタグが動くように設定していきたいと思います。

雛形設定

まずUnityエディターのTMPのテキストを以下のようにします。
TMPのリンク

するとGame上ではこんな感じになっています。
ゲーム上の見た目
ちなみにuタグをlinkタグの内側に入れないのは検索ワードにuタグも含まれてしまうので外側に記述しています。

ContentSizeFitterをアタッチ

続いてこのTMPの高さが50になっていますので、高さを文字全体にしたいと思います。
RectTransformのHeightを調整しても良いのですが、検索したいジャンルが増えた場合、いちいち高さを調整するのがめんどくさいのでContentSizeFitterをアタッチしVertical FitをPreferrd Sizeにします。
全体の縦位置は作っているUIに合わせて調整してください。
TMPの高さ調整

スクリプト修正

続いてスクリプトの方を修正していきます。
今回はlinkで囲まれた文字列を取得してエンコードし、URLとして整形していきます。
以下のようになりました。


using UnityEngine;
using UnityEngine.EventSystems;
using System.Collections.Generic;
using TMPro;
public class TMPLink : MonoBehaviour,IPointerClickHandler
{
    [SerializeField] TextMeshProUGUI tmpText = default;

    const string BASEURL = "https://www.google.com/search?q=";


    public void OnPointerClick(PointerEventData eventData)
    {

        int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmpText, Input.mousePosition, eventData.pressEventCamera);
        
        if(linkIndex != -1)
        {
            TMP_LinkInfo linkInfo = tmpText.textInfo.linkInfo[linkIndex];
            string param = UnityEngine.Networking.UnityWebRequest.EscapeURL("unity " + linkInfo.GetLinkText() + " 作り方") ;
            string searchUrl = $"{BASEURL}{param}";
            Application.OpenURL(searchUrl);
        }
    }
}

クリックされたlinkのIndexを取得して、linkタグで囲まれている文字を取得して検索するパラーメータとしてのエンコードを行い、検索用URLの雛形として定義したBASEURLとつなげてsearchUrlにしています。
最後にApplication.OpneURLでsearchUrlを渡して検索しています。
では実際にちゃんと検索されているか確認してみましょう。

検索結果

いかがでしょうか?
この様に文字数がオーバーしてもインデックスさえわかればlinkにIDを設定する必要がなく、長いURLでも問題なく遷移することができます。
またIDを使いたい場合はDictionaryであらかじめキー(検索したい文字)と値(実際の検索URL)を保持しておいて、クリック時にIDから値を取得することもできます。
今回はシンプルな実装でしたが色々試していただければと思います。
またLinkタグはURL以外でもゲーム内の内部ナビゲーションやダイアログ表示、アクションのトリガーなどにも使えますので興味のある方は是非調べて使ってみてください。

関連記事

関連記事はまだありません

最後までご覧頂いてありがとうございました。

セール中!
2Dローグライトゲーム

2Dローグライトゲーム開発

Unityヴァンパイアサバイバーズ風講座