تطوير Unity AI: برنامج تعليمي رسومية FSM قائم على xNode

نشرت: 2022-08-12

في "Unity AI Development: A Finite-State Machine Tutorial" ، أنشأنا لعبة تخفي بسيطة - ذكاء اصطناعي معياري قائم على FSM. في اللعبة ، يقوم عميل العدو بدوريات في مساحة الألعاب. عندما يكتشف اللاعب ، يغير العدو حالته ويتبع اللاعب بدلاً من الدوريات.

في هذه المرحلة الثانية من رحلة الوحدة الخاصة بنا ، سنقوم ببناء واجهة مستخدم رسومية (GUI) لإنشاء المكونات الأساسية لآلة الحالة المحدودة (FSM) لدينا بسرعة أكبر ، مع تجربة مطور Unity محسّنة.

تحديث سريع

تم بناء FSM المفصل في البرنامج التعليمي السابق من كتل معمارية مثل نصوص C #. أضفنا إجراءات وقرارات ScriptableObject المخصصة كفئة. أتاح نهج ScriptableObject إمكانية صيانة FSM بسهولة وتخصيصها. في هذا البرنامج التعليمي ، نستبدل ScriptableObjects ScriptableObject الخاصة بالسحب والإفلات في FSM بخيار رسومي.

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

الشروع في العمل مع xNode

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

دعنا نضيف xNode إلى مشروعنا كتبعية Git باستخدام Unity Package Manager:

  1. في الوحدة ، انقر فوق Window> Package Manager لتشغيل نافذة مدير الحزم.
  2. انقر فوق + (علامة الجمع) في الزاوية العلوية اليسرى من النافذة وحدد إضافة حزمة من git URL لعرض حقل نصي.
  3. اكتب أو الصق https://github.com/siccity/xNode.git في مربع النص غير المسماة وانقر فوق الزر " إضافة ".

نحن الآن جاهزون للغوص بعمق وفهم المكونات الرئيسية لـ xNode:

فئة Node يمثل العقدة ، الوحدة الأساسية في الرسم البياني. في هذا البرنامج التعليمي xNode ، نستمد من فئات Node class الجديدة التي تعلن أن العقد مجهزة بوظائف وأدوار مخصصة.
فئة NodeGraph يمثل مجموعة من العقد (مثيلات فئة Node ) والحواف التي تربطهم. في هذا البرنامج التعليمي xNode ، نستمد من NodeGraph فئة جديدة تعالج العقد وتقيّمها.
فئة NodePort يمثل بوابة اتصال ، منفذ من نوع الإدخال أو نوع الإخراج ، يقع بين مثيلات Node في NodeGraph . تعتبر فئة NodePort فريدة بالنسبة إلى xNode.
سمة [Input] إضافة السمة [Input] إلى المنفذ تعينه كمدخل ، مما يتيح للمنفذ تمرير القيم إلى العقدة التي هو جزء منها. فكر في السمة [Input] كمعامل دالة.
سمة [Output] إن إضافة السمة [Output] إلى المنفذ تعينه كإخراج ، مما يتيح للمنفذ تمرير القيم من العقدة التي هو جزء منها. فكر في السمة [Output] على أنها القيمة المرجعة للدالة.

تصور بيئة بناء xNode

في Transition ، نعمل مع الرسوم البيانية حيث تأخذ كل State وانتقال شكل عقدة. يُمكِّن اتصال (اتصالات) الإدخال و / أو الإخراج العقدة من الارتباط بأي أو جميع العقد الأخرى في الرسم البياني الخاص بنا.

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

عقدة الفرع ، التي يمثلها مستطيل كبير في المركز ، تتضمن الرمز الكاذب "If C == True A Else B." يوجد على اليسار ثلاثة مستطيلات ، يحتوي كل منها على سهم يشير إلى العقدة الفرعية: "A (عشوائي)" و "B (عشوائي)" و "C (منطقي)". أخيرًا ، تحتوي عقدة الفرع على سهم يشير إلى مستطيل "الإخراج".
مثال Branch العقدة

