Unity Editor Adventures: Serialized Matryoshka

Pengantar singkat


Sebagai aturan, untuk sampai ke bidang properti serial yang menarik minat kami, terapi manual menyarankan kami untuk menggunakan metode FindPropertyRelative (), di mana nama variabel dilemparkan.

Untuk alasan tertentu, pendekatan ini tidak selalu nyaman. Alasannya bisa sangat beragam. Misalnya, nama variabel dapat berubah, kita perlu akses mimisan ke properti non-serial, kita perlu memiliki akses ke pengambil setter atau bahkan metode objek serial. Kami tidak akan menanyakan pertanyaan “mengapa Anda membutuhkan ini sama sekali” dan “mengapa Anda tidak dapat melakukannya tanpa cara tradisional”. Misalkan kita perlu - dan hanya itu.

Jadi, mari kita cari tahu cara mendapatkan objek yang sedang kita kerjakan, serta semua objek induknya dari properti berseri, dan tidak terjebak di jalan serialisasi yang penuh jebakan.

Perhatian. Artikel ini menyiratkan bahwa Anda sudah tahu cara bekerja dengan UnityEditor, setidaknya sekali menulis PropertyDrawers kustom, dan setidaknya secara umum memahami bagaimana properti berseragam berbeda dari objek berseri.


Jalur serialisasi


Untuk memulainya, letakkan semua titik di atas O.

Dalam kasus yang paling primitif, kami memiliki kelas pewaris tertentu dari MonoBehaviour, dan memiliki bidang tertentu yang termasuk dalam kelas serial, jelas bukan pewaris dari sapi kesatuan suci A.K.A. UnityEngine.Object.

public class ExamplePulsar : MonoBehaviour
{
...
        [Space][Header("Example Sandbox Inspector Field")]
        public SandboxField sandboxField;
...
}

Dalam kode di atas, SandboxField adalah kelas dengan atribut Serializable.

Untuk mendapatkan akses ke MonoBehaviour pemilik bukan masalah:

UnityEngine.Object serializationRoot = property.serializedObject.targetObject;

Jika mau, Anda dapat menerimanya sebagai, tetapi sekarang kami tidak membutuhkannya. Kami tertarik pada bidang serial itu sendiri untuk menggambarnya dengan semua blackjack seperti pada gambar di bawah ini.


Kita dapat mengambil jalur serialisasi sebagai berikut:

string serializationPath = property.propertyPath;

Dalam kasus kami, jalur serialisasi akan terdiri dari salah satu bidang kami dan akan mengembalikan "sandboxField", yang darinya kami tidak kedinginan atau panas, karena untuk tingkat pertama peneluran, kami hanya perlu mengetahui nama variabel (yang, kebetulan, dikembalikan kepada kami).

Harap dicatat bahwa tidak ada orang tua MonoBehaviour di jalan. Sekarang tidak masalah, tetapi akan menjadi penting ketika kita mulai membongkar boneka Rusia yang terlihat seperti ini:
nestedClassVariable.exampleSandboxesList.Array.data [0] .evenMoreNested.Array.data [0]
Agar tidak ketahuan, ketika properti bersarang, sebelumnya kami akan melakukan hal berikut:

string[] path = property.propertyPath.Split('.');

Sekarang kita memiliki semua simpul jalur serialisasi. Tetapi dalam kasus yang paling primitif, kita hanya perlu simpul nol. Ambil:

string pathNode = path[0];

Nyalakan sedikit refleksi dan dapatkan bidang dari sini:

Type objectType = serializationRoot.GetType();
FieldInfo objectFieldInfo = objectType.GetField(pathNode);
object field = objectFieldInfo.GetValue(serializationRoot);

Kami meninggalkan pertanyaan tentang kecepatan usaha ini. Untuk sejumlah kecil bidang tersebut dan bersarang kecil, biaya refleksi akan jauh lebih sedikit daripada semua yang terjadi di bawah kap UnityEditor selama rendering. Jika Anda ingin bukti - di github, pengembang Unity memiliki hal yang menarik, UnityCsReference, lihat waktu luang Anda, bagaimana rendering ObjectField diimplementasikan, misalnya.

Sebenarnya, ini bukan (ha ha) segalanya. Kami memiliki bidang, kami bahagia, kami dapat melakukan apa pun yang kami inginkan dengannya dan bahkan mencoba untuk menulis UnityEvent kami sendiri dengan semua tombol dan tindakan penting yang hanya memengaruhi bidang kami, tidak peduli objek apa yang menggantung.

