قد يكون العمل مع مشاهد متعددة في وقت واحد في Unity أمرًا صعبًا ، وتحسين سير العمل هذا له تأثير كبير على أداء لعبتك وإنتاجية فريقك. سنشارك معك اليوم تلميحات حول إعداد مهام سير العمل باستخدام Scene التي يمكنها توسيع نطاق المشروعات الأكبر.
تحتوي معظم الألعاب على مستويات متعددة ، وغالبًا ما تحتوي المستويات على أكثر من مشهد واحد. في الألعاب التي تكون فيها المشاهد صغيرة نسبيًا ، يمكنك تقسيمها إلى أجزاء مختلفة باستخدام المباني الجاهزة. ومع ذلك ، من أجل الاتصال بها أو إنشاء مثيل لها أثناء اللعبة ، تحتاج إلى الرجوع إلى كل هذه المباني الجاهزة. هذا يعني أنه كلما كبرت لعبتك واحتلت هذه الروابط مساحة أكبر من الذاكرة ، يصبح استخدام المشاهد أكثر فاعلية.
يمكنك تقسيم المستويات إلى مشهد واحد أو أكثر من مشاهد الوحدة. يصبح العثور على أفضل طريقة لإدارتها هو النقطة الأساسية. يمكنك فتح العديد من المشاهد في وقت واحد في المحرر وفي وقت التشغيل باستخدام ميزة التحرير متعدد المشاهد . يؤدي تقسيم الطبقات إلى مشاهد متعددة أيضًا إلى تسهيل العمل الجماعي ، حيث يتجنب تعارضات الدمج في أدوات التعاون مثل Git و SVN و Unity Collaborate والمزيد.
إدارة مشاهد متعددة لإنشاء مستوى
في الفيديو أدناه ، سنوضح لك كيفية تحميل مستوى بشكل أكثر كفاءة عن طريق تقسيم منطق اللعبة وأجزاء مختلفة من المستوى إلى عدة مشاهد Unity منفصلة. بعد ذلك ، باستخدام وضع تحميل المشهد الإضافي عند تحميل تلك المشاهد ، نقوم بتحميل وتفريغ الأجزاء الضرورية جنبًا إلى جنب مع منطق اللعبة الذي لا يذهب إلى أي مكان. نحن نستخدم المباني الجاهزة كمثبتات للمشاهد ، والتي توفر أيضًا قدرًا كبيرًا من المرونة عند العمل كفريق ، نظرًا لأن كل مشهد هو جزء من المستوى ويمكن تحريره بشكل منفصل.
لا يزال بإمكانك تحميل هذه المشاهد في وضع التحرير والضغط على Play في أي وقت لعرضها جميعًا معًا أثناء العمل على تصميم المستوى.
سنعرض طريقتين مختلفتين لتحميل هذه المشاهد. الأول يعتمد على المسافة ، والتي تعمل بشكل جيد مع المستويات غير الداخلية مثل العالم المفتوح. هذه التقنية مفيدة أيضًا لبعض التأثيرات المرئية (مثل الضباب) لإخفاء عملية التحميل والتفريغ.
الطريقة الثانية تستخدم Trigger للتحقق من المشاهد التي يجب تحميلها ، وهي أكثر كفاءة عند العمل مع التصميمات الداخلية.
الآن وقد اكتشفنا كل شيء داخل المستوى ، يمكننا إضافة طبقة إضافية فوقه لإدارة المستويات بأنفسهم بشكل أفضل.
التحكم في مستويات اللعبة المتعددة باستخدام ScriptableObjects
نريد أن نتتبع المشاهد المختلفة في كل مستوى ، وكذلك جميع المستويات طوال طريقة اللعب بأكملها. إحدى الطرق الممكنة لتحقيق ذلك هي استخدام المتغيرات الثابتة والأحرف المفردة في البرامج النصية أحادية السلوك ، لكن هذا الحل ليس سلسًا. يشير استخدام المفرد إلى روابط قوية بين أنظمتك ، لذا فهي ليست معيارية بشكل صارم. لا يمكن أن توجد الأنظمة بشكل منفصل وستعتمد دائمًا على بعضها البعض.
مشكلة أخرى تتعلق باستخدام المتغيرات الثابتة. نظرًا لأنه لا يمكنك رؤيتها في المفتش ، فأنت بحاجة إلى تعريفها من خلال الكود ، مما يجعل من الصعب على الفنانين أو مصممي المستوى اختبار اللعبة. عندما تحتاج إلى مشاركة البيانات بين المشاهد المختلفة ، فإنك تستخدم متغيرات ثابتة مع DontDestroyOnLoad ، ولكن يجب تجنب الأخير كلما أمكن ذلك.
لتخزين المعلومات حول المشاهد المختلفة ، يمكنك استخدام ScriptableObject ، فئة قابلة للتسلسل تُستخدم بشكل أساسي لتخزين البيانات. على عكس البرامج النصية أحادية السلوك ، والتي تُستخدم كمكونات مرتبطة بـ GameObjects ، فإن ScriptableObjects ليست مرتبطة بأي كائن GameObject وبالتالي يمكن استخدامها بواسطة مشاهد مختلفة في جميع أنحاء المشروع.
سيكون من الجيد أن تكون قادرًا على استخدام هذا الهيكل للمستويات بالإضافة إلى مشاهد القائمة في لعبتك. للقيام بذلك ، قم بإنشاء فئة GameScene تحتوي على العديد من الخصائص العامة للمستويات والقوائم.
public class GameScene : ScriptableObject
{
[Header("Information")]
public string sceneName;
public string shortDescription;
[Header("Sounds")]
public AudioClip music;
[Range(0.0f, 1.0f)]
public float musicVolume;
[Header("Visuals")]
public PostProcessProfile postprocess;
}
لاحظ أن الفئة ترث من ScriptableObject ، وليس MonoBehaviour. يمكنك إضافة العديد من الخصائص حسب الحاجة للعبتك. بعد هذه الخطوة ، يمكنك إنشاء فئات المستوى والقائمة التي ترث من فئة GameScene التي أنشأتها للتو ، لذا فهي أيضًا كائنات ScriptableObjects.
[CreateAssetMenu(fileName = "NewLevel", menuName = "Scene Data/Level")]
public class Level : GameScene
{
// ,
[Header("Level specific")]
public int enemiesCount;
}
تسمح لك إضافة سمة CreateAssetMenu في الجزء العلوي بإنشاء مستوى جديد من قائمة الأصول في الوحدة. يمكنك أن تفعل الشيء نفسه بالنسبة لفئة القائمة. يمكنك أيضًا إضافة تعداد لتتمكن من تحديد نوع القائمة من المفتش.
public enum Type
{
Main_Menu,
Pause_Menu
}
[CreateAssetMenu(fileName = "NewMenu", menuName = "Scene Data/Menu")]
public class Menu : GameScene
{
// ,
[Header("Menu specific")]
public Type type;
}
الآن بعد أن أصبح بإمكانك إنشاء المستويات والقوائم ، دعنا نضيف قاعدة بيانات تسردها (المستويات والقوائم) للراحة. يمكنك أيضًا إضافة فهرس لتتبع المستوى الحالي للاعب. يمكنك بعد ذلك إضافة طرق لتحميل لعبة جديدة (في هذه الحالة سيتم تحميل المستوى الأول) ، لتكرار المستوى الحالي ، والانتقال إلى المستوى التالي. لاحظ أنه يتم تغيير الفهرس فقط في هذه الطرق الثلاث ، لذلك يمكنك إنشاء طريقة تقوم بتحميل المستوى بالفهرس لإعادة استخدامه.
[CreateAssetMenu(fileName = "sceneDB", menuName = "Scene Data/Database")]
public class ScenesData : ScriptableObject
{
public List<Level> levels = new List<Level>();
public List<Menu> menus = new List<Menu>();
public int CurrentLevelIndex=1;
/*
*
*/
//
public void LoadLevelWithIndex(int index)
{
if (index <= levels.Count)
{
//
SceneManager.LoadSceneAsync("Gameplay" + index.ToString());
//
SceneManager.LoadSceneAsync("Level" + index.ToString() + "Part1", LoadSceneMode.Additive);
}
// ,
else CurrentLevelIndex =1;
}
//
public void NextLevel()
{
CurrentLevelIndex++;
LoadLevelWithIndex(CurrentLevelIndex);
}
//
public void RestartLevel()
{
LoadLevelWithIndex(CurrentLevelIndex);
}
// ,
public void NewGame()
{
LoadLevelWithIndex(1);
}
/*
*
*/
//
public void LoadMainMenu()
{
SceneManager.LoadSceneAsync(menus[(int)Type.Main_Menu].sceneName);
}
//
public void LoadPauseMenu()
{
SceneManager.LoadSceneAsync(menus[(int)Type.Pause_Menu].sceneName);
}
توجد أيضًا طرق قائمة ، ويمكنك استخدام نوع التعداد الذي قمت بإنشائه مسبقًا لتحميل القائمة المحددة التي تريدها - فقط تأكد من أن الترتيب في التعداد والترتيب في قائمة القائمة متماثلان.
أخيرًا ، يمكنك الآن إنشاء مستوى قاعدة بيانات أو قائمة أو ScriptableObject من قائمة الأصول بالنقر بزر الماوس الأيمن في نافذة المشروع.
من هناك ، استمر في إضافة المستويات والقوائم التي تريدها ، وضبط المعلمات ثم إضافتها إلى قاعدة بيانات المشهد. يوضح المثال أدناه كيف تبدو بيانات Level1 و MainMenu و Scenes.
حان الوقت لاستدعاء هذه الأساليب. في هذا المثال ، يقوم زر المستوى التالي في واجهة المستخدم (UI) الذي يظهر عندما يصل المشغل إلى نهاية المستوى باستدعاء طريقة NextLevel. لربط طريقة بأحد الأزرار ، انقر فوق الزر مع حدث عند النقر بالإضافة إلى مكون الزر لإضافة حدث جديد ، ثم اسحب Scene Data ScriptableObject إلى حقل الكائن وحدد طريقة NextLevel من ScenesData كما هو موضح أدناه.
الآن يمكنك القيام بنفس العملية للأزرار الأخرى - إعادة تشغيل المستوى أو الانتقال إلى القائمة الرئيسية وما إلى ذلك. يمكنك أيضًا الرجوع إلى ScriptableObject من أي برنامج نصي آخر للوصول إلى خصائص متنوعة مثل AudioClip لموسيقى الخلفية أو ملف تعريف ما بعد المعالجة واستخدامها على المستوى.
نصائح لتقليل الأخطاء في عملياتك
تقليل التحميل /
التفريغ في نص ScenePartLoader الذي يظهر في الفيديو ، يمكنك أن ترى أن المشغل يمكنه الاستمرار في الدخول والخروج من المصادم عدة مرات ، مما يتسبب في إعادة تحميل المشهد وتفريغه. لتجنب ذلك ، يمكنك إضافة coroutine قبل استدعاء طرق تحميل وتفريغ المشهد في البرنامج النصي وإيقاف coroutine إذا ترك المشغل المشغل.
اصطلاحات التسمية
نصيحة عالمية أخرى هي استخدام اصطلاحات تسمية قوية في مشروعك. يجب أن يتفق الفريق مقدمًا على كيفية تسمية الأنواع المختلفة من الأصول ، من النصوص والمشاهد إلى المواد والأشياء الأخرى في المشروع. سيؤدي ذلك إلى تسهيل العمل في المشروع ودعمه ليس فقط من أجلك ، ولكن أيضًا لزملائك في الفريق. إنها دائمًا فكرة جيدة ، ولكن في هذه الحالة بالذات ، من المهم جدًا إدارة المشاهد باستخدام ScriptableObjects. استخدم مثالنا نهجًا بسيطًا يعتمد على اسم المشهد ، ولكن هناك العديد من الحلول المختلفة التي تعتمد بشكل أقل على اسم المشهد. يجب عليك تجنب النهج القائم على السلسلة لأنه إذا قمت بإعادة تسمية مشهد الوحدة في هذا السياق ، فلن يتم تحميل هذا المشهد في أي مكان آخر من اللعبة.
ادوات خاصة
تتمثل إحدى طرق تجنب الاعتماد على الأسماء في جميع أنحاء اللعبة في تكوين البرنامج النصي للإشارة إلى المشاهد على أنها من النوع Object . يتيح لك هذا سحب مصدر مشهد وإفلاته في المفتش ثم الحصول على اسمه بهدوء في البرنامج النصي. ومع ذلك ، نظرًا لأنها فئة محرر ، فليس لديك حق الوصول إلى فئة AssetDatabase في وقت التشغيل ، لذلك تحتاج إلى دمج كلا الجزأين من البيانات للحصول على حل يعمل في المحرر ، ويمنع الأخطاء البشرية ، ولا يزال يعمل في وقت التشغيل. يمكنك الرجوع إلى واجهة ISerializationCallbackReceiver للحصول على مثال عن كيفية تنفيذ كائن يمكنه ، بعد التسلسل ، استخراج مسار السلسلة من أصل المشهد وتخزينه لاستخدامه في وقت التشغيل.
بدلاً من ذلك ، يمكنك أيضًا إنشاء المفتش الخاص بك لتسهيل إضافة المشاهد بسرعة إلى إعدادات البناء باستخدام الأزرار ، بدلاً من إضافتها يدويًا من خلال هذه القائمة وإبقائها متزامنة.
للحصول على مثال لهذا النوع من الأدوات ، تحقق من هذا التطبيق الرائع مفتوح المصدر من المطور JohannesMP (هذا ليس مورد Unity رسميًا).
اسمحوا لنا أن نعرف ما هو رأيك
يُظهر هذا المنشور طريقة واحدة فقط يمكن من خلالها لـ ScriptableObjects تحسين سير العمل عند العمل مع مشاهد متعددة مع المباني الجاهزة. تستخدم الألعاب المختلفة طرقًا مختلفة تمامًا للتحكم في المشاهد - لا يوجد حل واحد يناسب جميع هياكل اللعبة مرة واحدة. من المنطقي أن تقوم بتنفيذ أدواتك الخاصة لتناسب تنظيم مشروعك.
نأمل أن تساعدك هذه المعلومات في مشروعك ، أو ربما تلهمك لإنشاء أدوات إدارة المشهد الخاصة بك.
أخبرنا في التعليقات إذا كان لديك أي أسئلة. نود أن نسمع التقنيات التي تستخدمها لمعالجة المشاهد في لعبتك. ولا تتردد في اقتراح حالات استخدام أخرى ترغب في اقتراحها للنظر فيها في المشاركات المستقبلية.