オブジェクトの頭上に情報を表示する

2022年9月20日

3Dオブジェクトの頭上にステータスなどの情報を表示できるようにしましょう
2つの方法を紹介します
1つ目は、シーン上にCanvasを1つだけ作り、その子オブジェクトとして表示UIを追加していく方法です
2つ目は、オブジェクト毎にCanvasを子オブジェクトとして作っていく方法です

では、それぞれ見ていきましょう

UIの共通Canvas上にメッセージを表示

3Dオブジェクトの座標(ワールド座標)をCanvas用の2D座標(スクリーン座標)に変換してUI上にテキストを表示します

実行イメージ

シーンの構成

WorldToScreenSampleでプロジェクトを作成します

メッセージを頭上に表示するUIのTextオブジェクトの作成

作成したら、Prefabにしておきます
作成した時のオブジェクトは消しておきます

Canvasの設定を変更

スクリーンのサイズを設定しておきます

メッセージを頭上に表示したいオブジェクトを作成

Cubeを作成し、スクリプトをアタッチします
インスペクターで、メッセージを表示させるためのCanvasとメッセージのTextのPrefabをそれぞれアウトレット接続します

スクリプト

メッセージ用

メッセージにアタッチして、表示場所を決定する役割を持ちます

using UnityEngine;

public class OverHeadMsg : MonoBehaviour
{
    public Transform targetTran;

    void Update()
    {
        transform.position = RectTransformUtility.WorldToScreenPoint(
             Camera.main,
             targetTran.position + Vector3.up);
    }
}
説明
public Transform targetTran;

メッセージを表示するゲームオブジェクトを代入するためのフィールドです
代入されたオブジェクトをメッセージが追従します

transform.position = RectTransformUtility.WorldToScreenPoint(
             Camera.main,
             targetTran.position + Vector3.up);

ワールド座標(3Dオブジェクトの座標)からスクリーンの座標に変換して、そこに移動します
第1引数にカメラオブジェクトを渡します
第2引数にメッセージを表示したいオブジェクトの座標を渡します
今回は、頭上に表示したいのでY軸方向に1上げた場所を指定します。
Vector3.upはnew Vector3(0, 1, 0)を同じです
ここは調整可能です
例えば、上に0.5上げたければ、vector3.up * 0.5とすればいいです

メッセージオブジェクト作成用

Cubeにアタッチして、メッセージの生成を担当します

using UnityEngine;

public class OverHeadMsgCreater : MonoBehaviour
{
    [SerializeField]
    RectTransform canvasRect;

    [SerializeField]
    OverHeadMsg overHeadMsgPrefab;

    OverHeadMsg overHeadMsg;

    void Start()
    {
        overHeadMsg = Instantiate(overHeadMsgPrefab, canvasRect);
        overHeadMsg.targetTran = transform;
    }
}

説明

[SerializeField]
RectTransform canvasRect;

メッセージのTextを保持させたい親Canvasを登録します


[SerializeField]
OverHeadMsg overHeadMsgPrefab;

頭上に表示するメッセージPrefabを登録します
Prefabそのものはゲームオブジェクトですが、型をそのPrefabがアタッチしているスクリプトとすることでスクリプトのインスタンスそのものの取得ができます


overHeadMsg = Instantiate(overHeadMsgPrefab, canvasRect);

Prefabからインスタンスを生成します
第1引数はPrefabにします。スクリプトなどコンポーネントをアタッチした場合、アタッチされているゲームオブジェクト自身が生成されます
第2引数は生成場所を指します。canvasRectを登録することでcanvasの子オブジェクトとして生成できます
戻り値の型はPrefabにアタッチされているスクリプトにしておきます


overHeadMsg.targetTran = transform;

メッセージにアタッチされているoverHeadMsgスクリプトのtargetフィールドにCubeのTransformを代入します
メッセージを表示したもらうためにCube自身の場所を渡します

メッセージ変更機能追加

表示するメッセージを変更できる機能を追加してみましょう

メッセージ用

public void ShowMsg(string msg)
{
    GetComponent<Text>().text = msg;
}

メソッドを1つ追加します
このメソッドが呼ばれるとTextコンポーネントのtextプロパティに引数の値が代入されます

更新したスクリプト
using UnityEngine;
using UnityEngine.UI;

public class OverHeadMsg : MonoBehaviour
{
   public Transform targetTran;

