مغامرات محرر الوحدة: Serialized Matryoshka

مقدمة قصيرة


كقاعدة ، من أجل الوصول إلى مجال خاصية متسلسلة تهمنا ، ينصحنا العلاج اليدوي باستخدام طريقة FindPropertyRelative () ، التي يتم فيها طرح اسم المتغير.

لأسباب معينة ، هذا النهج ليس مناسبًا دائمًا. يمكن أن تكون الأسباب متنوعة للغاية. على سبيل المثال ، يمكن أن يتغير اسم متغير ، فنحن بحاجة إلى الوصول إلى نزيف في الأنف إلى خاصية غير متسلسلة ، ونحتاج إلى الوصول إلى المستوطنين أو حتى طرق كائن متسلسل. لن نسأل الأسئلة "لماذا تحتاج إلى هذا على الإطلاق" و "لماذا لا يمكنك الاستغناء عن الطرق التقليدية". لنفترض أننا نحتاج - وهذا كل شيء.

لذا ، دعنا نكتشف كيفية الحصول على الكائن الذي نعمل معه ، بالإضافة إلى جميع الكائنات التابعة له من الخاصية المتسلسلة ، ولا ننشغل في طريق التسلسل المليء بالعثرات.

انتباه. تشير هذه المقالة إلى أنك تعرف بالفعل كيفية العمل مع UnityEditor ، وقد كتب على الأقل مرة واحدة PropertyDrawers مخصصة ، وعلى الأقل بشكل عام فهم كيف تختلف الخاصية المتسلسلة عن كائن متسلسل.


مسار التسلسل


بادئ ذي بدء ، ضع كل النقاط فوق O.

في الحالة الأكثر بدائية ، لدينا فئة وريث معينة من MonoBehaviour ، ولها مجال معين ينتمي إلى فئة متسلسلة ، من الواضح أنه ليس وريثًا من البقرة الوحدوية المقدسة لـ A.K.A. UnityEngine.Object.

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

في الكود أعلاه ، يعد SandboxField فئة ذات سمة Serializable.

الوصول إلى MonoBehaviour للمالك ليس مشكلة:

UnityEngine.Object serializationRoot = property.serializedObject.targetObject;

إذا كنت ترغب ، يمكنك أن تأخذه كما هو ، لكننا الآن لسنا بحاجة إليه. نحن مهتمون بالحقل المتسلسل نفسه من أجل رسمه بكل لعبة ورق كما هو موضح في الشكل أدناه.


يمكننا أن نأخذ مسار التسلسل على النحو التالي:

string serializationPath = property.propertyPath;

في حالتنا ، سيتألف مسار التسلسل من أحد حقولنا وسيعيد "sandboxField" ، الذي لا نشعر بالبرد أو الحار منه ، لأننا في المستوى الأول من التعشيش نحتاج فقط إلى معرفة اسم المتغير (الذي ، بالمناسبة ، تم إرجاعه إلينا).

يرجى ملاحظة أنه لا يوجد أحد MonoBehaviour في الطريق. الآن لا يهم ، ولكن سيصبح مهمًا عندما نبدأ في تفكيك دمية روسية تبدو مثل هذا:
nestedClassVariable.exampleSandboxesList.Array.data [0] .evenMoreNested.Array.data [0]
لكي لا يتم القبض عليك في وقت لاحق ، عندما يتم دمج الخصائص ، سنقوم مسبقًا بما يلي:

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

الآن لدينا كل عقد مسار التسلسل. ولكن في الحالة الأكثر بدائية ، نحتاج فقط إلى العقدة الصفرية. خذها:

string pathNode = path[0];

قم بتشغيل انعكاس بسيط واحصل على المجال من هنا:

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

نترك وراء الكواليس مسألة سرعة هذا المشروع. بالنسبة لعدد قليل من هذه الحقول وتعشيش صغير ، ستكون تكلفة الانعكاس أقل بكثير من كل ما يحدث تحت غطاء UnityEditor أثناء العرض. إذا كنت تريد البراهين - على github ، فإن مطوري Unity لديهم شيء مثير للاهتمام ، UnityCsReference ، انظر إلى وقت فراغك ، وكيف يتم تنفيذ عرض ObjectField ، على سبيل المثال.

في الواقع ، هذا ليس كل شيء. لقد حصلنا على المجال ، ونحن سعداء ، يمكننا أن نفعل ما نريده به ، وحتى نحاول كتابة UnityEvent الخاص بنا مع جميع الأزرار والإجراءات المهمة التي تؤثر على مجالنا فقط ، بغض النظر عن الكائن الذي يعلق عليه.

على الأقل ، أثناء تعليقه في جذر هذا الكائن ، سيكون كل شيء على ما يرام ، ولكن بعد ذلك لم يعد كثيرًا. في مسار التسلسل ، نحن في انتظار المصفوفات وجميع أنواع القوائم ، التي تتمثل رغبتها الرئيسية في وضعنا في شبشب ، وتغيير عدد العناصر صعودًا في الوقت المناسب. ولكن الجحيم بهذا ، سنحفر أولاً تحت الصفيف نفسه.

هدفنا هو مقاومة هذه الدمى

إذا لم يكن لدينا صفائف في مسار التسلسل ، فستكون المهمة تافهة: سوف ننتقل عبر عقد التسلسل حتى نصل إلى نهاية السلسلة. شيء مثل الرمز التالي:

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;

نحن هنا في انتظار خبرين مزعجين في آن واحد. سأبدأ بالثاني.

