20
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

画面外のターゲットへの方向が画面端に表示されるアレ(Offscreen Indicator)を作る

Posted at

完成デモ

demo.gif

作り方

CanvasとUIを準備する

CanvasのRenderModeはScreen Space - Overlay前提です。
img1.png
以下の画像を参考にUIを作成して、Canvasの子要素としてヒエラルキーに追加しておきます。
img2.png

スクリーン座標を求めるスクリプトを作成する

まずCamera.WorldToScreenPoint()でターゲットのワールド座標をスクリーン座標に変換して、UIをターゲット上に表示させます。

TargetIndicator.cs
using UnityEngine;

[RequireComponent(typeof(RectTransform))]
public class TargetIndicator : MonoBehaviour
{
    [SerializeField]
    private Transform target = default;

    private Camera mainCamera;
    private RectTransform rectTransform;

    private void Start() {
        mainCamera = Camera.main;
        rectTransform = GetComponent<RectTransform>();
    }

    private void LateUpdate() {
        var center = 0.5f * new Vector3(Screen.width, Screen.height);

        // (画面中心を原点(0,0)とした)ターゲットのスクリーン座標を求める
        var pos = mainCamera.WorldToScreenPoint(target.position) - center;

        rectTransform.anchoredPosition = pos;
    }
}

カメラ後方にあるターゲットのUIの位置について

上のコードの段階では、意図しない場所にもUIが表示されることがあります。これは、カメラ後方にあるターゲットのUIも表示されていることが原因です。
img3.png
この画面をEditorのシーンビューで見てみたのが下の図です。カメラ後方の左下側にあるターゲットのワールド座標が、右上側(画面中心に対する点対称の位置)のスクリーン座標に変換されていることがわかります。
img4.png
Camera.WorldToScreenPoint()z値にはカメラからターゲットへの距離が入っています。この値の正負を確認することで、ターゲットがカメラ後方にあるかどうかを判定できます。ここでは一旦、カメラ後方のターゲットのUIを画面外に移動するようにしておきましょう。

        var pos = mainCamera.WorldToScreenPoint(target.position) - center;
        // カメラ後方にあるターゲットのスクリーン座標は、画面外に移動する
        if (pos.z < 0f) {
            pos.x = Screen.width;
            pos.y = Screen.height;
        }

UIとカメラの動きを合わせる

UIのスクリーン座標を計算する処理は、ターゲットやカメラの位置・向きを計算する処理よりも後に行わないと、ターゲットとUIの表示が(1フレーム分)ずれてしまいます。もしずれている場合は、Edit > Project Settings > Script Execution Orderからスクリプトの実行順を制御する必要があります。
img5.png

画面外のUIの位置を画面端にする

画面内のUIの位置が正しく表示できるようになりました。次に、画面外のUIの位置を画面端に調整するための処理を追加していきます。

カメラ後方にあるターゲットの方向を求める

カメラ後方にあるターゲットは、画面中心に対する点対称の位置のスクリーン座標になっていました。この値をもう一度点対称の位置に移動させれば、元のターゲットの方向がわかるようになります。

        var pos = mainCamera.WorldToScreenPoint(target.position) - center;
        // カメラ後方にあるターゲットのスクリーン座標は、画面中心に対する点対称の座標にする
        if (pos.z < 0f) {
            pos.x = -pos.x;
            pos.y = -pos.y;
        }

画面外のスクリーン座標の画面端の位置を求める

スクリーン座標を画面サイズの半分で割ると、以下の図のようなビューポート座標の値になります。
viewport.png
この値のx座標かy座標かどちらかの絶対値が1になるように計算すると、画面端のスクリーン座標を求めることができます。


        float d = Mathf.Max(
            Mathf.Abs(pos.x / center.x),
            Mathf.Abs(pos.y / center.y)
        );

        // ターゲットのスクリーン座標が画面外なら、画面端になるよう調整する
        bool isOffscreen = (pos.z < 0f || d > 1f);
        if (isOffscreen) {
            pos.x /= d;
            pos.y /= d;
        }
        rectTransform.anchoredPosition = pos;

いろいろ調整する