    void Update()
    {
        transform.position = RectTransformUtility.WorldToScreenPoint(
             Camera.main,
             targetTran.position + Vector3.up);
    }

    public void ShowMsg(string msg)
    {
        GetComponent<Text>().text = msg;
    }
}

メッセージオブジェクト作成用

表示変更用のスクリプトを呼び出します

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        overHeadMsg.ShowMsg("Hello");
    }
}

スペースキーを押すとメッセージが変更されるようにします


更新したスクリプト
using UnityEngine;

public class OverHeadMsgCreater : MonoBehaviour
{
    [SerializeField]
    RectTransform canvasRect;

    [SerializeField]
    OverHeadMsg overHeadMsgPrefab;

    OverHeadMsg overHeadMsg;

    void Start()
    {
        overHeadMsg = Instantiate(overHeadMsgPrefab, canvasRect);
        overHeadMsg.targetTran = transform;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            overHeadMsg.ShowMsg("Hello");
        }
    }
}

機能追加

オブジェクトの追加、削除対応

エディタ上で実験してみましょう

実行後、Cubeをコピーしてペースト(またはDuplicate)

ここまでは問題ありませんが、右クリックで削除するとエラーになります

コンソール表示エラー

MissingReferenceException: The object of type 'Transform’ has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
UnityEngine.Transform.get_position () <0x32097e8c0 + 0x00053> in <9ad194799e0949a88479bdcdc6083226>:0
OverHeadMsg.Update () (at Assets/OverHeadMsg.cs:10)
訳)
参照先が見当たらない例外: Transform’ 型のオブジェクトは破棄されましたが、あなたはまだそれにアクセスしようとしています。
スクリプトでは、NULLかどうかを確認するか、オブジェクトを破棄しないようにする必要があります。
UnityEngine.Transform.get_position () <0x32097e8c0 + 0x00053> in <9ad194799e0949a88479bdcdc6083226>:0
OverHeadMsg.Update () (at Assets/OverHeadMsg.cs:10)

実際のテストの様子

理由

Cubeが削除されているため、targetTranがnullになってしまいます

どのように機能アップする?

考え方としては、Cubeが削除された時(実際は無効化された時も対応したい)にメッセージのオブジェクトも削除すればよさそうです

OverHeadMsgCreaterスクリプト

void Start()メソッドのブロック内容をOnEnable()に移動しましょう
これで有効化された時に実行できるようになります

void OnEnable()
{
    overHeadMsg = Instantiate(overHeadMsgPrefab, canvasRect);
    overHeadMsg.targetTran = transform;
}

無効化された時を有効化とのセットで追加します
メッセージのオブジェクトを削除しています

void OnDisable()
{
    Destroy(overHeadMsg.gameObject);
}

結果

上記のテストは問題なくなりました
しかし・・・・エディターを終わらせるとエラーになります

コンソール表示エラー

MissingReferenceException: The object of type 'OverHeadMsg’ has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
OverHeadMsgCreater.OnDisable () (at Assets/OverHeadMsgCreater.cs:29)
訳)
参照先が見当たらない例外: OverHeadMsg’ 型のオブジェクトは破棄されましたが、まだアクセスしようとしています。
スクリプトで null かどうかを確認するか、オブジェクトを破棄しないでください。
OverHeadMsgCreater.OnDisable () (at Assets/OverHeadMsgCreater.cs:29)

実行終了時にエラーになる

実際のテストの様子

理由

このCubeが削除されようとしている時、先にメッセージオブジェクトが削除されている
アプリケーションの終了時なので、当たり前ですがすべてのオブジェクトは破棄されます
すでに無いものを消すことはできません

OverHeadMsgCreaterスクリプト

overHeadMsgがすでになければ(nullでない)消すようにしてみましょう

void OnDisable()
{
    if (overHeadMsg != null)
    {
        Destroy(overHeadMsg.gameObject);
    }
    else
    {
        Debug.Log("アプリ終了時は先に文字オブジェクトが消えてる場合がある");
    }
}

実際のテストの様子

Cubeの追加、無効化、削除、プログラム終了を試しています

実験が終わったらコードは次のようにしてネストを減らしてもいいでしょう
(インデントの階層化を少なくすること)

void OnDisable()
{
    if (overHeadMsg == null) return;

    Destroy(overHeadMsg.gameObject);
}

変更を踏まえたコード全体