لتحويل FSM الحالي إلى رسم بياني ، نقوم بتعديل فئتي State و Transition لتورث فئة Node بدلاً من فئة ScriptableObject . نقوم بإنشاء كائن رسم بياني من النوع NodeGraph لاحتواء جميع كائنات State Transition الخاصة بنا.

تعديل BaseStateMachine لاستخدامه كنوع أساسي

ابدأ في بناء الواجهة الرسومية عن طريق إضافة طريقتين افتراضيتين جديدتين إلى فئة BaseStateMachine الموجودة لدينا:

Init يعيّن الحالة الأولية لخاصية CurrentState
Execute ينفذ الحالة الحالية

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

 using System; using System.Collections.Generic; using UnityEngine; namespace Demo.FSM { public class BaseStateMachine : MonoBehaviour { [SerializeField] private BaseState _initialState; private Dictionary<Type, Component> _cachedComponents; private void Awake() { Init(); _cachedComponents = new Dictionary<Type, Component>(); } public BaseState CurrentState { get; set; } private void Update() { Execute(); } public virtual void Init() { CurrentState = _initialState; } public virtual void Execute() { CurrentState.Execute(this); } // Allows us to execute consecutive calls of GetComponent in O(1) time public new T GetComponent<T>() where T : Component { if(_cachedComponents.ContainsKey(typeof(T))) return _cachedComponents[typeof(T)] as T; var component = base.GetComponent<T>(); if(component != null) { _cachedComponents.Add(typeof(T), component); } return component; } } }

بعد ذلك ، ضمن مجلد FSM الخاص بنا ، لنقم بإنشاء:

FSMGraph مجلد
BaseStateMachineGraph فئة AC # داخل FSMGraph

في الوقت الحالي ، سيرث BaseStateMachineGraph فئة BaseStateMachine فقط:

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { } }

لا يمكننا إضافة وظائف إلى BaseStateMachineGraph حتى نقوم بإنشاء نوع العقدة الأساسي الخاص بنا ؛ لنفعل ذلك بعد ذلك.

تنفيذ NodeGraph وإنشاء نوع عقدة أساسية

ضمن مجلد FSMGraph الذي تم إنشاؤه حديثًا ، سننشئ:

FSMGraph صف

في الوقت الحالي ، سيرث FSMGraph فئة NodeGraph فقط (بدون وظائف إضافية):

 using UnityEngine; using XNode; namespace Demo.FSM.Graph { [CreateAssetMenu(menuName = "FSM/FSM Graph")] public class FSMGraph : NodeGraph { } }

قبل إنشاء فئات لعقدنا ، دعنا نضيف:

FSMNodeBase فئة يتم استخدامها كفئة أساسية من قبل جميع عقدنا

ستحتوي فئة FSMNodeBase على إدخال يسمى Entry من النوع FSMNodeBase لتمكيننا من توصيل العقد ببعضها البعض.

سنضيف أيضًا وظيفتين مساعدتين:

GetFirst استرداد العقدة الأولى المتصلة بالإخراج المطلوب
GetAllOnPort استرداد كافة العقد المتبقية التي تتصل بالإخراج المطلوب
 using System.Collections.Generic; using XNode; namespace Demo.FSM.Graph { public abstract class FSMNodeBase : Node { [Input(backingValue = ShowBackingValue.Never)] public FSMNodeBase Entry; protected IEnumerable<T> GetAllOnPort<T>(string fieldName) where T : FSMNodeBase { NodePort port = GetOutputPort(fieldName); for (var portIndex = 0; portIndex < port.ConnectionCount; portIndex++) { yield return port.GetConnection(portIndex).node as T; } } protected T GetFirst<T>(string fieldName) where T : FSMNodeBase { NodePort port = GetOutputPort(fieldName); if (port.ConnectionCount > 0) return port.GetConnection(0).node as T; return null; } } }

في النهاية ، سيكون لدينا نوعان من عقد الحالة ؛ دعنا نضيف فصل دراسي لدعم هذه:

BaseStateNode فئة أساسية لدعم كل من StateNode و RemainInStateNode
 namespace Demo.FSM.Graph { public abstract class BaseStateNode : FSMNodeBase { } }

بعد ذلك ، قم بتعديل فئة BaseStateMachineGraph :

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { public new BaseStateNode CurrentState { get; set; } } }

