
الوحدة هي محرك ألعاب ذو عتبة بعيدة عن الصفر (مقارنة مع نفس Game Maker Studio) ، وفي هذه المقالة سوف أخبرك بالمشاكل التي واجهتها عند البدء في دراستها ، وما هي الحلول التي وجدتها. سوف أصف هذه اللحظات بمثال لعبة الألغاز ثنائية الأبعاد لنظام Android (والتي آمل أن يتم إصدارها قريبًا في سوق Play).
أنا لا أدعي أن هذا حقيقي ، وأنا لا أحثك على التكرار بعد نفسك ، إذا كنت تعرف أفضل طريقة ، أنا فقط أريك كيفية القيام بذلك بنفسك ، وربما شخص ما بدأ للتو في معرفة أن يونيتي سوف تخلق تحفة إيندي جاميديف الخاصة به بأقل جهد.
أنا مهندس تصميم محطة كهرباء ، لكنني كنت دائمًا مهتمًا بالتشفير ، وأنا معتاد على بعض لغات البرمجة. لذلك ، نوافق على إنشاء ألعاب على الوحدة:
- تحتاج إلى معرفة القليل من C # أو JavaScript (على الأقل بناء جملة C).
كل ما سيتم كتابته أدناه ليس تعليميًا للوحدة ، والذي تم تربيته على الشبكة بدون كافي. أدناه سيتم جمع اللحظات الصعبة التي قد تحدث عند إنشاء مشروعك الأول على الوحدة.
تجدر الإشارة إلى أن البرامج النصية المقدمة تحذف معظم منطق اللعبة (الذي يمثل "الأسرار التجارية") ، ولكن تم التحقق من أدائها كأمثلة.
مشكلة واحدة - التوجه
قفل التوجيهكانت الصعوبة الأولى التي ظهرت في نفسي هي أنني لم أعير الاهتمام الواجب لتحسين الواجهة البصرية لتوجيه الشاشة. الحل أبسط - إذا كنت لا تحتاج إلى تغيير في اتجاه الشاشة للعب ، فمن الأفضل حظره. لا حاجة إلى المرونة المفرطة ، فأنت تكتب لعبة مستقلة وليس مشروعًا على الجانب الآخر من مليون دولار. لماذا طن من التحولات الشرطية وتغيير المراسي إذا كانت اللعبة تبدو أفضل في صورة (على سبيل المثال). يمكنك قفل اتجاه الشاشة هنا:
تحرير> إعدادات المشروع> لاعب
أذونات مختلفةمن المهم أيضًا اختبار الواجهة المرئية بدقة مختلفة في الاتجاه المحدد ، وعند الاختبار ، لا تنسَ وجود أجهزة بنسب 4: 3 (جيدًا ، أو 3: 4) ، حتى نتمكن من إضافة 768 × 1024 (أو 1024 × 768).
أفضل المواقعلضبط تحديد موضع وحجم كائنات اللعبة ، من الأفضل استخدام Rect Transform.

المشكلة الثانية - الاتصالات
واجهت مشكلة مشابهة نظرًا لأنني تعرفت أولاً على dev game باستخدام Game Maker Studio ، حيث يعد البرنامج النصي جزءًا كاملاً من كائن اللعبة ، ولديه على الفور إمكانية الوصول الكامل إلى جميع مكونات الكائن. تحتوي الوحدة على نصوص شائعة ، وتتم فقط إضافة مثيلات منها إلى الكائن. عند التحدث بشكل مبسط ، لا يعرف البرنامج النصي بشكل مباشر أي كائن ينفذه حاليًا. لذلك ، عند كتابة البرامج النصية ، يجب أن تأخذ في الاعتبار تهيئة الواجهات للعمل مع مكونات كائن أو مع مكونات كائنات أخرى.
نحن ندرب على القططفي لعبتي يوجد كائن GameField ، على المسرح يوجد مثيل واحد فقط منه ، وهناك أيضًا برنامج نصي يحمل نفس الاسم. الكائن مسؤول عن عرض نتيجة اللعبة وإعادة إنتاج صوت اللعبة بالكامل ، لذلك أرى أنه أكثر اقتصادا بالنسبة للذاكرة (بشكل عام ، تحتوي اللعبة على ثلاثة مصادر صوت فقط - موسيقى خلفية واحدة ، مؤثران صوتيان آخران). يحل البرنامج النصي مشكلات تخزين حساب لعبة ، واختيار AudioClip لتشغيل الصوت ، وبعض منطق اللعبة.
دعونا نلقي الضوء على الصوت بمزيد من التفصيل ، لأن هذا المثال يُظهر بسهولة تفاعل البرنامج النصي مع مكونات الكائن.

