【Unity】オブジェクトの頭上に情報を表示する(基本)

2024年10月15日

様々な方法がありますが、今回は、UI(TMPro)オブジェクトにスクリプトをアタッチすることで、3Dオブジェクトの頭上に情報を表示する基本的な方法について解説します。

考え方

3DオブジェクトのプレイヤーとUIオブジェクトは異なる座標系を使用しているため、位置を適切に変換してUIオブジェクトをプレイヤーの頭上に表示する必要があります。これにより、UIオブジェクトが常にプレイヤーの頭上に表示されるように見せることができます。

サンプルシーン構成

以下の構成でプロジェクトを作成します。

  • プロジェクト名:LifeGaugeSample
  • プレイヤーオブジェクト:プレイヤーのモデルやキャラクター
  • ライフゲージオブジェクト:プレイヤーの頭上に常に表示されるUIオブジェクト(例:テキストや画像)

プレイヤーオブジェクト

プレイヤーオブジェクトは、シーン内で動くキャラクターやモデルです。このオブジェクトの頭上にUIが常に表示されるように設定します。

ライフゲージオブジェクト

ライフゲージオブジェクトには、テキストUI(TMPro)を使用します。このオブジェクトをUIのローカル座標系で移動させ、常にプレイヤーの頭上に留まるようにします。テキスト以外のUI要素(画像やボタンなど)でも同様の手法が適用できます。

スクリプト

ライフゲージオブジェクトにアタッチするスクリプトを作成します。このスクリプトでは、以下の2つのアウトレットをインスペクターから接続します。

  1. UIテキストの親オブジェクト(Canvas)
  2. 追従するプレイヤーオブジェクト(Cubeなど)

注意点

  • それぞれの型はGameObject型ではなく、具体的なコンポーネント型(RectTransformTransform)である必要があります。これにより、ゲームオブジェクトにアタッチされているコンポーネントのインスタンスを取得できます。

以下は、スクリプトのサンプルです。

using UnityEngine;

// プレイヤーのライフゲージを表示するクラス
public class LifeGauge : MonoBehaviour
{
    // 親のRectTransformを指定します。
    [SerializeField]
    private RectTransform parentRectTransform;

    // プレイヤーのTransformを指定します。
    [SerializeField]
    private Transform playerTransform;

    // フレームごとに呼び出されるUpdateメソッド
    void Update()
    {
        // プレイヤーの位置をスクリーン座標に変換します。
        Vector3 screenPoint = Camera.main.WorldToScreenPoint(playerTransform.position);

        // スクリーン座標をローカル座標に変換します。
        RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, screenPoint, null, out Vector2 localPoint);

        // ゲージの位置をプレイヤーの頭上に設定します(Y座標を50ピクセル上に設定)。
        transform.localPosition = localPoint + new Vector2(0, 50);
    }
}

コード解説

RectTransform の役割

RectTransformは、UnityのUIシステムで使用されるコンポーネントで、UI要素(テキスト、画像、ボタンなど)の位置、サイズ、回転、スケールを制御します。ライフゲージの親オブジェクト(Canvas)のRectTransformを参照することで、UI内での正確な位置調整が可能です。

[SerializeField]
private RectTransform parentRectTransform;

parentRectTransformはプライベートな変数で、[SerializeField]属性によりインスペクターから設定可能です。この変数はライフゲージを配置する親のRectTransformを参照します。

ワールド座標からスクリーン座標への変換

プレイヤーの位置を3D空間のワールド座標から2Dスクリーン座標に変換します。これにより、UI上で正確な位置に表示することが可能です。

Vector3 screenPoint = Camera.main.WorldToScreenPoint(playerTransform.position);

Camera.mainを使用して、プレイヤーの位置をスクリーン座標に変換し、その結果をscreenPointに格納します。

ワールド座標

3D空間を指す座標

スクリーン座標

UI(Canvas)での2次元座標
左下が原点とした位置を示します

プレイヤーとスクリーン座標

Camera.mainを使用して、playerTransformの位置をワールド座標からスクリーン座標に変換し、その座標をscreenPointに格納します。これにより、キャラクターの位置情報を画面上の座標に変換できます。