هنا ، قمنا بإخفاء خاصية CurrentState الموروثة من الفئة الأساسية وغيرنا نوعها من BaseState إلى BaseStateNode .

إنشاء اللبنات الأساسية لرسمنا البياني FSM

بعد ذلك ، لتشكيل وحدات البناء الرئيسية لـ FSM ، دعنا نضيف ثلاث فئات جديدة إلى مجلد FSMGraph بنا:

StateNode يمثل حالة الوكيل. عند التنفيذ ، تتكرر StateNode عبر TransitionNode s المتصلة بمنفذ الإخراج الخاص بـ StateNode (تم استرداده بواسطة طريقة مساعدة). يستعلم StateNode كل واحد عما إذا كان سيتم نقل العقدة إلى حالة مختلفة أو ترك حالة العقدة كما هي.
RemainInStateNode يشير إلى أن العقدة يجب أن تظل في الحالة الحالية.
TransitionNode يتخذ قرار الانتقال إلى حالة مختلفة أو البقاء في نفس الحالة.

في البرنامج التعليمي السابق للوحدة FSM ، يتكرر فصل State على قائمة الانتقالات. هنا في xNode ، تعمل StateNode State للتكرار عبر العقد التي تم استردادها عبر طريقة مساعد GetAllOnPort .

أضف الآن سمة [Output] إلى الاتصالات الصادرة (عقد الانتقال) للإشارة إلى أنها يجب أن تكون جزءًا من واجهة المستخدم الرسومية. من خلال تصميم xNode ، تنشأ قيمة السمة في العقدة المصدر: العقدة التي تحتوي على الحقل المميز بالسمة [Output] . نظرًا لأننا نستخدم سمات [Output] و [Input] لوصف العلاقات والاتصالات التي سيتم تعيينها بواسطة xNode GUI ، فلا يمكننا التعامل مع هذه القيم كما نفعل عادةً. ضع في اعتبارك كيف نكرر من خلال Actions مقابل Transitions :

 using System.Collections.Generic; namespace Demo.FSM.Graph { [CreateNodeMenu("State")] public sealed class StateNode : BaseStateNode { public List<FSMAction> Actions; [Output] public List<TransitionNode> Transitions; public void Execute(BaseStateMachineGraph baseStateMachine) { foreach (var action in Actions) action.Execute(baseStateMachine); foreach (var transition in GetAllOnPort<TransitionNode>(nameof(Transitions))) transition.Execute(baseStateMachine); } } }

في هذه الحالة ، يمكن أن يحتوي إخراج Transitions على عقد متعددة مرفقة به ؛ يتعين علينا استدعاء الأسلوب المساعد GetAllOnPort للحصول على قائمة اتصالات [Output] .

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

 namespace Demo.FSM.Graph { [CreateNodeMenu("Remain In State")] public sealed class RemainInStateNode : BaseStateNode { } }

في هذه المرحلة ، لا تزال فئة TransitionNode غير مكتملة ولن يتم تجميعها. سيتم مسح الأخطاء المرتبطة بمجرد تحديث الفصل.

