Yの位置が下にある画像は手前に表示する(Z座標制御編)

前回、2Dゲームの画像表示時に Transparency Sort Mode を Custom Axis にすることで Y が下にあるオブジェクトほど手前に表示する + 補助コンポーネントを作成する記事を書きましたが、今回は Y 位置が下にある画像ほど手前に表示する処理を Z 位置を使って制御する方法を紹介したいと思います。

タイトルの通りですが、Isometoric Sorting(Y軸ソート)を Y 座標に応じて Z の位置を変更することで実現します。

確認環境

今回の確認環境は以下の通りです。

  • Unity 2019.4.10f1
  • VisualStudio 2019
  • Windows10

補足:

Windows上のみで確認、実機ビルドなし

実行結果

以降で紹介するコンポーネントを SpriteRenderer を持つゲームオブジェクトにアタッチすると以下のような見た目になります。

f:id:Takachan:20200919174109p:plain
コンポーネントをアタッチしたときの表示

このコンポーネントのインスペクターの表示は以下の通りです。

項目
Mode One : ゲーム開始直後に1度だけ位置を更新する
- Always : 常に値を更新し続ける(ゲーム実行中・編集中の両方)
- Always Only Editing : 編集中は常に値を更新するが実行時は更新しない
Z Offset 前後が入れ替わる位置、シーンビュー上にこの値の位置に緑色の線が表示されるので調整する
Z Group グループ内で前後が入れ替わる、Group{N} の数字が大きくなるほど奥側に表示される
Show Layer Name シーンビュー上に、Z Group の名前を表示するかどうか(Default だけの時は表示不要)

画像の足元で前後が入れ替わってほしいので Z Offset を調整すると緑色の線の表示も併せて更新されます。この位置で前後が入れ替わることを表します。

f:id:Takachan:20200919174930p:plain
Z Offset を変更したときの表示

設定すると同じ Z Grpup 内の画像の前後関係が緑の線で入れ替わるようになります。

f:id:Takachan:20200919181745g:plain
緑色の線で前後が入れ替わる様子

実装コード

ちょっと長いですが勘弁してください。

このコンポーネントをゲーム中に動くオブジェクトには Always、動かない背景などは Always Only Editing とすると大体期待通りの動きになります。