スクリーン座標から親(Canvas)の子オブジェクト(Text(TMP))としてのローカル座標系に変換

ライフゲージのText(TMP)が子オブジェクトなので、続いてローカル座標系に変換します

screenPointを parentRectTransform のローカル座標系に変換します。結果は localPoint に格納されます。これにより、親RectTransform内での位置を取得できます。

RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, screenPoint, null, out Vector2 localPoint);

RectTransformUtility.ScreenPointToLocalPointInRectangle メソッドは、UnityのUIシステムで使用される、スクリーン座標から指定された親要素のローカル座標系への変換を行うための便利なメソッドです。このメソッドはUI要素の操作やイベント処理の際に非常に役立ちます。

以下に、このメソッドの各引数と役割を説明します:

  1. parentRectTransform: これは、ローカル座標系に変換するための親要素の RectTransform コンポーネントを指定するパラメータです。スクリーン座標からローカル座標に変換する際、この親要素の座標系を基準として変換が行われます。
  2. screenPoint: screenPoint は、スクリーン上での座標を指定するパラメータです。通常、マウスの位置やタッチイベントの位置など、デバイスの画面上での座標を指定します。
  3. canvas (null): canvas パラメータは、特定の Canvas グループに関連する場合に指定されます。通常、これをnullに設定し、screenPoint と parentRectTransform の間の変換を行います。ただし、特定の Canvas グループに関連付けられている場合、それを指定できます。
  4. localPoint: localPoint パラメータは、メソッドの出力として使用されます。メソッドが成功した場合、localPoint に変換されたローカル座標が格納されます。この座標は、指定された parentRectTransform 内での位置を示します。

このメソッドは、スクリーン上の座標(screenPoint)を、指定した親要素のローカル座標系に変換するのに使用されます。ローカル座標系の原点はCanvasの中心地点です。例えば、マウスカーソルの位置を取得し、それをUI要素のローカル座標系内で使用したい場合に便利です。このメソッドを使用することで、UI要素の位置に関する計算を簡略化し、正確な位置情報を得ることができます。

アンカーを中心とした時の座標系のイメージがローカル座標系のイメージになります
実際は、アンカーの位置を変えてもローカル座標系の原点には反映されません

UnityでUI要素(例えば、ライフゲージ)を3Dオブジェクト(プレイヤー)の頭上に表示する際、RectTransformUtility.ScreenPointToLocalPointInRectangle メソッドを使用してスクリーン座標をUIのローカル座標系に変換します。このときのローカル座標系の中心位置(原点)がどこに設定されているかは、UIの配置や設定において重要なポイントです。

ローカル座標系の原点とは?

ローカル座標系の原点は、特定のRectTransformコンポーネントにおける基準点のことを指します。この原点は、主に以下の2つのプロパティによって決定されます。

  1. アンカー(Anchor)
  2. ピボット(Pivot)

ピボット(Pivot)の役割

RectTransformのローカル座標系における原点は、ピボットポイントによって決定されます。ピボットは、UI要素の回転やスケーリングの基準点として機能するだけでなく、ローカル座標系の原点としても使用されます。

  • デフォルト設定: 通常、RectTransformのピボットは (0.5, 0.5) に設定されており、これはUI要素の中心を意味します。
  • カスタマイズ: ピボットを変更することで、ローカル座標系の原点をUI要素の異なる位置(例えば、左下や右上)に移動させることができます。

この場合の原点位置

今回のスクリプトおよび設定において、以下のように考えることができます。

  1. RectTransform(Canvas)のピボット:
    • 通常、CanvasのRectTransformのピボットは (0.5, 0.5) であり、Canvasの中心がローカル座標系の原点となります。
    • これにより、RectTransformUtility.ScreenPointToLocalPointInRectangle メソッドで変換されたlocalPointは、Canvasの中心を基準とした相対位置となります。
  2. RectTransform(ライフゲージ)のローカル座標:
    • ライフゲージのRectTransformのピボットも通常は (0.5, 0.5) です。
    • スクリプト内で設定されるtransform.localPositionは、親Canvasのローカル座標系における位置であり、ピボットを基準とした相対位置になります。

図解による説明