Setidaknya, sementara itu tergantung pada akar dari objek ini, semuanya akan baik-baik saja, tetapi kemudian tidak begitu banyak lagi. Di jalan serialisasi, kami menunggu array dan semua jenis daftar, yang keinginan utamanya adalah untuk menempatkan kami di sandal, tepat waktu mengubah jumlah elemen ke atas. Tapi persetan dengan ini, pertama-tama kita akan menggali di bawah array itu sendiri.

Tujuan kami adalah resistensi terhadap boneka semacam itu

Jika kami tidak memiliki array di jalur serialisasi, tugas tersebut akan diselesaikan secara sepele: kami akan mengulangi simpul serialisasi di loop hingga kami mencapai akhir rantai. Sesuatu seperti kode berikut:

object currentObject = serializationRoot;
for (int i = 0; i < directSearchDepth; i++)
{
       string pathNode = path[i];
       Type objectType = currentObject.GetType();
       FieldInfo objectFieldInfo = objectType.GetField(pathNode);
       object nextObject = objectFieldInfo.GetValue(currentObject);
       currentObject = nextObject;
}
return currentObject;

Di sini kita menunggu dua berita yang tidak menyenangkan sekaligus. Saya akan mulai dengan yang kedua.

Langkah mengambil nextObject mungkin tiba-tiba mengembalikan nol daripada objek yang diharapkan. Biasanya ini terjadi ketika kita pertama kali membuat objek induk di inspektur, dan jalur serialisasi sudah ada, tetapi bidang yang sesuai tidak (ini akan membawa kita lebih banyak fasilitas).

Dalam hal ini, alangkah baiknya untuk segera menambahkan jalan keluar dari metode dengan pengembalian nol:

object nextObject = objectFieldInfo.GetValue(currentObject);
if (nextObject == null) return null;
currentObject = nextObject;

"Tunggu sebentar! - kamu bilang. "Dan apa yang harus dilakukan di OnGUI jika nol dikembalikan kepada kita?"

Jawab: tidak ada. Tidak ada yang secara harfiah. Cukup buat kembali dan lewati langkah menggambar ini, sambil menunggu bidang yang akan dibuat. Tidak ada yang mengerikan akan terjadi dari ini.

public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
       SandboxField sandboxField = GetTarget<T>(property);
       if (sandboxField == null) return;
}

di sini GetTarget () adalah fungsi yang sesuai, mengambil objek serial dari properti.

Ngomong-ngomong, saya akan menyarankan Anda untuk membawa bidang yang menarik kepada kami tidak di sini, tetapi di GetPropertyHeight. Ini akan diperlukan jika kita menulis bidang yang dapat dilipat yang dapat diperluas dengan ukuran yang berbeda tergantung pada konten. GetPropertyHeight () dipanggil sebelum OnGUI (), jadi jika kita membawa bidang di sana dan menulisnya di bidang PropertyDrawer kita, kita tidak perlu membawanya lagi ke OnGUI.

Harap perhatikan bahwa instance dari PropertyDrawer khusus dibuat sendiri untuk menggambar semua properti berseri yang terlihat saat ini, dan properti baru dilemparkan ke sana secara bergantian dari atas ke bawah. Ini harus diperhitungkan agar tidak mengacaukan perhitungan ketinggian properti berikutnya, jika tidak, Anda mungkin mendapatkan situasi yang tidak menyenangkan ketika Anda mengklik flip dan bidang yang Anda harapkan diperluas.

Juga, semua perada yang bertanggung jawab untuk menampilkan bidang dalam editor dan yang ingin Anda serialkan, Anda harus membuat serial di sisi kelas yang dapat diserialkan, bukan PropertyDrawer, dan demi akurasi - sertakan kurung kompilasi bersyarat sehingga semua malu Spanyol ini tidak mencoba untuk pergi ke build :

[Serializable]
public class SandboxField
{
#if UNITY_EDITOR
        public bool editorFoldout;
#endif
}

Perangkap lain yang menunggu kita di sini: semua bidang yang dibuat melalui editor ingin meludah pada konstruktor kelas dan pada nilai-nilai default yang ditentukan dalam kelas. Jika Anda melakukannya, misalnya, seperti ini (contoh dari proyek saya, di mana itu adalah nilai dari node permukaan air):