لبناء TransitionNode ، نحتاج إلى الالتفاف حول متطلبات xNode بأن قيمة المخرجات تنشأ في العقدة المصدر - كما فعلنا عندما أنشأنا StateNode . يتمثل أحد الاختلافات الرئيسية بين StateNode و TransitionNode في أن إخراج TransitionNode قد يتم إرفاقه بعقدة واحدة فقط. في حالتنا ، سيقوم GetFirst بإحضار العقدة المرفقة بكل من منافذنا (عقدة حالة واحدة للانتقال إليها في الحالة الحقيقية وأخرى للانتقال إليها في الحالة الخاطئة):

 namespace Demo.FSM.Graph { [CreateNodeMenu("Transition")] public sealed class TransitionNode : FSMNodeBase { public Decision Decision; [Output] public BaseStateNode TrueState; [Output] public BaseStateNode FalseState; public void Execute(BaseStateMachineGraph stateMachine) { var trueState = GetFirst<BaseStateNode>(nameof(TrueState)); var falseState = GetFirst<BaseStateNode>(nameof(FalseState)); var decision = Decision.Decide(stateMachine); if (decision && !(trueState is RemainInStateNode)) { stateMachine.CurrentState = trueState; } else if(!decision && !(falseState is RemainInStateNode)) stateMachine.CurrentState = falseState; } } }

دعنا نلقي نظرة على النتائج الرسومية من الكود الخاص بنا.

إنشاء الرسم البياني المرئي

مع فرز جميع فئات FSM ، يمكننا المضي قدمًا في إنشاء FSM Graph الخاص بنا لعامل العدو في اللعبة. في نافذة مشروع الوحدة ، انقر بزر الماوس الأيمن فوق مجلد EnemyAI واختر: إنشاء> FSM> رسم بياني FSM . لتسهيل التعرف على الرسم البياني ، دعنا نعيد تسميته EnemyGraph .

في نافذة محرر xNode Graph ، انقر بزر الماوس الأيمن للكشف عن قائمة منسدلة تسرد الحالة والانتقال و RemainInState . إذا كانت النافذة غير مرئية ، فانقر نقرًا مزدوجًا فوق ملف EnemyGraph لبدء تشغيل نافذة محرر xNode Graph.

  1. لإنشاء دولتي Chase و Patrol :

    1. انقر بزر الماوس الأيمن واختر الحالة لإنشاء عقدة جديدة.

    2. اسم العقدة Chase .

    3. ارجع إلى القائمة المنسدلة ، اختر الحالة مرة أخرى لإنشاء عقدة ثانية.

    4. اسم العقدة Patrol .

    5. قم بسحب وإسقاط إجراءات Chase و Patrol الحالية إلى الحالات المطابقة التي تم إنشاؤها حديثًا.

  2. لإنشاء الانتقال:

    1. انقر بزر الماوس الأيمن واختر انتقال لإنشاء عقدة جديدة.

    2. قم بتعيين كائن LineOfSightDecision إلى حقل Decision الخاص بالانتقال.

  3. لإنشاء عقدة RemainInState :

    1. انقر بزر الماوس الأيمن واختر RemainInState لإنشاء عقدة جديدة.
  4. لتوصيل الرسم البياني:

    1. قم بتوصيل إخراج Transitions عقدة Patrol بإدخال Entry العقدة Transition .

    2. قم بتوصيل إخراج True State الخاص بالعقدة Transition بإدخال Entry عقدة Chase .

    3. قم بتوصيل إخراج False State الخاص بالعقدة Transition بإدخال Entry عقدة Remain In State .

يجب أن يبدو الرسم البياني كما يلي:

أربع عقد ممثلة بأربعة مستطيلات ، كل منها به دوائر إدخال في الجانب الأيسر العلوي. من اليسار إلى اليمين ، تعرض عقدة حالة باترول إجراءً واحدًا: إجراء دورية. تتضمن عقدة حالة باترول أيضًا دائرة إخراج انتقالات في الجانب الأيمن السفلي منها والتي تتصل بدائرة الدخول في العقدة الانتقالية. تعرض العقدة الانتقالية قرارًا واحدًا: LineOfSight. يحتوي على دائرتين للإخراج في الجانب الأيمن السفلي ، وهما True State و False State. ترتبط True State بدائرة الدخول لهيكلنا الثالث ، عقدة حالة Chase. تعرض عقدة حالة Chase إجراءً واحدًا: Chase Action. تحتوي عقدة حالة Chase على دائرة إخراج انتقالات. تتصل الدائرة الثانية من دائرتي الإخراج في Transition ، False State ، بدائرة Entry للهيكل الرابع والأخير لدينا ، عقدة RemainInState (التي تظهر أسفل عقدة حالة Chase).
نظرة أولية على الرسم البياني FSM الخاص بنا

