مرحبا بالجميع! اسمي Grisha وأنا مؤسس CGDevs. اليوم أريد أن أتحدث عن إضافات المحرر وأتحدث عن أحد مشاريعي ، والتي قررت نشرها في OpenSource.
الوحدة هي أداة رائعة ، لكن لديها مشكلة صغيرة. للمبتدئين ، لإنشاء غرفة بسيطة (صندوق به نوافذ) ، عليك إما إتقان النمذجة ثلاثية الأبعاد ، أو محاولة تجميع شيء ما من الكواد. في الآونة الأخيرة ، أصبح برنامج ProBuilder مجانيًا تمامًا ، ولكنه أيضًا حزمة مبسطة ثلاثية الأبعاد. أردت أداة بسيطة تسمح لنا بإنشاء بيئات مثل الغرف ذات النوافذ والأشعة فوق البنفسجية بشكل صحيح. منذ وقت طويل ، قمت بتطوير مكون إضافي لبرنامج Unity ، والذي يسمح لك بإجراء نموذج سريع لبيئات مثل الشقق والغرف باستخدام رسم ثنائي الأبعاد ، والآن قررت وضعه في OpenSource. باستخدام مثاله ، سنقوم بتحليل كيف يمكنك توسيع المحرر والأدوات الموجودة لهذا الغرض. إذا كنت مهتما ، مرحبا بكم في القط. يتم إرفاق رابط للمشروع في النهاية ، كما هو الحال دائمًا.