[SerializeField]private int m_nodesPerUnit = 5;

Nilai ini akan diabaikan serialisasi titik-kosong segera setelah Anda menambahkan item baru ke daftar. Memanggil konstruktor untuk bantuan tidak kurang berguna: semua yang Anda tulis di sana akan diabaikan. Objek baru adalah tujuan seperti elang, semua nilainya benar-benar nilai default, itu bukan saja yang ingin Anda lihat di sana, tetapi semua jenis null, false, 0, Color.clear dan hal-hal cabul lainnya.

Kruk dalam daging
. NonUnitySerializableClass, . , DefaultEditorObject(), .

- :

public abstract class NonUnitySerializableClass
{
        protected virtual void DefaultEditorObject()
        {
            // virtually do nothing
        }

        [SerializeField]private bool validated = false;
        public void EditorCreated(bool force = false)
        {
            if (validated && !force) return;
            DefaultEditorObject();
            validated = true;
        }

        public NonUnitySerializableClass()
        {
            EditorCreated(true);
        }
}

DefaultEditorObject(), , EditorCreated .

: .



Mari kita kembali ke domba kita, atau lebih tepatnya, ke array. Masalah lain yang mungkin muncul pada tahap lebih awal terletak pada baris ini:

FieldInfo objectFieldInfo = objectType.GetField(pathNode);

Sisi kanan dapat mengembalikan kita nol jika muncul terhadap array di jalur serialisasi (dan objek "IList" akan menjadi array "Array"). Tidak menyenangkan.

Apa yang harus dilakukan? Cobalah untuk tidak jatuh ke dalam situasi seperti itu dengan menulis pawang:

for (int i = 0; i < pathNode.Length; i++)
{
        string pathNode = path[i];
        Type objectType = currentObject.GetType();
        FieldInfo objectFieldInfo = objectType.GetField(pathNode);

        if (objectFieldInfo == null)
        {
                if (pathNode == "Array")
                {
                        i++;
                        string nextNode = path[i];
                        string idxstr = nextNode.Substring(nextNode.IndexOf("[") + 1);
                        idxstr = idxstr.Replace("]", "");
                        int arrayNumber = Convert.ToInt32(idxstr);
                        IList collection = currentObject as IList;
                        if (collection.Count <= arrayNumber) return null;
                        currentObject = collection[arrayNumber];
                }
                else
                {
                        throw new NotImplementedException("   ");
                }
        }
        else //  ,     
        {
                object nextObject = objectFieldInfo.GetValue(currentObject);
                if (nextObject == null) return null;
                currentObject = nextObject;
        }
}
return currentObject;

Ya, kita bahkan dapat masuk ke situasi yang tidak menyenangkan di sini, ketika jalur serialisasi sudah memiliki, misalnya, data [0] atau data [1] elemen, dan array belum mengimplementasikannya. Misalnya, kami membuat Daftar kosong. Kami bertanya kepadanya N elemen - dan tanpa garis yang indah ini:

if (collection.Count <= arrayNumber) return null;

... kami mendapat banyak pengecualian dalam dengungan. Dan semua yang diperlukan adalah untuk melewatkan langkah rendering, setelah menunggu sampai bidang yang menarik bagi kami dibuat.

Saya belum menemukan kasus-kasus lain ketika objectFieldInfo == null, tetapi pada saat yang sama node serialisasi tidak ditunjuk sebagai Array, jadi melempar pengecualian yang mengerikan ke dalam situasi hipotetis yang luar biasa adalah untuk kemudian memecahkannya.

Secara umum, kami mendapat fungsi kerja yang lebih atau kurang yang memungkinkan kami untuk mengekstrak bidang dengan properti berseri. Di masa depan, fungsi ini dapat dimodifikasi dengan memaksa untuk mengekstrak semua objek di jalur serialisasi, serta mencari "induk" terdekat, termasuk atau mengecualikan array di sepanjang jalur.

Retas seumur hidup untuk menggambar properti bersarang
- , Rect position Rect indentedPosition = EditorGUI.IndentedRect(position). , EditorGUI, position , GUI – indentedPosition. EditorGUILayout OnGUI, ( , ).

, MonoScript ( -, ), static-, AssetDatabase, .

Terimakasih atas perhatiannya.

All Articles