画面外のターゲットのUIを画面端に表示できたので、ここから細かく調整してクオリティを上げていきます。

カメラと水平なターゲットのUIの表示を補正する

カメラと水平な位置にあるターゲットのUIは、中央左端か中央右端の位置に偏ります。このターゲットがカメラ後方になった時には、スクリーン座標に補正をかけることでUIの動きをなめらかにすることができます。
またこの補正は、カメラ後方のターゲットのスクリーン座標が画面中心になった場合に、画面端の位置の計算で(ゼロ除算)エラーが発生する問題を回避する効果もあります。

        var pos = mainCamera.WorldToScreenPoint(target.position) - center;
        if (pos.z < 0f) {
            pos.x = -pos.x;
            pos.y = -pos.y;

            // カメラと水平なターゲットのスクリーン座標を補正する
            if (Mathf.Approximately(pos.y, 0f)) {
                pos.y = -center.y;
            }
        }

画面端の表示位置を調整する

画面端の表示位置をUIのサイズの半分だけ画面中心側に寄せて、画面端のUIが見切れないようにします。

        var halfSize = 0.5f * rectTransform.sizeDelta;
        float d = Mathf.Max(
            Mathf.Abs(pos.x / (center.x - halfSize.x)),
            Mathf.Abs(pos.y / (center.y - halfSize.y))
        );

矢印を表示する

UIに矢印を追加します。矢印の角度はMathf.Atan2()にスクリーン座標を渡すだけで求めることができます。
img9.png

    [SerializeField]
    private Image arrow = default;
        // ターゲットのスクリーン座標が画面外なら、ターゲットの方向を指す矢印を表示する
        arrow.enabled = isOffscreen;
        if (isOffscreen) {
            arrow.rectTransform.eulerAngles = new Vector3(
                0f, 0f,
                Mathf.Atan2(pos.y, pos.x) * Mathf.Rad2Deg
            );
        }

CanvasScalerに対応する

最後にCanvasのスケールを調整しても表示が崩れないようにします。
Canvasのスケール値は、CanvasのTransform.localScaleから取得できるので、この値を使って適切な座標系に変換して計算するようにします。
img10.png

        // ルート(Canvas)のスケール値を取得する
        float canvasScale = transform.root.localScale.z;

        // UI座標系の値をスクリーン座標系の値に変換する
        var halfSize = 0.5f * canvasScale * rectTransform.sizeDelta;
        // スクリーン座標系の値をUI座標系の値に変換する
        rectTransform.anchoredPosition = pos / canvasScale;

完成スクリプト

TargetIndicator.cs
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(RectTransform))]
public class TargetIndicator : MonoBehaviour
{
    [SerializeField]
    private Transform target = default;
    [SerializeField]
    private Image arrow = default;

    private Camera mainCamera;
    private RectTransform rectTransform;

    private void Start() {
        mainCamera = Camera.main;
        rectTransform = GetComponent<RectTransform>();
    }

    private void LateUpdate() {
        float canvasScale = transform.root.localScale.z;
        var center = 0.5f * new Vector3(Screen.width, Screen.height);

        var pos = mainCamera.WorldToScreenPoint(target.position) - center;
        if (pos.z < 0f) {
            pos.x = -pos.x;
            pos.y = -pos.y;

            if (Mathf.Approximately(pos.y, 0f)) {
                pos.y = -center.y;
            }
        }

        var halfSize = 0.5f * canvasScale * rectTransform.sizeDelta;
        float d = Mathf.Max(
            Mathf.Abs(pos.x / (center.x - halfSize.x)),
            Mathf.Abs(pos.y / (center.y - halfSize.y))
        );

        bool isOffscreen = (pos.z < 0f || d > 1f);
        if (isOffscreen) {
            pos.x /= d;
            pos.y /= d;
        }
        rectTransform.anchoredPosition = pos / canvasScale;

        arrow.enabled = isOffscreen;
        if (isOffscreen) {
            arrow.rectTransform.eulerAngles = new Vector3(
                0f, 0f,
                Mathf.Atan2(pos.y, pos.x) * Mathf.Rad2Deg
            );
        }
    }
}

参考記事

20
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?