OverHeadMsgCreaterスクリプト

随分コードが長くなりましたが、これで目的が達せられました

using UnityEngine;

public class OverHeadMsgCreater : MonoBehaviour
{
    [SerializeField]
    RectTransform canvasRect;

    [SerializeField]
    OverHeadMsg overHeadMsgPrefab;

    OverHeadMsg overHeadMsg;

    void OnEnable()
    {
        overHeadMsg = Instantiate(overHeadMsgPrefab, canvasRect);
        overHeadMsg.targetTran = transform;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            overHeadMsg.ShowMsg("Hello");
        }
    }

    void OnDisable()
    {
        if (overHeadMsg == null) return;

        Destroy(overHeadMsg.gameObject);
    }
}

発展

このCubeをPrefabにしてSpawnマネージャ(CubeをInstantiateするスクリプト)から目的に応じて使うといいでしょうね

サンプルとしてPrefab対応用のコードを載せておきます

Spawnerスクリプト

新規でゲームオブジェクトを作成してアタッチします

using UnityEngine;

public class Spawner : MonoBehaviour
{
    [SerializeField]
    OverHeadMsgCreater overHeadMsgCreater;

    [SerializeField]
    RectTransform canvasRect;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            // 新しいCubeを作成、戻り値としてそのオブジェクトのOverHeadMsgCreaterスクリプトを取得する
            OverHeadMsgCreater newCube = Instantiate(overHeadMsgCreater);

            // canvasRectフィールドに代入する
            newCube.canvasRect = canvasRect;
        }
    }
}

OverHeadMsgCreaterスクリプト(更新)

Cubeの有効化・無効化・新規生成・破棄(Destroy)・ プログラム終了時のそれぞれに対応できるようにしてあります
Unityイベント(Start, OnEnable, Update, OnDisable)の処理記述で対応しています

using UnityEngine;

public class OverHeadMsgCreater : MonoBehaviour
{

    public RectTransform canvasRect;

    [SerializeField]
    OverHeadMsg overHeadMsgPrefab;

    OverHeadMsg overHeadMsg;

    void Start()
    {
        overHeadMsg = Instantiate(overHeadMsgPrefab, canvasRect);
        overHeadMsg.targetTran = transform;
    }

    void OnEnable()
    {
        if (overHeadMsg == null) return;

        overHeadMsg.gameObject.SetActive(true);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            overHeadMsg.ShowMsg("Hello");
        }
    }

    void OnDisable()
    {
        if (overHeadMsg == null) return;

        overHeadMsg.gameObject.SetActive(false);
    }

    void OnDestroy()
    {
        if (overHeadMsg == null) return;

        Destroy(overHeadMsg.gameObject);
    }
}

ゲームオブジェクトの子オブジェクトとしてメッセージを表示

3Dオブジェクトの座標(ワールド座標)と同じ座標系にCanvasを作成してUI上にテキストを表示します

実行イメージ

シーンの構成

新しくプロジェクトを作成してもいいですし、新しくシーンを作成して続けてもいいです
Git管理されている場合、ブランチを作成して続けてもいいです

サンプルのキャプチャでは、Hierarchyのルート階層にCanvasがありますが、不要です

メッセージを頭上に表示したいオブジェクトを作成

Cubeを作成します

スクリプトを作成した後にスクリプトをアタッチして表示テキストをアウトレット接続します

メッセージを頭上に表示するUIのTextオブジェクトの作成

表示用のTextはCubeの子オブジェクトとして、一緒に動くようにします

Cubeを選択して、右クリックメニューからText(Legacy)を作成します
Canvasが同時に作成されますので、Canvasを選択してインスペクターでパラメータを調整します
3Dオブジェクトと同じ座標系に表示するため、RendereModeをWorldSpaceにします

メッセージを頭上に表示するUIのTextオブジェクトの調整

モードをWorldSpaceに変更して、3Dオブジェクトと同じ座標系にします
Textもサイズ感に合わせてパラメータを更新します

CubeをPrefab化

調整ずみのCubeをベースに、どんどん作成できるようにPrefab化します
Prefabは、Projectウィンドウにドラッグ&ドロップすると作成されます

作成できたら、HierarchyにあるCubeは削除しておきます

Cubeを作成するためのオブジェクトを作成します