以下に、ローカル座標系の原点位置を視覚的に説明します。

Canvas(親 RectTransform)
+---------------------------+
|                           |
|          (0,0)            |  ← ピボット(中心)がローカル座標系の原点
|           +               |
|                           |
|        ライフゲージ         |
|                           |
+---------------------------+
  • Canvasの中心(0,0) の原点となります。
  • RectTransformUtility.ScreenPointToLocalPointInRectangle によって取得されたlocalPointは、この原点を基準に計算されます。
  • スクリプトでは、localPointにオフセット(例えば、new Vector2(0, 50))を加えることで、ライフゲージがプレイヤーの頭上に適切に配置されます。

ピボットの変更がローカル座標系に与える影響

ピボットを変更すると、ローカル座標系の原点も移動します。例えば、ピボットを左下に設定すると、ローカル座標系の原点はCanvasの左下に移動します。この設定により、localPointの基準が変わるため、ライフゲージの表示位置も相対的に変動します。

例: ピボットを左下に設定

Canvas(親 RectTransform)
+---------------------------+
|                           |
| (0, Canvas.height)        |  ← ピボットが左下に移動
|           +               |
|                           |
|        ライフゲージ         |
|                           |
+---------------------------+

この場合、localPointは左下を基準として計算されるため、スクリプト内でのオフセット調整もそれに合わせて行う必要があります。

まとめ

  • ローカル座標系の原点は、RectTransformピボットポイントに基づいて決定されます。
  • 通常、Canvasのピボットは中央に設定されており、これがローカル座標系の原点となります。
  • ピボットを変更することで、ローカル座標系の原点も変更され、UI要素の配置やスクリプト内での位置調整に影響を与えます。
  • スクリプトを使用してUI要素をプレイヤーの頭上に表示する際は、親Canvasのピボット設定を確認し、それに基づいて位置調整を行うことが重要です。

これにより、UI要素が期待通りに表示され、プレイヤーの動きに合わせて正確に追従するようになります。

ライフゲージの位置設定

取得したローカル座標にオフセットを加算して、ライフゲージをプレイヤーの頭上に表示します。

transform.localPosition = localPoint + new Vector2(0, 50);

ここでは、Y軸方向に50ピクセルのオフセットを加えています。この値を調整することで、ライフゲージの表示位置を微調整できます。

このスクリプトは、ゲームオブジェクトにアタッチされた際に、指定された親RectTransform内でキャラクターの位置に合わせてライフゲージを表示するのに役立ちます。

ここでのコードでは以下の行が使用されています:

transform.localPosition = localPoint + new Vector2(0, 50);

これに対して、「なぜ RectTransform.localPosition ではなく transform.localPosition を使用しているのか」ですが、具体的には、このスクリプトがアタッチされているオブジェクトがCanvas下のTextMeshPro子オブジェクトであることを踏まえています。

1. TransformRectTransform の関係

まず、TransformRectTransform の関係を明確にしましょう。

  • Transform:
    • Unityのすべてのゲームオブジェクトは Transform コンポーネントを持っています。
    • オブジェクトの位置、回転、スケールを管理します。
  • RectTransform:
    • Transform のサブクラスであり、主にUI要素(Canvasの子オブジェクトなど)に使用されます。
    • UIのレイアウトやアンカー、ピボットなどの追加機能を提供します。

重要な点は、RectTransformTransform を継承しているということです。つまり、RectTransformTransform のすべてのプロパティとメソッドを持ちながら、さらにUI特有の機能を追加しています。

2. なぜ transform.localPosition で問題ないのか

RectTransformTransform を継承しているため、transform.localPosition を使用しても機能的には問題ありません。具体的には以下のようになります。

// RectTransformがTransformを継承しているため、localPositionは使用可能
transform.localPosition = localPoint + new Vector2(0, 50);

この場合、transform は実際には RectTransform コンポーネントへの参照となります。したがって、localPosition を設定することで、UI要素の位置を変更することができます。

3. RectTransform を明示的に使用する利点

