【Unity】オブジェクトの位置にUIを表示する

キャラクターの位置にUIを表示するにはどうすればいいの?

表示位置をUI座標に変換してから、その位置にUIを移動させれば良いわ。

uGUIのUI要素をオブジェクトの位置に表示させる方法の紹介です。

シーンに配置されているオブジェクトとuGUIオブジェクトは座標系が異なるため、オブジェクトの位置をそのままUI座標に指定しても正しく表示されません。

オブジェクトと見かけ上同じ位置にUIを表示するためには、

オブジェクトのワールド座標→スクリーン座標→TransformRectのローカル座標

の順に座標変換する必要があります。

この時、カメラ背後に位置するオブジェクトもスクリーン座標に投影されるため、背後のものを非表示にしたい場合は前後判定を行う必要があります。

本記事では、このようなオブジェクトの位置にuGUIのUIを表示する方法について解説します。

この作品はユニティちゃんライセンス条項の元に提供されています

動作環境
  • Unity2021.1.21f1

想定する状況

次のようにシーン内のキャラクターの位置にUIを表示することを想定します。

また、キャラクターの位置に表示するためにはuGUIのCanvasが必要ですので、配置されていない場合は配置してください。

Canvasの下に次のようなUIマーカーを表示することを例にとって解説していきます。

なお、上記ゲージの表示部分には次のアセットを使用させていただきました。

オブジェクトの座標変換

オブジェクトの位置をUIのローカル座標に変換するためには、

  • オブジェクトのワールド座標→スクリーン座標変換
  • スクリーン座標→UIローカル座標変換

という2つの変換処理を行う必要があります。

ワールド座標→スクリーン座標変換

オブジェクトのワールド座標をスクリーン座標に変換する処理は次のようになります。

// オブジェクト
Transform target;
// オブジェクトを映すカメラ
Camera targetCamera;

・・・(中略)・・・

// オブジェクトのワールド座標
var targetWorldPos = target.position;

// ワールド座標をスクリーン座標に変換する
var targetScreenPos = targetCamera.WorldToScreenPoint(targetWorldPos);

スクリーン座標への変換にはCameraクラスのWorldToScreenPoint()メソッドを使います。

public Vector3 WorldToScreenPoint(Vector3 position);

引数には変換元のワールド座標を指定します。

スクリーン座標→UIローカル座標変換

スクリーン座標からUIのローカル座標に変換する処理は次のようになります。

RectTransform targetUI;

・・・(中略)・・・

RectTransform parentUI = targetUI.parent.GetComponent<RectTransform>();

// スクリーン座標→UIローカル座標変換
RectTransformUtility.ScreenPointToLocalPointInRectangle(
    parentUI,
    targetScreenPos,
    null, // オーバーレイモードの場合はnull
    out var uiLocalPos
);

座標変換には次のメソッドを使います。

public static bool ScreenPointToLocalPointInRectangle(
    RectTransform rect,
    Vector2 screenPoint,
    Camera cam,
    out Vector2 localPoint
);

これは、指定されたスクリーン座標を指定されたRectTransformオブジェクトのローカル座標に変換するメソッドです。

第1引数には、変換先のRectTransformローカル座標の親を指定します。

第2引数には、変換元のスクリーン座標を指定します。

第3引数には、Canvasに関連するカメラを指定します。Canvasがオーバーレイモード [1] 場合はnullを指定しなければいけません。

第4引数には、RectTransformのローカル座標を受け取るための変数を指定します。

背後にオブジェクトがある場合への対処

ここまでの変換処理でUIをオブジェクトの位置の表示させると、次のように背後にオブジェクトがあるときもUIが表示されてしまいます。

これは、ワールド座標をスクリーン座標に変換するとき、カメラ背後の位置も画面上に投影されてしまうためです。

そのため、カメラ背後のオブジェクトを非表示にしたい場合、前後判定処理を行う必要があります。

前後判定の計算方法

前後判定は、カメラの向きベクトルカメラからオブジェクトへのベクトルとの内積から判定できます。

内積計算式
\vec{n} \cdot (P-C)

\vec{n} : カメラの向きベクトル

C : カメラの位置

P : オブジェクトの位置

内積の値が正の場合はカメラの前方負の場合はカメラの背後0の場合はカメラの真横に位置していることになります。

位置と内積の関係

したがって、内積の値が0より大きいときのみUIを表示するようにすれば良いです。

