【Unity】【エディタ拡張】シリアライズした配列の要素数をインスペクタから増やしたときに要素がコピーされるのを防ぐ

シリアライズした配列の要素数をインスペクタから増やしたときに要素がコピーされるのを防ぐ方法です。

Unity2018.3.9

やりたいこと

Unityで配列をシリアライズするとこんな感じにインスペクタで表示されます。

f:id:halya_11:20190301140714p:plain

ここでSizeを増やすと要素数が増えますが、このとき末尾の要素が自動的にコピーされます。

f:id:halya_11:20190301140843g:plain

今回これをコピーしてほしくないケースに当たったので対応しました。

対応方法

かなりHackyな対応となりますが、対応方法を紹介します。

まず準備として配列要素にするクラスを定義します。

using UnityEngine;

public class ElementBehaviour : MonoBehaviour
{
}

そしてこれを要素にした配列を持つクラスを定義します。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField]
    private ElementBehaviour[] _elements;
}

そしてElementBehaviourのPropertyDrawerを次のように作ります。

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(ElementBehaviour))]
public class ElementBehaviourDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // パスは _elements.Array.data[x]
        var splitPath = property.propertyPath.Split('.');
        var isArrayElement = splitPath[splitPath.Length - 2] == "Array";
        if (isArrayElement && property.objectReferenceValue != null) {
            // 配列indexを取得
            var arrayIndexStr = splitPath[splitPath.Length - 1].Replace("data[", "").Replace("]", "");
            var arrayIndex = int.Parse(arrayIndexStr);

            // 文字列 _elements.Array.data[{0}] を作成
            var formatSplitPath = splitPath;
            formatSplitPath[formatSplitPath.Length - 1] = "data[{0}]";
            var formatPath = string.Join(".", formatSplitPath);

            // 前の要素と次の要素を取得
            var previousElementPath =  string.Format(formatPath, arrayIndex - 1);
            var nextElementPath = string.Format(formatPath, arrayIndex + 1);
            var previousElement = property.serializedObject.FindProperty(previousElementPath);
            var nextElement = property.serializedObject.FindProperty(nextElementPath);
            var isLastElement = nextElement == null;

            // 前の要素がある、かつ最後の要素(追加されたばかりの要素)、かつ前の要素と参照が一緒だったら参照を消す
            if (arrayIndex >= 1 && isLastElement && previousElement.objectReferenceValue == property.objectReferenceValue) {
                property.objectReferenceValue = null;
            }
        }

        // 普通にプロパティを描画
        using (new EditorGUI.PropertyScope(position, label, property)) {
            EditorGUI.PropertyField(position, property);
        }
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return EditorGUIUtility.singleLineHeight;
    }
}

説明はコメントの通りですが、要素が描画されるときにそれが配列要素化をチェックし、
最後の配列要素、かつ一個前の要素と同じ参照を持っていた場合に参照を消す、という処理をしています。

結果

要素を追加しても参照がコピーされなくなりました。

f:id:halya_11:20190301140955g:plain