技術的には transform.localPosition でも動作しますが、コードの可読性と意図の明確化の観点から、RectTransform を明示的に使用することが推奨されます。以下にその理由を示します。

  • 可読性の向上:
    • 他の開発者がコードを読んだ際に、このオブジェクトがUI要素であることが明確になります。
  • UI特有のプロパティへのアクセス:
    • RectTransform には anchoredPositionsizeDelta など、UIレイアウトに特化したプロパティが存在します。これらを使用する場合、RectTransform を明示的に扱う方が自然です。
  • 一貫性:
    • プロジェクト全体で RectTransform を明示的に使用することで、一貫したコードスタイルを維持できます。

具体例:

以下のように RectTransform をキャッシュして使用する方法が考えられます。

using UnityEngine;

public class LifeGauge : MonoBehaviour
{
    [SerializeField]
    RectTransform parentRectTransform;

    [SerializeField]
    Transform playerTransform;

    // RectTransformをキャッシュする
    RectTransform rectTransform;

    void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
    }

    void Update()
    {
        Vector3 screenPoint = Camera.main.WorldToScreenPoint(playerTransform.position);

        RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, screenPoint, null, out Vector2 localPoint);

        Debug.Log("screenPoint: " + screenPoint);
        Debug.Log("localPoint: " + localPoint);
        
        // RectTransformを使用
        rectTransform.localPosition = localPoint + new Vector2(0, 50);
    }
}

4. anchoredPosition の使用を検討する

UI要素の位置を設定する際には、localPosition よりも anchoredPosition を使用する方が適切な場合があります。anchoredPosition はアンカーに基づいた位置を設定するため、レイアウトの柔軟性が向上します。

rectTransform.anchoredPosition = localPoint + new Vector2(0, 50);

まとめ

  • transform.localPosition は技術的に問題なく使用可能:
    • RectTransformTransform を継承しているため、transform.localPosition を使用しても機能します。
  • RectTransform を明示的に使用することを推奨:
    • コードの可読性と意図の明確化のために、RectTransform を使用する方が望ましいです。
    • UI特有のプロパティ(例: anchoredPosition)を使用する際にも便利です。
  • 実装例の改善:
    • RectTransform をキャッシュし、anchoredPosition を使用することで、より柔軟で明確なコードになります。

元のコードで transform.localPosition を使用している理由は、おそらく単純に Transform がベースクラスであり、機能的には問題がないためです。しかし、将来的な拡張性や可読性を考慮すると、RectTransform を明示的に使用する方がベストプラクティスといえます。

発展(インスペクターで頭上位置の調整ができるようにする)

次のようにリファクタリングすることで、プレイヤーと表示されるUIとの位置関係の調整をインスペクターから調整できるようになります

using UnityEngine;

public class LifeGauge : MonoBehaviour
{
    [SerializeField] private RectTransform parentRectTransform;
    [SerializeField] private Transform playerTransform;
    [SerializeField] private Vector2 offset = new Vector2(0, 50);

    private void Update()
    {
        UpdateLifeGaugePosition();
    }

    private void UpdateLifeGaugePosition()
    {
        Vector3 screenPoint = Camera.main.WorldToScreenPoint(playerTransform.position);
        RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, screenPoint, null, out Vector2 localPoint);
        transform.localPosition = localPoint + offset;
    }
}

変更点の説明

  1. オフセットフィールドの導入csharpコードをコピーする[SerializeField] private Vector2 offset = new Vector2(0, 50); offsetフィールドを追加し、UIの表示位置をインスペクターから調整できるようにしました。これにより、表示位置の微調整が容易になります。
  2. 位置更新処理の分離csharpコードをコピーするprivate void UpdateLifeGaugePosition() { // 位置更新の処理 } 位置更新のコードをUpdateLifeGaugePositionメソッドに切り出し、Updateメソッドをシンプルに保ちました。これにより、コードの可読性と保守性が向上します。

利点

保守性の向上:将来的な変更や拡張が容易になります。保守性が向上しました。また、オフセットの調整が簡単になり、コードの変更が効率的に行えるようになります。

柔軟な調整:インスペクターからオフセット値を変更できるため、ライフゲージの表示位置を簡単に調整できます。

コードの整理:位置更新処理をメソッドに分離することで、コードが整理され、理解しやすくなります。

Unity

Posted by hidepon