لا شيء في الرسم البياني يشير إلى العقدة - حالة Patrol أو Chase - هي العقدة الأولية. تكتشف فئة BaseStateMachineGraph أربع عقد ولكن ، مع عدم وجود مؤشرات ، لا يمكنها اختيار الحالة الأولية.

لحل هذه المشكلة ، دعنا ننشئ:

FSMInitialNode فئة يتم تسمية إخراجها الفردي من النوع StateNode باسم InitialNode

يشير الناتج الأولي InitialNode إلى الحالة الأولية. بعد ذلك ، في FSMInitialNode ، أنشئ:

NextNode خاصية تمكننا من جلب العقدة المتصلة InitialNode
 using XNode; namespace Demo.FSM.Graph { [CreateNodeMenu("Initial Node"), NodeTint("#00ff52")] public class FSMInitialNode : Node { [Output] public StateNode InitialNode; public StateNode NextNode { get { var port = GetOutputPort("InitialNode"); if (port == null || port.ConnectionCount == 0) return null; return port.GetConnection(0).node as StateNode; } } } }

الآن بعد أن أنشأنا فئة FSMInitialNode ، يمكننا توصيلها بإدخال Entry للحالة الأولية وإرجاع الحالة الأولية عبر خاصية NextNode .

لنعد إلى الرسم البياني ونضيف العقدة الأولية. في نافذة محرر xNode:

  1. انقر بزر الماوس الأيمن واختر عقدة أولية لإنشاء عقدة جديدة.
  2. قم بإرفاق إخراج FSM Node بإدخال Entry عقدة Patrol .

يجب أن يبدو الرسم البياني الآن كما يلي:

نفس الرسم البياني كما في صورتنا السابقة ، مع مستطيل أخضر مضاف من FSM Node إلى يسار المستطيلات الأربعة الأخرى. يحتوي على مخرجات عقدة أولية (ممثلة بدائرة زرقاء) التي تتصل بمدخل "دخول" عقدة باترول (ممثلة بدائرة حمراء داكنة).
الرسم البياني FSM الخاص بنا مع العقدة الأولية المرفقة بحالة الدورية

لجعل حياتنا أسهل ، سنضيف إلى FSMGraph :

InitialState ملكية

في المرة الأولى التي نحاول فيها استرداد قيمة خاصية InitialState ، سوف يجتاز محصل الخاصية جميع العقد في الرسم البياني الخاص بنا أثناء محاولته العثور على FSMInitialNode . بمجرد FSMInitialNode ، نستخدم خاصية NextNode للعثور على عقدة الحالة الأولية الخاصة بنا:

 using System.Linq; using UnityEngine; using XNode; namespace Demo.FSM.Graph { [CreateAssetMenu(menuName = "FSM/FSM Graph")] public sealed class FSMGraph : NodeGraph { private StateNode _initialState; public StateNode InitialState { get { if (_initialState == null) _initialState = FindInitialStateNode(); return _initialState; } } private StateNode FindInitialStateNode() { var initialNode = nodes.FirstOrDefault(x => x is FSMInitialNode); if (initialNode != null) { return (initialNode as FSMInitialNode).NextNode; } return null; } } }

بعد ذلك ، في BaseStateMachineGraph الخاص بنا ، دعنا نشير إلى FSMGraph أساليب Init Execute في BaseStateMachine . يؤدي تجاوز Init إلى تعيين CurrentState كحالة أولية للرسم البياني ، وتجاوز Execute الاستدعاءات Execute في CurrentState :

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { [SerializeField] private FSMGraph _graph; public new BaseStateNode CurrentState { get; set; } public override void Init() { CurrentState = _graph.InitialState; } public override void Execute() { ((StateNode)CurrentState).Execute(this); } } }