بطبيعة الحال ، يجب أن يكون للكائن البرنامج النصي GameField.cs نفسه ومكون مصدر الصوت ، وفي حالتي الثانية بأكملها (سيكون من الواضح لاحقًا السبب).
كما ذكرنا سابقًا ، فإن البرنامج النصي "ليس في علم" أن الكائن يحتوي على مكون مصدر صوت ، وبالتالي فإننا نعلن عن تهيئة الواجهة (في الوقت الحالي ، نعتبر أن هناك مصدر صوت واحد فقط):
private AudioSource Sound; void Start(){ Sound = GetComponent<AudioSource> (); }
الأسلوب GetComponent <component_type> () سيعود المكون الأول من النوع المحدد من الكائن.
بالإضافة إلى مصدر الصوت ، ستحتاج إلى عدة AudioClip:
[Header ("Audio clips")] [SerializeField] private AudioClip OnStart; [SerializeField] private AudioClip OnEfScore; [SerializeField] private AudioClip OnHighScore; [SerializeField] private AudioClip OnMainTimer; [SerializeField] private AudioClip OnBubbMarker; [SerializeField] private AudioClip OnScoreUp;
فيما يلي ، يلزم الأمر بين قوسين معقوفين لـ Inspector`a ، مزيد من التفاصيل
هنا .

يحتوي البرنامج النصي في Inspector الآن على حقول جديدة نسحب فيها الأصوات اللازمة.
بعد ذلك ، قم بإنشاء طريقة SoundPlay في البرنامج النصي الذي يأخذ في AudioClip:
public void PlaySound(AudioClip Clip = null){ Sound.clip = Clip; Sound.Play (); }
لتشغيل الصوت في اللعبة ، نسميها في الوقت المناسب بهذه الطريقة باستخدام المقطع.
يوجد ناقص واحد مهم لهذا النهج ، حيث يمكن تشغيل صوت واحد فقط في كل مرة ، ولكن أثناء اللعبة قد يكون من الضروري تشغيل صوتين أو أكثر ، باستثناء تشغيل موسيقى الخلفية باستمرار.
لمنع نشوب cacophony ، أوصي بتجنب إمكانية التشغيل المتزامن لأكثر من 4-5 أصوات (ويفضل أن تكون بحد أقصى 2-3) ، أعني أصوات اللعبة من الدرجة الأولى (القفز ، النقود المعدنية ، لقطة اللاعب ...) ، من الأفضل أن تنشئ مصدرًا خاصًا لك في الخلفية. صوت على الكائن الذي يجعل هذه الضوضاء (إذا كنت بحاجة إلى صوت ثنائي الأبعاد) أو كائن واحد مسؤول عن كل ضجيج الخلفية (إذا لم تكن "مستوى الصوت" مطلوبة).
في لعبتي ، ليست هناك حاجة للعب في وقت واحد أكثر من اثنين من AudioClips. من أجل تشغيل مضمون لكل من الأصوات الافتراضية ، أضفت مصدران للصوت إلى كائن GameField. لتحديد المكونات في البرنامج النصي ، نستخدم الطريقة
GetComponents<_>()
الذي يعرض مجموعة من جميع مكونات النوع المحدد من الكائن.
سيبدو الرمز كالتالي:
private AudioSource[] Sound;
ستؤثر معظم التغييرات على طريقة PlaySound. أرى إصدارين من هذه الطريقة: "عالمي" (لأي عدد من مصدر الصوت في كائن) و "أخرق" (لـ 2-3 مصدر صوت ، وليس الأكثر أناقة ولكن أقل كثافة في استخدام الموارد).
خيار "الخرقاء" لاثنين من مصدر الصوت (اعتدت عليه)
private void PlaySound(AudioClip Clip = null){ if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } }
يمكنك أن تمتد إلى ثلاثة أو أكثر من مصدر الصوت ، ولكن عدد الشروط سوف تلتهم جميع وفورات الأداء.
الخيار "العالمي"
private void PlaySound(AudioClip Clip = null){ foreach (AudioSource _Sound in Sound) { if (!_Sound.isPlaying) { _Sound.clip = Clip; _Sound.Play (); break; } } }
الوصول إلى مكون خارجيفي الملعب ، هناك العديد من الأمثلة على الجاهزة فيشكا ، مثل شريحة اللعبة. بنيت مثل هذا:
- كائن الأصل مع SpriteRenderer الخاص به؛
- كائنات الطفل مع SpriteRenderer الخاصة بهم.
الكائنات التابعة هي المسؤولة عن رسم جسم الشريحة ولونها وعناصر قابلة للتغيير. يرسم الأصل حد علامة حول الشريحة (يجب تمييز الشريحة النشطة في اللعبة). البرنامج النصي موجود فقط على الكائن الأصل. وبالتالي ، لإدارة العفاريت الفرعية ، يحتاج البرنامج النصي الأصل إلى تحديد هذه العفاريت. قمت بتنظيمها على هذا النحو - في البرنامج النصي ، قمت بإنشاء واجهات للوصول إلى أطفال SpriteRenderer:
[Header ("Graphic objects")] public SpriteRenderer Marker; [SerializeField] private SpriteRenderer Base; [Space] [SerializeField] private SpriteRenderer Center_Red; [SerializeField] private SpriteRenderer Center_Green; [SerializeField] private SpriteRenderer Center_Blue;
الآن النص في المفتش لديه حقول إضافية:

سحب وإفلات الأطفال في الحقول المقابلة يتيح لنا الوصول إليهم في البرنامج النصي.
مثال للاستخدام:
void OnMouseDown(){
استدعاء البرنامج النصي لشخص آخربالإضافة إلى معالجة المكونات الخارجية ، يمكنك أيضًا الوصول إلى البرنامج النصي الخاص بكائن الطرف الثالث ، والعمل مع المتغيرات العامة والأساليب والفئات الفرعية.
سأقدم مثالاً على كائن GameField المعروف بالفعل.
يحتوي البرنامج النصي GameField على طريقة عامة لـ FishkiMarkerDisabled () ، وهي ضرورية "لإزالة" علامة من جميع الرقائق الموجودة في الحقل ويتم استخدامها في عملية تعيين علامة عند النقر فوق شريحة ، حيث لا يمكن أن يكون هناك سوى نشط واحد.
في البرنامج النصي Fishka.cs ، يكون SpriteRenderer Marker عامًا ، أي أنه يمكن الوصول إليه من برنامج نصي آخر. للقيام بذلك ، أضف الإعلان وتهيئة واجهات لجميع مثيلات فئة Fishka في البرنامج النصي GameField.cs (عند إنشاء برنامج نصي ، يتم إنشاء فئة تحمل نفس الاسم فيه) مماثلة لكيفية القيام به لعدة AudioSource:
private Fishka[] Fishki; void Start(){ Fishki = GameObject.FindObjectsOfType (typeof(Fishka)) as Fishka[]; } public void FishkiMarkerDisabled(){ foreach (Fishka _Fishka in Fishki) { _Fishka .Marker.enabled = false; } }
في البرنامج النصي Fishka.cs ، أضف التعريف والتهيئة لواجهة مثيل فئة GameField ، وعندما نقر على الكائن ، سوف نتصل بطريقة FishkiMarkerDisabled () لهذه الفئة:
private GameField gf; void Start(){ gf = GameObject.FindObjectOfType (typeof(GameField)) as GameField; } void OnMouseDown(){ gf.FishkiMarkerDisabled(); Marker.enabled = !Marker.enabled; }
وبالتالي ، يمكنك التفاعل بين البرامج النصية (أو بالأحرى الطبقات) لكائنات مختلفة.
المشكلة ثلاثة - الحفاظات
حارس الحساببمجرد ظهور شيء مثل الحساب في اللعبة ، فإن المشكلة العاجلة هي تخزينه ، سواء أثناء اللعبة أو خارجها ، وأريد أيضًا الاحتفاظ بسجل لتشجيع اللاعب على تجاوزه.
لن أفكر في الخيارات عندما يتم بناء اللعبة بأكملها (القائمة ، اللعبة ، سحب الحساب) في مشهد واحد ، لأنه ، أولاً ، ليست هذه هي أفضل طريقة لإنشاء المشروع الأول ، وثانيًا ، في رأيي ، يجب أن يكون مشهد التحميل الأولي . لذلك ، نحن نوافق على أن هناك أربعة مشاهد في المشروع:
- محمل - مشهد يتم فيه تهيئة كائن الموسيقى في الخلفية (سيكون هناك المزيد في وقت لاحق) ، وتحميل الإعدادات من الحفظ ؛
- القائمة - مشهد مع قائمة.
- لعبة - مشهد لعبة
- النتيجة - مشهد النتيجة ، سجل ، المتصدرين.
ملاحظة: يتم تعيين ترتيب تحميل المشهد في ملف> إعدادات البناء.يتم تخزين النقاط المتراكمة أثناء اللعبة في متغير نقاط فئة GameField. للوصول إلى البيانات عند الانتقال إلى مشهد النتائج ، قم بإنشاء ScoreHolder فئة ثابتة عامة ، حيث نعلن عن متغير لتخزين القيمة وخاصية للحصول عليها وتعيين قيمة هذا المتغير (تم
تجسس الأسلوب بواسطة
apocatastas ):
using UnityEngine; public static class ScoreHolder{ private static int _Score = 0; public static int Score { get{ return _Score; } set{ _Score = value; } } }
لا يلزم إضافة فئة ثابتة عامة إلى أي كائن ، فهي متوفرة على الفور في أي مشهد من أي برنامج نصي.
مثال للاستخدام في فئة GameField في درجات أسلوب انتقال المشهد:
using UnityEngine.SceneManagement; public class GameField : MonoBehaviour { private int Score = 0;
بنفس الطريقة ، يمكنك إضافة حساب قياسي إلى ScoreHolder أثناء اللعبة ، ولكن لن يتم حفظه عند الخروج.
حارس الإعداداتضع في اعتبارك مثال حفظ قيمة المتغير Boolean SoundEffectsMute ، اعتمادًا على الحالة التي لها بها تأثيرات صوتية أو لا.
يتم تخزين المتغير نفسه في الفئة الثابتة العامة SettingsHolder:
using UnityEngine; public static class SettingsHolder{ private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; } } }
الفصل مشابه لـ ScoreHolder ، يمكنك حتى دمجها في فئة واحدة ، لكن في رأيي هذا سلوك خاطئ.
كما ترون من البرنامج النصي ، يتم افتراضيًا إعلان _SoundEffectsMute كاذبة ، لذلك في كل مرة تبدأ فيها اللعبة ، ستعود SettingsHolder.SoundEffectsMute كاذبة بغض النظر عما إذا كان المستخدم قد قام بتغييرها من قبل أم لا (يتم تغييره باستخدام الزر الموجود في مشهد القائمة).
حفظ المتغيراتالأكثر مثالية لتطبيق أندرويد هو استخدام طريقة PlayerPrefs.SetInt للحفظ (لمزيد من التفاصيل ، انظر
الوثائق الرسمية ). هناك خياران للحفاظ على قيمة SettingsHolder.SoundEffectsMute في PlayerPrefs ، دعونا نسميها "بسيطة" و "أنيقة".
الطريقة "البسيطة" (بالنسبة لي هكذا) هي طريقة OnMouseDown () لفئة الزر المذكور أعلاه. يتم تحميل القيمة المخزنة في نفس الفئة ولكن في طريقة البدء ():
using UnityEngine; public class ButtonSoundMute : MonoBehaviour { void Start(){
الطريقة "الأنيقة" ، في رأيي ، ليست هي الأكثر صحة ، لأن تعقيد صيانة الكود ، ولكن هناك شيء ما فيه ، ولا يسعني إلا مشاركته. من مميزات هذه الطريقة أن يتم تعيين أداة ضبط الخاصية SettingsHolder.SoundEffectsMute في وقت لا يتطلب أداءً عاليًا ، ويمكن تحميله (أوه ، رعب) باستخدام PlayerPrefs (القراءة والكتابة إلى ملف). تغيير فئة ثابت العامة SettingsHolder:
using UnityEngine; public static class SettingsHolder { private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; if (_SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } }
سيتم تبسيط أسلوب OnMouseDown للفئة ButtonSoundMute إلى:
void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; }
لا يستحق تحميل getter من خلال القراءة من أحد الملفات ، حيث إنه يشارك في عملية حاسمة في الأداء - في طريقة PlaySound () لفئة GameField:
private void PlaySound(AudioClip Clip = null){ if (!SettingsHolder.SoundEffectsMute) {
بالطريقة أعلاه ، يمكنك تنظيم التخزين داخل اللعبة لأي متغيرات.
المشكلة الخامسة - واحدة للجميع
هذه الموسيقى ستكون أبديةعاجلاً أم آجلاً ، يواجه الجميع مثل هذه المشكلة ، ولم أكن استثناءً. كما هو مخطط ، تبدأ الموسيقى الخلفية في التشغيل حتى في مشهد القائمة ، وإذا لم يتم إيقاف تشغيلها ، فإنها تلعب القائمة واللعبة وتسجيل النتائج على المشاهد دون انقطاع. ولكن إذا تم تثبيت موسيقى خلفية "تشغيل" الكائن في مشهد القائمة ، وعندما تذهب إلى مشهد اللعبة ، يتم إتلافها ويختفي الصوت ، وإذا وضعت نفس الكائن في مشهد اللعبة ، ثم بعد ذلك يتم تشغيل الموسيقى أولاً. تبين أن الحل هو استخدام طريقة DontDestroyOnLoad (هدف الهدف) الموضوعة في طريقة Start () للفئة التي يوجد بها كائن الموسيقى. للقيام بذلك ، قم بإنشاء البرنامج النصي DontDestroyThis.cs:
using UnityEngine; public class DontDestroyThis: MonoBehaviour { void Start(){ DontDestroyOnLoad(this.gameObject); } }

لكي يعمل كل شيء ، يجب أن يكون الكائن "الموسيقي" هو الجذر (في نفس مستوى التسلسل الهرمي للكاميرا الرئيسية).
لماذا الموسيقى الخلفية في محملتوضح لقطة الشاشة أن الكائن "الموسيقي" لا يقع في مشهد القائمة بل على مشهد اللودر. هذا مقياس ناجم عن حقيقة أنه يمكن تحميل مشهد القائمة أكثر من مرة (بعد مشهد النتائج ، والانتقال إلى مشهد القائمة) ، وفي كل مرة يتم تحميله ، سيتم إنشاء كائن "موسيقي" آخر ، ولن يتم حذف الكائن القديم. يمكن أن يتم ذلك كما في مثال
الوثائق الرسمية ، لكنني قررت الاستفادة من حقيقة أن مشهد اللودر مضمون للتحميل مرة واحدة فقط.
في هذا الصدد ، فإن المشاكل الرئيسية التي واجهتها عند تطوير لعبتي الأولى على Unity ، قبل التحميل إلى سوق Play (لم أسجل بعد حساب مطور) ، قد انتهت بنجاح.
PSإذا كانت المعلومات مفيدة ، يمكنك دعم المؤلف ، وسيقوم أخيرًا بتسجيل حساب مطور Android.