// カメラの向き
var cameraDir = camera.forward;
// カメラからオブジェクトへの向き
var targetDir = targetWorldPos - camera.position;

// 両者の向きベクトルの内積が正のとき、
// オブジェクトはカメラ前方に位置していると判断できる
var isFront = Vector3.Dot(targetDir, cameraDir) > 0;

// 前方のときのみオブジェクトを表示する
targetUI.gameObject.SetActive(isFront);

2つのベクトルの内積は、Vector3.Dot()メソッドから計算できます。

public static float Dot(Vector3 lhs, Vector3 rhs);

今回の場合、内積を使うとシンプルに実装できるわ。しかも内積は計算がとても軽いというメリットもあるの。

サンプルスクリプト

指定されたオブジェクトの位置にUIを表示させるサンプルです。カメラ背後にオブジェクトがある場合に非表示にする処理も入っています。

ObjectMarker.cs
using UnityEngine;

public class ObjectMarker : MonoBehaviour
{
    // オブジェクトを映すカメラ
    [SerializeField] private Camera _targetCamera;

    // UIを表示させる対象オブジェクト
    [SerializeField] private Transform _target;

    // 表示するUI
    [SerializeField] private Transform _targetUI;

    // オブジェクト位置のオフセット
    [SerializeField] private Vector3 _worldOffset;

    private RectTransform _parentUI;

    // 初期化メソッド(Prefabから生成する時などに使う)
    public void Initialize(Transform target, Camera targetCamera = null)
    {
        _target = target;
        _targetCamera = targetCamera != null ? targetCamera : Camera.main;

        OnUpdatePosition();
    }

    private void Awake()
    {
        // カメラが指定されていなければメインカメラにする
        if (_targetCamera == null)
            _targetCamera = Camera.main;

        // 親UIのRectTransformを保持
        _parentUI = _targetUI.parent.GetComponent<RectTransform>();
    }

    // UIの位置を毎フレーム更新
    private void Update()
    {
        OnUpdatePosition();
    }

    // UIの位置を更新する
    private void OnUpdatePosition()
    {
        var cameraTransform = _targetCamera.transform;

        // カメラの向きベクトル
        var cameraDir = cameraTransform.forward;
        // オブジェクトの位置
        var targetWorldPos = _target.position + _worldOffset;
        // カメラからターゲットへのベクトル
        var targetDir = targetWorldPos - cameraTransform.position;

        // 内積を使ってカメラ前方かどうかを判定
        var isFront = Vector3.Dot(cameraDir, targetDir) > 0;

        // カメラ前方ならUI表示、後方なら非表示
        _targetUI.gameObject.SetActive(isFront);
        if (!isFront) return;

        // オブジェクトのワールド座標→スクリーン座標変換
        var targetScreenPos = _targetCamera.WorldToScreenPoint(targetWorldPos);

        // スクリーン座標変換→UIローカル座標変換
        RectTransformUtility.ScreenPointToLocalPointInRectangle(
            _parentUI,
            targetScreenPos,
            null,
            out var uiLocalPos
        );

        // RectTransformのローカル座標を更新
        _targetUI.localPosition = uiLocalPos;
    }
}

使い方

表示対象の親UIにスクリプトをアタッチします。大本のCanvasでも問題ありません。

例ではヒエラルキーのPanelを表示対象とし、その親オブジェクトMarkerにスクリプトをアタッチしています。

そして、赤枠の項目を一通り設定します。PrefabをInstantiateして生成する場合は、Target Camera、Targetはそのまま設定できないため、Initializeメソッド経由で初期化するようにします。

[SerializeField] private RectTransform _markerPanel;
[SerializeField] private ObjectMarker _markerPrefab;

・・・(中略)・・・

var marker = Instantiate(_markerPrefab, _markerPanel);
marker.Initialize(target);

実行結果

カメラ前方にあるオブジェクトだけが描画されるようになりました。

さいごに

オブジェクトの見かけ上の位置にUIを表示する方法を紹介しました。座標変換を駆使することで実現可能です。

カメラに映らないオブジェクトに対するUIを非表示にする方法は、本記事で紹介した以外にも存在します。例えば、ある距離以上離れたオブジェクトを非表示にしたい場合、今回の方法では対処できず距離判定処理を加える必要があります。

この辺りの実装は、開発するコンテンツに合わせて適宜判断してください。

参考サイト