الآن ، دعنا نطبق الرسم البياني على كائن العدو ، ونراه عمليًا.

اختبار الرسم البياني FSM

استعدادًا للاختبار ، في نافذة مشروع Unity Editor's Project:

  1. افتح الأصل SampleScene.

  2. حدد موقع كائن لعبة Enemy في نافذة التسلسل الهرمي للوحدة.

  3. استبدل مكون BaseStateMachine بمكون BaseStateMachineGraph :

    1. انقر فوق إضافة مكون وحدد البرنامج النصي BaseStateMachineGraph الصحيح.

    2. قم بتعيين رسم FSM الخاص بنا ، EnemyGraph ، إلى حقل Graph لمكون BaseStateMachineGraph .

    3. احذف مكون BaseStateMachine (حيث لم تعد هناك حاجة إليه) بالنقر بزر الماوس الأيمن وتحديد إزالة المكون .

يجب أن يبدو كائن لعبة Enemy كما يلي:

من أعلى إلى أسفل ، في شاشة المفتش ، يوجد علامة اختيار بجوار العدو. يتم تحديد "Player" في القائمة المنسدلة للعلامات ، ويتم تحديد "العدو" في القائمة المنسدلة للطبقة. تعرض القائمة المنسدلة التحويل الموضع والدوران والمقياس. يتم ضغط قائمة الكبسولة المنسدلة ، وتظهر القوائم المنسدلة Mesh Renderer و Capsule Collider و Nav Mesh Agent مضغوطة بفحص إلى اليسار. تعرض قائمة Enemy Sight Sensor القائمة المنسدلة Script and Ignore Mask (قناع التجاهل). تعرض القائمة المنسدلة PatrolPoints البرنامج النصي وأربع نقاط PatrolPoints. توجد علامة اختيار بجانب القائمة المنسدلة للرسم البياني الآلي للحالة الأساسية (البرنامج النصي). يعرض البرنامج النصي "BaseStateMachineGraph" ، وتعرض الحالة الأولية "بلا (الحالة الأساسية) ، ويظهر الرسم البياني" EnemyGraph (FSM Graph). "وأخيرًا ، يتم ضغط القائمة المنسدلة Blue Enemy (Material) ، ويظهر زر" Add Component "أدناه هو - هي.
كائن لعبة Enemy

هذا هو! الآن لدينا FSM معياري مع محرر رسومي. يُظهر النقر فوق الزر " تشغيل " أن العدو الذي تم إنشاؤه بيانياً يعمل تمامًا مثل عدو ScriptableObject الذي تم إنشاؤه مسبقًا.

المضي قدمًا: تحسين ولايات ميكرونيزيا الموحدة

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

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

هذه التحسينات ليست تجميلية فقط. تساعد مراجع اللون والحجم في تحديد مكان ووقت التصحيح. الرسم البياني الذي يسهل على العين هو أيضًا أبسط في التقييم والتحليل والفهم. أي خطوات تالية متروكة لك - مع وجود أساس محرر الرسوم لدينا ، لا يوجد حد لتحسينات تجربة المطور التي يمكنك إجراؤها.

مزيد من القراءة على مدونة Toptal Engineering:

  • الأخطاء العشرة الأكثر شيوعًا التي يرتكبها مطورو الوحدة
  • الوحدة مع MVC: كيفية رفع مستوى تطوير لعبتك
  • إتقان الكاميرات ثنائية الأبعاد في الوحدة: برنامج تعليمي لمطوري الألعاب
  • أفضل ممارسات Unity والنصائح المقدمة من Toptal Developers

تعرب مدونة Toptal Engineering عن امتنانها لـ Goran Lalic لخبرته ومراجعته الفنية لهذه المقالة.