لدى Unity3d صندوق أدوات واسع بما يكفي لتوسيع قدرات المحرر. بفضل الفصول الدراسية مثل
EditorWindow ، بالإضافة إلى وظائف
Custom Inspector و
Property Drawer و
TreeView (+
UIElements يجب أن تظهر قريبًا) ، فمن السهل بناء أطر العمل الخاصة بك بدرجات متفاوتة من التعقيد أعلى الوحدة.
اليوم سنتحدث عن أحد الأساليب التي استخدمتها لتطوير حل بلدي وعن اثنين من المشاكل المثيرة للاهتمام التي كان عليَّ مواجهتها.
يعتمد الحل على استخدام ثلاث فئات ، مثل
EditorWindow (كافة الإطارات الإضافية) و
ScriptableObject (تخزين البيانات) و
CustomEditor (وظائف مفتش إضافية
للكائن Scriptable ).
عند تطوير إضافات المحرر ، من المهم محاولة الالتزام بمبدأ أن مطوري Unity سيستخدمون الامتداد ، لذلك يجب أن تكون الواجهات واضحة وموائمة وملائمة لسير عمل Unity.
دعنا نتحدث عن المهام المثيرة للاهتمام.
لكي نتمكن من وضع نموذج أولي لشيء ما ، بادئ ذي بدء ، نحن بحاجة إلى معرفة كيفية رسم الرسومات التي سنولد منها بيئتنا. للقيام بذلك ، نحتاج إلى نافذة EditorWindow خاصة ، والتي سوف نعرض فيها جميع الرسومات. من حيث المبدأ ، سيكون من الممكن رسم SceneView ، لكن الفكرة الأولية كانت أنه عند الانتهاء من الحل ، قد ترغب في فتح العديد من الرسومات في نفس الوقت. بشكل عام ، يعد إنشاء نافذة منفصلة في وحدة مهمة بسيطة إلى حد ما. يمكن العثور على هذا في
أدلة الوحدة. لكن شبكة الرسم هي مهمة أكثر إثارة للاهتمام. هناك العديد من المشاكل في هذا الموضوع.
الوحدة لديها العديد من الأنماط التي تؤثر على ألوان النافذة.الحقيقة هي أن معظم الأشخاص الذين يستخدمون إصدار Pro من Unity يستخدمون سمة مظلمة ، ولا يتوفر سوى الإصدار الخفيف في الإصدار المجاني. ومع ذلك ، يجب عدم دمج الألوان المستخدمة في محرر الرسم مع الخلفية. هنا يمكنك التوصل إلى حلين. الشيء الصعب هو إنشاء نسختك الخاصة من الأنماط والتحقق منها وتغيير اللوحة لإصدار الوحدة. والشيء البسيط هو ملء خلفية النافذة بلون معين. في التطوير ، تقرر استخدام طريقة بسيطة. مثال على كيفية القيام بذلك هو استدعاء هذا الرمز في طريقة OnGUI.
لون معينGUI.color = BgColor; GUI.DrawTexture(new Rect(Vector2.zero, maxSize), EditorGUIUtility.whiteTexture); GUI.color = Color.white;
في جوهرها ، قمنا فقط برسم نسيج لون BgColor إلى النافذة بأكملها.
ارسم الشبكة وانقلهاهنا تم الكشف عن العديد من المشاكل في وقت واحد. أولاً ، كان عليك إدخال نظام الإحداثيات الخاص بك. الحقيقة هي أنه من أجل العمل الصحيح والمريح ، نحتاج إلى إعادة حساب إحداثيات واجهة المستخدم الرسومية للإطار في إحداثيات الشبكة. لهذا ، تم تنفيذ طريقتين للتحويل (في جوهرهما ، هاتان هما مصفوفات TRS مصبوغة)
تحويل إحداثيات النافذة إلى إحداثيات الشاشة public Vector2 GUIToGrid(Vector3 vec) { Vector2 newVec = ( new Vector2(vec.x, -vec.y) - new Vector2(_ParentWindow.position.width / 2, -_ParentWindow.position.height / 2)) * _Zoom + new Vector2(_Offset.x, -_Offset.y); return newVec.RoundCoordsToInt(); } public Vector2 GridToGUI(Vector3 vec) { return (new Vector2(vec.x - _Offset.x, -vec.y - _Offset.y) ) / _Zoom + new Vector2(_ParentWindow.position.width / 2, _ParentWindow.position.height / 2); }
حيث
_ParentWindow هي النافذة التي
سنرسم بها الشبكة ،
_Offset هو الموضع الحالي للشبكة ، و
_Zoom هي درجة التقريب.
ثانياً ، لرسم الخطوط نحتاج إلى طريقة
Handles.DrawLine . تحتوي فئة المقابض على العديد من الطرق المفيدة لتقديم رسومات بسيطة في نوافذ المحرر أو المفتش أو SceneView. في وقت تطوير البرنامج المساعد (الوحدة 5.5) ،
Handles.DrawLine - تخصيص الذاكرة وعموما ببطء شديد. لهذا السبب ، تم تحديد عدد الخطوط المحتملة للعرض بواسطة ثابت
CELLS_IN_LINE_COUNT ، كما تم إنشاء "LOD level" عند التكبير / التصغير لتحقيق معدل إطارات في الثانية مقبول في المحرر.
رسم الشبكة void DrawLODLines(int level) { var gridColor = SkinManager.Instance.CurrentSkin.GridColor; var step0 = (int) Mathf.Pow(10, level); int halfCount = step0 * CELLS_IN_LINE_COUNT / 2 * 10; var length = halfCount * DEFAULT_CELL_SIZE; int offsetX = ((int) (_Offset.x / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; int offsetY = ((int) (_Offset.y / DEFAULT_CELL_SIZE)) / (step0 * step0) * step0; for (int i = -halfCount; i <= halfCount; i += step0) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 0.3f); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } offsetX = (offsetX / (10 * step0)) * 10 * step0; offsetY = (offsetY / (10 * step0)) * 10 * step0; ; for (int i = -halfCount; i <= halfCount; i += step0 * 10) { Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, 1); Handles.DrawLine( GridToGUI(new Vector2(-length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2(length + offsetX * DEFAULT_CELL_SIZE, (i + offsetY) * DEFAULT_CELL_SIZE)) ); Handles.DrawLine( GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, -length + offsetY * DEFAULT_CELL_SIZE)), GridToGUI(new Vector2((i + offsetX) * DEFAULT_CELL_SIZE, length + offsetY * DEFAULT_CELL_SIZE)) ); } }
تقريبا كل شيء جاهز للشبكة. يوصف حركته ببساطة شديدة. _Offset هو في الأساس موقف "الكاميرا" الحالي.
حركة الشبكة public void Move(Vector3 dv) { var x = _Offset.x + dv.x * _Zoom; var y = _Offset.y + dv.y * _Zoom; _Offset.x = x; _Offset.y = y; }
في المشروع نفسه ، يمكنك التعرف على رمز النافذة بشكل عام ومعرفة كيفية إضافة الأزرار إلى النافذة.
نحن نذهب أبعد من ذلك. بالإضافة إلى نافذة منفصلة لرسم الرسومات ، نحتاج إلى تخزين الرسومات بطريقة أو بأخرى. يعد محرك تسلسل الوحدة الداخلي ، الكائن القابل للبرمجة ، أمرًا رائعًا لهذا الغرض. في الواقع ، يسمح لك بتخزين الفئات الموصوفة كأصول في المشروع ، وهو ملائم وموطن لكثير من مطوري الوحدات. على سبيل المثال ، جزء من فئة الشقة المسؤولة عن تخزين معلومات التخطيط بشكل عام
جزء من فئة الشقة public class Apartment : ScriptableObject { #region fields public float Height; public bool IsGenerateOutside; public Material OutsideMaterial; public Texture PlanImage; [SerializeField] private List<Room> _Rooms; [SerializeField] private Rect _Dimensions; private Vector2[] _DimensionsPoints = new Vector2[4]; #endregion
في المحرر ، يبدو هذا في الإصدار الحالي:

هنا ، بالطبع ، تم تطبيق CustomEditor بالفعل ، ولكن مع ذلك ، يمكنك ملاحظة أن المعلمات مثل _Dimensions و Height و IsGenerateOutside و OutsideMaterial و PlanImage يتم عرضها في المحرر.
جميع الحقول والحقول العامة التي تحمل علامة [SerializeField] متسلسلة (أي ، يتم حفظها في ملف في هذه الحالة). يساعد هذا كثيرًا إذا كنت بحاجة إلى حفظ الرسومات ، ولكن عند العمل مع ScriptableObject ، وجميع موارد المحرر ، يجب أن تتذكر أنه من الأفضل استدعاء الأسلوب AssetDatabase.SaveAssets () لحفظ حالة الملفات. خلاف ذلك ، لن يتم حفظ التغييرات. إذا كنت لا تقم بحفظ المشروع بيديك.
الآن سنقوم بتحليل جزء ApartmentCustomInspector جزئيًا ، وكيف يعمل.
فئة ApartmentCustomInspector [CustomEditor(typeof(Apartment))] public class ApartmentCustomInspector : Editor { private Apartment _ThisApartment; private Rect _Dimensions; private void OnEnable() { _ThisApartment = (Apartment) target; _Dimensions = _ThisApartment.Dimensions; } public override void OnInspectorGUI() { TopButtons(); _ThisApartment.Height = EditorGUILayout.FloatField("Height (cm)", _ThisApartment.Height); var dimensions = EditorGUILayout.Vector2Field("Dimensions (cm)", _Dimensions.size).RoundCoordsToInt(); _ThisApartment.PlanImage = (Texture) EditorGUILayout.ObjectField(_ThisApartment.PlanImage, typeof(Texture), false); _ThisApartment.IsGenerateOutside = EditorGUILayout.Toggle("Generate outside (Directional Light)", _ThisApartment.IsGenerateOutside); if (_ThisApartment.IsGenerateOutside) _ThisApartment.OutsideMaterial = (Material) EditorGUILayout.ObjectField( "Outside Material", _ThisApartment.OutsideMaterial, typeof(Material), false); GenerateButton(); var dimensionsRect = new Rect(-dimensions.x / 2, -dimensions.y / 2, dimensions.x, dimensions.y); _Dimensions = dimensionsRect; _ThisApartment.Dimensions = _Dimensions; } private void TopButtons() { GUILayout.BeginHorizontal(); CreateNewBlueprint(); OpenBlueprint(); GUILayout.EndHorizontal(); } private void CreateNewBlueprint() { if (GUILayout.Button( "Create new" )) { var manager = ApartmentsManager.Instance; manager.SelectApartment(manager.CreateOrGetApartment("New Apartment" + GUID.Generate())); } } private void OpenBlueprint() { if (GUILayout.Button( "Open in Builder" )) { ApartmentsManager.Instance.SelectApartment(_ThisApartment); ApartmentBuilderWindow.Create(); } } private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } } }
يعد CustomEditor أداة قوية للغاية تتيح لك حل العديد من المهام النموذجية بأناقة لتمديد المحرر. يقترن ScriptableObject ، فإنه يسمح لك لجعل ملحقات محرر بسيطة ومريحة وبديهية. هذه الفئة أكثر تعقيدًا قليلاً من مجرد إضافة أزرار ، كما ترون في الفصل الدراسي الأصلي أن حقل قائمة _Rooms الخاص [SerializeField] يجري تسلسله. عرضه في المفتش ، أولاً ، لا شيء ، وثانياً - قد يؤدي ذلك إلى أخطاء غير متوقعة وحالات رسم. تكون طريقة OnInspectorGUI مسؤولة عن تقديم المفتش ، وإذا كنت بحاجة فقط لإضافة أزرار ، فيمكنك استدعاء الأسلوب DrawDefaultInspector () فيه وسيتم رسم جميع الحقول.
ثم يتم رسم الحقول والأزرار اللازمة يدويًا. فئة EditorGUILayout نفسها لديها العديد من التطبيقات لمجموعة واسعة من أنواع الحقول التي تدعمها الوحدة. لكن تجسيد الأزرار في الوحدة يتم تطبيقه في فئة GUILayout. كيف يعمل الضغط على زر معالجة في هذه الحالة. OnInspectorGUI - ينفذ على كل حدث إدخال الماوس المستخدم (حركة الماوس ، والنقرات الماوس داخل نافذة المحرر ، وما إلى ذلك). إذا نقر المستخدم على الزر في المربع ، فإن الأسلوب يعود صحيحا ويعالج الطرق الموجودة داخل إذا "وصفها لك" أ. على سبيل المثال:
زر شبكة الجيل private void GenerateButton() { if (GUILayout.Button( "Generate Mesh" )) { MeshBuilder.GenerateApartmentMesh(_ThisApartment); } }
عند النقر فوق الزر "إنشاء شبكة" ، يتم استدعاء الأسلوب الثابت ، وهو المسؤول عن إنشاء شبكة ذات تخطيط معين.
بالإضافة إلى هذه الآليات الأساسية المستخدمة عند توسيع محرر Unity ، أود أن أذكر بشكل منفصل أداة بسيطة للغاية ومريحة للغاية ، والتي ينسى الكثيرون لسبب ما - اختيار. التحديد عبارة عن فئة ثابتة تتيح لك تحديد الكائنات الضرورية في المفتش و ProjectView.
من أجل تحديد كائن ، تحتاج فقط إلى كتابة Selection.activeObject = MyAwesomeUnityObject. وأفضل جزء هو أنه يعمل مع ScriptableObject. في هذا المشروع ، يكون مسؤولاً عن اختيار رسم وغرف في نافذة بها رسومات.
شكرا لاهتمامكم! آمل أن يكون المقال والمشروع مفيدًا لك ، وسوف تتعلم شيئًا جديدًا لنفسك في أحد الأساليب لتوسيع محرر Unity. وكما هو الحال دائمًا -
رابط إلى مشروع جيثب ، حيث يمكنك رؤية المشروع بأكمله. لا يزال رطبًا بعض الشيء ، لكنه مع ذلك يسمح لك بالفعل بوضع خطط ثنائية الأبعاد ببساطة وبسرعة.