قد تؤدي خطوة أخذ nextObject فجأة إلى إرجاع قيمة فارغة بدلاً من الكائن المتوقع. عادة ما يحدث هذا عندما نقوم أولاً بإنشاء الكائن الرئيسي في المفتش ، ومسار التسلسل موجود بالفعل ، ولكن الحقل المقابل غير موجود (وهذا سيجلب لنا المزيد من وسائل الراحة).

في هذه الحالة ، سيكون من الجيد إضافة المخرج من الطريقة على الفور مع الإرجاع فارغًا:

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

"انتظر دقيقة! - قول انت. "وماذا نفعل بعد ذلك في OnGUI إذا تم إرجاع صفر إلينا؟"

الجواب: لا شيء. لا شيء حرفيًا. ما عليك سوى العودة ، وبالتالي تخطي خطوة الرسم هذه ، في انتظار إنشاء الحقل. لن يحدث شيء فظيع من هذا.

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

هنا GetTarget () هي الوظيفة المقابلة ، مع أخذ الكائن المتسلسل من الخاصية.

بالمناسبة ، أنصحك بأخذ مجال الاهتمام إلينا ليس هنا ، ولكن في GetPropertyHeight. سيكون هذا مطلوبًا في حالة كتابة حقول قابلة للتوسيع قابلة للتوسيع بأحجام مختلفة اعتمادًا على المحتوى. يتم استدعاء GetPropertyHeight () قبل OnGUI () ، لذلك إذا أخذنا الحقل هناك وكتبهنا في حقل PropertyDrawer الخاص بنا ، فلن نضطر إلى نقله مرة أخرى إلى OnGUI.

يرجى ملاحظة أنه يتم إنشاء مثيل من PropertyDrawer المخصص وحده لرسم جميع الخصائص المتسلسلة المرئية حاليًا ، ويتم طرح الخصائص الجديدة عليه من أعلى إلى أسفل. يجب أن يؤخذ هذا في الاعتبار حتى لا تعبث مع حساب ارتفاع الخاصية التالية ، وإلا قد تحصل على موقف غير سار عند النقر على مطوية ويتم توسيع الحقل الذي تتوقعه.

أيضًا ، كل الزينة المسؤولة عن عرض الحقل في المحرر والذي تريد إجراء تسلسل له ، يجب عليك إجراء تسلسل على جانب الفئة القابلة للتسلسل ، وليس PropertyDrawer ، ومن أجل الدقة - إرفاق أقواس تجميع شرطية بحيث لا يحاول كل هذا العار الإسباني الذهاب إلى البناء :

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

مأزق آخر ينتظرنا هنا: أرادت جميع الحقول التي تم إنشاؤها من خلال المحرر أن تبصق على مُنشئ الفئة وعلى القيم الافتراضية المحددة في الفصل. إذا قمت بذلك ، على سبيل المثال ، مثل هذا (مثال من مشروعي ، حيث كانت قيمة عقد سطح الماء):

[SerializeField]private int m_nodesPerUnit = 5;

سيتم تجاهل هذه القيمة تسلسل فارغ نقطة بمجرد إضافة عنصر جديد إلى القائمة. استدعاء المنشئ للحصول على المساعدة ليس أقل فائدة: سيتم تجاهل كل ما كتبته هناك. الكائن الجديد هو هدف مثل الصقر ، وجميع قيمه هي القيم الافتراضية حقًا ، وهذا ليس فقط القيم التي تريد رؤيتها هناك ، ولكن جميع أنواع الأشياء الخالية والباطلة و 0 و Color.clear وغيرها من الأشياء الفاحشة.

عكاز في اللحم
. 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 .

: .



دعونا نعود إلى خرافنا ، أو بالأحرى إلى المصفوفات. مشكلة أخرى قد تنشأ في مرحلة أبكر تكمن في هذا السطر:

FieldInfo objectFieldInfo = objectType.GetField(pathNode);

يمكن أن يعيدنا الجانب الأيمن صفرًا إذا واجه صفيفًا في مسار التسلسل (وأي كائن "IList" سيكون صفيف "Array"). غير سارة.

ماذا أفعل؟ حاول ألا تقع في مثل هذه المواقف عن طريق كتابة معالج:

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;

نعم ، يمكننا حتى الدخول في موقف غير سار هنا ، عندما يكون مسار التسلسل بالفعل ، على سبيل المثال ، عنصر البيانات [0] أو البيانات [1] ، ولم يقم الصفيف بتنفيذها بعد. على سبيل المثال ، أنشأنا قائمة فارغة. نسأله عناصر N - وبدون هذا الخط الجميل:

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

... نحصل على مجموعة من الاستثناءات في الخرخرة. وكل ما كان مطلوبًا هو تخطي خطوة التقديم ، بعد الانتظار حتى يتم إنشاء الحقول التي تهمنا.

لم أجد بعد حالات أخرى عندما يكون objectFieldInfo == فارغًا ، ولكن في نفس الوقت لم يتم تعيين عقدة التسلسل على أنها صفيف ، لذا فإن طرح استثناء رهيب في مثل هذه الحالة الاستثنائية الافتراضية هو كسرها لاحقًا.

بشكل عام ، حصلنا على وظيفة عمل أكثر أو أقل تسمح لنا باستخراج حقل من خلال خصائصه المتسلسلة. في المستقبل ، يمكن تعديل هذه الوظيفة من خلال إجبارها على استخراج جميع الكائنات في مسار التسلسل ، وكذلك البحث عن أقرب "أصل" ، بما في ذلك أو استبعاد الصفائف على طول المسار.

اختراق الحياة لرسم خصائص متداخلة
- , Rect position Rect indentedPosition = EditorGUI.IndentedRect(position). , EditorGUI, position , GUI – indentedPosition. EditorGUILayout OnGUI, ( , ).

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

شكرا للانتباه.

All Articles