السلام عليكم ورحمة الله وبركاته
كثيرًا ما نحتاج إلى عرض المتغيرات الموجودة في ملفاتنا البرمجية في الـinspector أثناء العمل في محرك Unity.
لكي نستطيع أن نقوم بتعيين وحفظ قيمة للمتغير لكل عنصر في كل كائن يستخدم الملف البرمجي في المشروع .. سواء كان في مشهد ما أو في مورد prefab.
لكن الكثير غالبًا ما يقوم بفعل ذلك بطريقة الـpublic fields وهي غير جيدة، لذا سأقدم لكم حلاً وطريقة أفضل.
المحتوى
- مقدمة عن عرض المتغيرات في الinspector
- مشاكل تنتج من جعل المتغير عامًا
- الحل: خاصية SerializeField
- وظيفة هذه الخاصية – ماهو ال Serialization؟
- إضافة آليات أخرى للوصول
لنبدأ بمثال، إذا كان لدي ملف برمجي مختص بحركة السيارة CarMovement، يحتوي على متغير يحدد سرعة السيارة:
public class CarMovement : MonoBehaviour{ ... public float carSpeed = 28f; /* لاحظ أن المتغير عام أي يمكن الوصول إليه من أي ملف في المشروع */ /* دوال وعمليات تقوم بتحريك السيارة */ ... }
إذا أردتُ إنشاء عدة سيارات مختلفة السرعة، فقط أُضيف الملف البرمجي لكائنات السيارة GameObject في المشهد، وفي محرر الـinspector للملف البرمجي، أعين قيمة متغير السرعة:
لاحظ أنه لكي نجعل المتغير يظهر في المحرر، أضفنا متحكم الوصول public مما يجعل المحرك يحفظ المتغير ويعرضه، ولكن هذا يجلب معه بعض المشاكل.
مشاكل تنتج من جعل المتغير عامًا public
متحكم الوصول public يجعل المتغير مفتوح للجميع للحصول على قيمته والتعديل عليه.
وبما أنه في مثالنا المتغير يجب أن يتحكم فيه الملف البرمجي الخاص بالحركة فقط، سيكون من السيء جعله عامًا (لا يجب أن يقوم ملف برمجي مختص بالواجهة – مثلاُ – أن يقوم بتغيير السرعة!)
قد تقول “إنني لن أقوم بتعديله من خارج الملف، فأنا أعرف ماذا أكتب”، ولكن ماذا أثناء العمل مع فريق؟
ماذا إذا قام أحد المبرمجين حقًا بالوصول إلى متغير السرعة من ملف برمجي مختص بالواجهات؟ (ليوقف السيارة عند الضغط على زر معين مثلاً، هدفه نبيل أليس كذلك؟)
وإذا كان مبرمج يعمل لوحده، ماذا إذا نسي أهمية هذا المتغير في الملف البرمجي بعد – تقريبًا – 6 أشهر؟
نعم، في مثالنا، لن يسبب التعديل على متغير السرعة مشاكل كبيرة..
فالتغيير سوف يكون ظاهرًا مباشرة في سرعة السيارة أثناء اللعب، والمتغير باسمه يوضح ماذا سيحصل إذا تم تغيير قيمته. لذا لن نقع في مشاكل حقًا
لكننا يجب أن نقيس على أسوأ سيناريو محتمل..
قد تقوم مثلاً بتعريف متغير عام حساس مختص بمعامل انزلاق السيارة، الذي تغييره بالخطأ سيسبب مشاكل للاعب قد يصعب اكتشافها من قبل المطور.
ونحن لا نتكلم عن متغير واحد فقط، بل عن مشروع كبير يحتوي على عشرات بل مئات الملفات البرمجية التي تحتوي على العديد من المتغيرات.
فإذا كنت تسمح بالوصول إلى هذه المتغيرات جميعها من أي مكان في المشروع، سينتج عن هذا تداخل وترابط يجعل فهم الأنظمة البرمجية في مشروعك وكيفية تواصلها مع بعضها أمرًا صعبًا.
مما بدوره يجعل حل المشاكل البرمجية كابوس مرعب.
إذًا، كيف لنا عرض المتغيرات في المحرر، دون جعلها عامة الوصول؟
الحل: خاصية SerializeField
سنجعل المتغير خاص الوصول ونضيف هذه الخاصية إليه، هكذا:
... [SerializeField] private float carSpeed = 28f; ...
الآن أصبح بإمكاننا التعديل علي قيمته من محرر الـinspector للكائن الذي يحتوي على الملف البرمجي..
ولن نسمح للملفات البرمجية الأخرى بالتعديل على قيمة المتغير.
ما الذي تفعله هذه الخاصية؟ وماذا نعني ب Serialize؟
لكي يُعرَض المتغير في المحرر، يجب أن يحفظ مع المعلومات المخزنة للمشاهد أو الـprefabs، وهذا الحفظ يجب أن يكون على أقراص الجهاز.
أي أنه عند إغلاق المشروع ثم فتحه، تبقى قيمة متغير السرعة كما هي.
ولفعل ذلك يقوم محرك Unity بعملية تسمى Serialization أو تحويل البيانات:
حيث تقوم بأخذ البيانات المطلوب حفظها وتحويلها إلى مصفوفات من البتات (0 و 1) ثم تمريريها إلى القرص الصلب لحفظها (مهما كان نوعه)
ولكن هذه العملية تتطلب وقتًا، لذلك محرك Unity يقوم بحفظ البيانات المطلوبة المهمة فقط،
وبالنسبة لمتغيراتك في ملفاتك البرمجية، يحدد Unity من سيتم حفظه اعتبارًا على:
- متحكم الوصول public – أي متغير عام في ملف برمجي يتم حفظ قيمته مع بيانات الكائن المصاحب للملف.
- خاصية SerializeField – تفرض حفظ قيمة المتغير مهما كان متحكم الوصول الخاص به. (مثل private)
- وبالعكس، يمكنك منع المحرك من حفظ قيمة متغير ما عن طريق خاصية System.NonSerialized.
إضافة آليات أخرى للوصول:
جعل المتغير خاصًا ليس أفضل حل لمشكلتنا، فالآن ظهرت مشكلة أخرى وهي الوصول إلى المتغير.
ماذا إذا أردتُ معرفة قيمته من ملف برمجي آخر ، لأقومَ مثلاً بعرض السرعة على الواجهة في مثالنا؟
يمكننا إنشاء دوال Getter و Setter، حسب الحاجة، كالآتي:
/* تخبرنا ماهي سرعة السيارة */ public float GetCarSpeed(){ return carSpeed; } public void SetCarSpeed(float newSpeed){ carSpeed = newSpeed; }
من حقك أن تتسائل: “مهلاً لحظة، ما الفائدة من إضافة دالة لل Set، تسمح بتغيير قيمة المتغير من الخارج، في حين أن هذا المقال كله يحرّض على عدم السماح بهذا؟”
أعتقد أنك قد أسأت فهم المشكلة في هذه الحالة، مشكلتنا كانت بالوصول المطلق غير المحدد للمتغير، ولكننا الآن نقوم بتغيير قيمته في داخل الملف البرمجي نفسه.
أعني، مثلاً، إذا لم نكن نريد زيادة سرعة السيارة عن maxSpeed 100، يمكننا إضافة جملة شرط بسيطة:
float maxSpeed = 50f; public void SetCarSpeed(float newSpeed){ if(newSpeed > maxSpeed){ print ("Can't Move faster than 100); return; } carSpeed = newSpeed; }
نحن الآن نمتلك السلطة الكاملة على المتغير.
تجميل كتابة دوال ال get وال set: استخدام الخصائص Properties
أحد مزايا لغة C# هي الخصائص، والتي تعتبر دوال يمكن أن تُعالج وتَحسب وتُعَدّل على المتغيرات الخاصة.
لا تختلف فكرتها كثيرًا عن تطبيق دوال ال get وال set، لكنها مرنة أكثر وسهلة الاستخدام وسريعة التطبيق.
الخاصية بأبسط أشكالها قد تكون كالآتي:
public float carSpeed{ get; private set;}
هذه تجعل قيمة الخاصية قابلة للوصول من الخارج، ولكن لا يمكن تعديلها إلا من داخل الملف نفسه.
ولكن المشكلة أن الخصائص لا يمكن حفظها لتعرض في الـInspector في محرك Uniy.
لذلك يمكننا “تغليف” المتغير بالخاصية بالآتي:
[SerializeField] private float carSpeed; public float CarSpeed{ get{ return carSpeed; } set{ /*نقوم بعمليات التأكد هنا*/ if(value <= maxSpeed) carSpeed = value } }
هكذا مازال التعديل على المتغير مضمون ومحمي والوصول لقيمته مفتوح للجميع، ويمكننا تعيين قيمة للمتغير الأصلي من الـinspector.
وتذكر أنه يمكنك استخدام private set بدلاً من set لتقيّد الوصول من الملف نفسه فقط.
ملاحظة: إذا كنت ستقوم بالكثير من عمليات التحقق والتأكد داخل عنصر ال set { … } للخاصية، فيفضل استعمال الدوال مع متغير خاص بدلاً من الخصائص properties. حفاظًا على الأداء
ختامًا، أتمنى أن تكونوا قد استفدتم من النصيحة الخفيفة هذه، ولا تنسو مشاركتها مع المبرمجين الآخرين ليستفيدوا منها أيضًا.
وأرحب بأسئلتكم أو آرائكم في التعليقات في الأسفل 🙂