SpawnManagerオブジェクトを作成します
どんどん作成できるようにPrefabからCubeを作成する処理を担当します
新しいゲームオブジェクトを作成してスクリプトをアタッチします
ベースとなるPrefabをアウトレット接続します

スクリプト

メッセージ更新用

OverHeadMsgControllerスクリプト

Cubeにアタッチします
表示メッセージの変更処理を担当します

using UnityEngine;
using UnityEngine.UI;

public class OverHeadMsgController : MonoBehaviour
{
    [SerializeField]
    Text overHeadMsg;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            overHeadMsg.text = "Hello";
        }
    }
}

メッセージを常にカメラが見ている方向に向ける

LookAtCameraForwardスクリプト

Canvasオブジェクトにアタッチします
メッセージが常にカメラの向いている方法を向くようにします

using UnityEngine;

public class LookAtCameraForward : MonoBehaviour
{
    void Update()
    {
        transform.LookAt(transform.position + Camera.main.transform.forward);
    }
}
メッセージの向き

ルート階層にCanvasを置いてTextを子オブジェクトとする場合と違い、メッセージは常に正面を向いてくれません
カメラが移動しても、TextがCubeと同じ方向を向いたままです
看板の文字表示などではそのままの状態でいいのですが、今回はルート階層にCanvasを置いた時と同じように常にこちら側に向いたままにしたい(カメラ面の対して正体させておく)のでスクリプトで対応します

カメラが向いている方向ベクトルは次のコードで取得できます

Camera.main.transform.forward

Textも同じくこの向いている方法に向けます

注意として、カメラ自身に向かないようにすること
カメラを凝視するのではなく、カメラと同じベクトルの方向に合わせます
そうしないと不自然に見えます

次の位置情報(position)は、どこを見ればいいのかを計算しているコードになります

transform.position + Camera.main.transform.forward

Camera.main.transform.forwardからは、大きさ1の単位ベクトル(normal)が得られますので、これに自身のpositionを足して見るべき位置を計算しています

計算した見るべき位置をLookAtメソッドの引数に代入し、その方向を見るようにすればOKdesu

transform.LookAt(transform.position + Camera.main.transform.forward);

実行画面

Cubeが回転しても定位置にいたままのする

KeepLocalPositionスクリプト

Canvasオブジェクトにアタッチします
Canvasオブジェクトは、Cubeオブジェクト(Player、Enemyなど・・・をシミュレーション)の子オブジェクトになります
メッセージの位置がCubeの回転に影響されないようにします

using UnityEngine;

public class KeepLocalPosition : MonoBehaviour
{
    Vector3 ownInitialLocalPos;

    void Awake()
    {
        ownInitialLocalPos = transform.localPosition;
    }

    void Update()
    {
        Vector3 parentPos = transform.parent.position;
        transform.position = parentPos + ownInitialLocalPos;
    }
}
親オブジェクトの回転の影響を受けない子オブジェクト

ただ単に親子関係を構築すると、親オブジェクトが回転するとその状態をローカル座標系で維持しようとするため、親を中心に回転してしまいます

親の位置との相対位置(親から見てどれくらい離れているかを表す)の取得は次のようになります
この値を最初に取得しておきます

transform.localPosition;

次からの処理は常に計算されます(Update)
まず、親のワールド座標系の位置を取得します

Vector3 parentPos = transform.parent.position;

取得した親のワールド座標系の位置と最初に取得しておいた親との相対位置を足して新しい位置として移動しています
実際は回転をしているのですが、都度補正している感じですね

transform.position = parentPos + ownInitialLocalPos;

Cubeオブジェクト作成用

Spawnerスクリプト

SpawnManagerオブジェクトににアタッチします
Cubeの生成を担当します

using UnityEngine;

public class Spawner : MonoBehaviour
{
    [SerializeField]
    GameObject CubePrefab;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            Instantiate(CubePrefab);
        }
    }
}

実際のテストの様子

  • Cubeの移動に対応してメッセージの角度が変わり、自然な表示になっています
  • Cubeの回転に対してもメッセージの位置が変わりません
  • Prefabでの生成に対応しています
  • カメラの位置を変更、回転してもCubeは影響を受けません
  • 複数のCubeでもカメラの位置の影響を受けません
  • メッセージは更新が可能です

障害物があった場合の実際のテストの様子

壁の有無でテキストを適切に表示できています
ルート階層のCanvasでは対応が困難な機能になります

参考

Unity

Posted by hidepon