//
// (c) 2020 Takap.
//

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace Takap.Utility
{
    /// <summary>
    /// Zインデックスを更新するためのオブジェクト
    /// </summary>
    [ExecuteAlways]
    [RequireComponent(typeof(SpriteRenderer))]
    public class ZIndexUpdater : MonoBehaviour
    {
        //
        // 説明:
        // オブジェクトの位置に応じてZの位置を更新するスクリプト
        //  → GameObjectにアタッチすれば有効になる
        //

        //
        // 補足:
        // 前後関係の描画の優先順位は SortingLayer > Order In Layer > Z値
        // 
        // Order In Layer で前後関係を処理したくない場合にこのコンポーネントを使用して
        // Z値で前後関係を表現する。3Dビューで見たときに視覚的に分かりやすいかもしれない。
        //
        // Order In Layer と Z値 でパフォーマンス良い悪いは存在しない(らしい)
        // 両方使うと処理が大変なのでできればどちらか一方にしておいた方が無難。
        // 
        // Custom Axis の Y=1 とは同居できるが Z=1 すると描画がおかしくなるのでどちらを使用するかは
        // プロジェクトごとに最初に決めておくべき。
        // 

        //
        // Inner types
        // - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// Z位置の更新方法を表します。
        /// </summary>
        public enum UpdateMode
        {
            /// <summary>1回更新したら終了します。</summary>
            Once = 0,
            /// <summary>常に更新し続けます。</summary>
            Always,
            /// <summary>編集中のみ常に更新を行い実行時は更新しません。</summary>
            AlwaysOnlyEditing,
        }

        /// <summary>
        /// Z高さのグループを表します。
        /// </summary>
        public enum ZGroup
        {
            /// <summary>デフォルト(一番手前)</summary>
            Default = 0,
            /// <summary>1番目に表示されるグループ</summary>
            Group1,
            /// <summary>2番目のレイヤー</summary>
            Group2,
            /// <summary>3番目のレイヤー</summary>
            Group3,
            /// <summary>4番目のレイヤー</summary>
            Group4,
        }

        //
        // Constants
        // - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// レイヤーごとのオフセットを定義します。
        /// </summary>
        private Dictionary<ZGroup, float> layerTable;

        //
        // Inspector
        // - - - - - - - - - - - - - - - - - - - -

        // このオブジェクトの更新タイプ
        [SerializeField] private UpdateMode mode = UpdateMode.AlwaysOnlyEditing;
        // レンダー内のオフセット
        [SerializeField] private float zOffset = default;
        // 所属するZのグループ
        //  → 本当はこれを使わないで 'Order In Layer' で前後を表現したほうがよさそう
        [SerializeField] private ZGroup zGroup = ZGroup.Default;
        // シーンビュー上にレイヤー名を表示するかどうか、true : 表示する / false : 表示しない
        [SerializeField] private bool showLayerName = false;

        //
        // Fields
        // - - - - - - - - - - - - - - - - - - - -

        // Zの移動量の係数
        private const float factor = 0.001f;
        // キャッシュ
        private Transform myTransform;

        //
        // Runtime impl
        // - - - - - - - - - - - - - - - - - - - -

        public void Awake()
        {
            this.layerTable = this.createTable();
            this.myTransform = this.transform;
        }

        public void Start()
        {
            // 編集中のみ更新の場合は実行時は即座に deactive にして終了
            if (EditorApplication.isPlaying && this.mode == UpdateMode.AlwaysOnlyEditing)
            {
                this.enabled = false;
                return;
            }

            // 'Once' の場合1回値を設定したら更新を終了する
            //   → 'Always' の場合、以降毎フレーム更新する
            if (this.mode == UpdateMode.Once)
            {
                this.UpdateZIndex();
                this.enabled = false;
            }
        }

        public void Update() => this.UpdateZIndex();

        public void OnValidate()
        {
            if (this.mode == UpdateMode.Once)
            {
                return;
            }
            this.enabled = true;
        }

#if false
        // (たぶん必要ないけど) 常に表示が必要な場合 #if 'true'
        protected void OnDrawGizmos()
#else
        // オブジェクトが選択されているときだけ表示が必要なら #if 'false'
        protected void OnDrawGizmosSelected()
#endif
        {
            var t = this.transform;
            Vector3 wpos = t.GetPos();
            float y = wpos.y + this.zOffset * t.lossyScale.y;
            Debug.Log($"y={y}");

            var sr = this.GetComponent<SpriteRenderer>();
            float harfW = sr.bounds.size.x / 2.0f;
            var cx = sr.bounds.center.x;

            // シーンビュー上に目印を表示
            var s = new Vector3(cx + harfW + harfW * 0.5f, y, wpos.z);
            var e = new Vector3(cx - harfW - harfW * 0.5f, y, wpos.z);
            Gizmos.color = Color.green;
            Gizmos.DrawLine(s, e);
            GizmoDrawer.DispCrossMark(this.transform.GetPos(), 0.015f, Color.red);

#if UNITY_EDITOR
            // 不要であれば削除する、そんなに有益なものでもない
            if (this.showLayerName)
            {
                var style = new GUIStyle();
                style.normal.textColor = Color.green;
                style.fontSize = 9;
                Vector3 l = t.position;
                l.y = s.y;
                Handles.Label(l, this.zGroup.ToString(), style);
            }
#endif
        }


        //
        // Public Methods
        // - - - - - - - - - - - - - - - - - - - -

        public void UpdateZIndex()
        {
            // シーンビュー上の処理なのでおかしくならないように少し書き方が変になってる
            var t = this.myTransform == null ? this.transform : this.myTransform;
            if (t == null)
            {
                return;
            }
            var table = this.layerTable;
            if (table == null)
            {
                table = this.createTable();
            }

            float z = (t.GetPosY() + this.zOffset * t.lossyScale.y) * factor;
            z += table[this.zGroup];
            t.SetLocalPosZ(z);
        }

        /// <summary>
        /// スクリプト上から指定したモードに動作を変更します。
        /// </summary>
        public void ChangeMode(UpdateMode mode)
        {
            if (mode == UpdateMode.AlwaysOnlyEditing)
            {
                Debug.LogWarning($"This mode is unsuported in playing. {mode}");
                return; // 実行中の呼び出しを想定しているのでこれは受け付けない
            }

            this.enabled = true; // 有効にしてループで処理する
            this.Start();
        }

        // Z値のグループごとのオフセットを表すテーブルを取得します。
        private Dictionary<ZGroup, float> createTable()
        {
            return new Dictionary<ZGroup, float>()
            {
                // プロジェクトごとに状況が異なるため
                // 前後関係がおかしくなるようであれば個々の値を大きくする
                { ZGroup.Default, 1f },
                { ZGroup.Group1,  2f },
                { ZGroup.Group2,  3f },
                { ZGroup.Group3,  4f },
                { ZGroup.Group4,  5f },
            };
        }
    }
}