إنشاء لعبة إيقاع في الوحدة

صورة

مقدمة


إذاً أنت تريد أو حاولت إنشاء لعبة إيقاع ، ولكن عناصر اللعبة والموسيقى غير متزامنة بسرعة ، والآن أنت لا تعرف ماذا تفعل. هذه المادة سوف تساعدك مع هذا. لقد لعبت ألعاب إيقاعية من المدرسة الثانوية وكثيراً ما علقت على DDR في قاعة الألعاب المحلية. اليوم أنا أبحث دائمًا عن ألعاب جديدة من هذا النوع ، وتظهر مشاريع مثل Crypt of the Necrodancer أو Bit.Trip.Runner أنه يمكن القيام بالكثير في هذا النوع. عملت قليلاً على نماذج أولية من ألعاب الإيقاع في الوحدة ، ونتيجة لذلك أمضيت شهرًا في إنشاء لعبة / أحجية إيقاعية قصيرة. في هذه المقالة ، سأتحدث عن أكثر تقنيات بناء التعليمات البرمجية فائدة التي تعلمتها في إنشاء هذه الألعاب. لم أتمكن من العثور على معلومات عنها في أي مكان آخر ، أو تم تقديمها بتفاصيل أقل.

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

في قلب أي لعبة إيقاع هي توقيت. الناس حساسون للغاية لأي تشويه في توقيت الإيقاع ، لذلك من المهم للغاية أن تتم مزامنة جميع الإجراءات والحركات والمدخلات في لعبة الإيقاع مباشرة مع الموسيقى. لسوء الحظ ، ستفقد طرق تتبع وقت الوحدة التقليدية مثل Time.timeSinceLevelLoad و Time.time التزامن بسرعة مع الصوت الذي يتم تشغيله. لذلك ، سوف نصل إلى النظام الصوتي مباشرة باستخدام AudioSettings.dspTime ، والذي يستخدم العدد الحقيقي لعينات الصوت التي تتم معالجتها بواسطة نظام الصوت. لهذا السبب ، فإنه يحافظ دائمًا على التزامن مع الموسيقى التي يتم تشغيلها (ربما لا يكون هذا هو الحال مع الملفات الصوتية الطويلة جدًا ، عندما يتم تشغيل تأثيرات أخذ العينات ، ولكن في حالة الأغاني ذات الطول العادي ، يجب أن يعمل النظام بشكل مثالي). ستكون هذه الوظيفة جوهر تتبع وقت التكوين الخاص بنا ، وبناءً عليها ، سنقوم بإنشاء الفئة الرئيسية.

موصل الطبقة


فئة Conductor هي فئة إدارة التكوين الرئيسية التي سيتم على أساسها بناء بقية لعبة الإيقاع. مع ذلك ، فإننا سوف تتبع موقف التكوين وإدارة جميع الإجراءات المتزامنة الأخرى. لتتبع التكوين ، نحتاج إلى بعض المتغيرات

//Song beats per minute //This is determined by the song you're trying to sync up to public float songBpm; //The number of seconds for each song beat public float secPerBeat; //Current song position, in seconds public float songPosition; //Current song position, in beats public float songPositionInBeats; //How many seconds have passed since the song started public float dspSongTime; //an AudioSource attached to this GameObject that will play the music. public AudioSource musicSource; 

عند بدء المشهد ، نحتاج إلى إجراء عمليات حسابية لتحديد المتغيرات ، وكذلك تسجيل وقت بدء التكوين كمرجع.

 void Start() { //Load the AudioSource attached to the Conductor GameObject musicSource = GetComponent<AudioSource>(); //Calculate the number of seconds in each beat secPerBeat = 60f / songBpm; //Record the time when the music starts dspSongTime = (float)AudioSettings.dspTime; //Start the music musicSource.Play(); } 

إذا قمت بإنشاء GameObject فارغة مع مثل هذا البرنامج النصي المرفق به ، ثم قمت بإضافة مصدر الصوت مع التكوين وتشغيل البرنامج ، سترى أن البرنامج النصي سوف يسجل وقت بدء التكوين ، ولكن لن يحدث شيء آخر. سنحتاج أيضًا إلى إدخال BPM للموسيقى التي نضيفها إلى مصدر الصوت يدويًا.


بفضل كل هذه القيم ، يمكننا تتبع الموقف في التكوين في الوقت الحقيقي عند تحديث اللعبة. سنحدد توقيت التكوين ، أولاً بالثواني ، ثم بالكسور. الكسور هي طريقة أكثر ملاءمة لتتبع التكوين ، لأنها تسمح لنا بإضافة إجراءات وتوقيتات في الوقت المناسب بالتوازي مع التركيبة ، على سبيل المثال ، في الكسور 1 و 3 و 5.5 ، دون الحاجة إلى حساب الثواني بين الكسور. أضف العمليات الحسابية التالية إلى وظيفة Update () للفئة Conductor:

 void Update() { //determine how many seconds since the song started songPosition = (float)(AudioSettings.dspTime - dspSongTime); //determine how many beats since the song started songPositionInBeats = songPosition / secPerBeat; } 

لذلك نحن نحصل على الفرق بين الوقت الحالي وفقًا لنظام الصوت ووقت بدء التكوين ، والذي يعطي إجمالي عدد الثواني التي يتم تشغيلها في التكوين. سنقوم بحفظه في متغير songPosition.


لاحظ أن النتيجة في الموسيقى عادة ما تبدأ بوحدة ذات الكسور 1-2-3-4 وما إلى ذلك ، وأن songPositionInBeats يبدأ من 0 ويزيد من هذه القيمة ، وبالتالي فإن الجزء الثالث من التكوين سوف يتوافق مع songPositionInBeats ، وهو 2.0 ، وليس 3.0.

في هذه المرحلة ، إذا كنت ترغب في إنشاء لعبة تقليدية على غرار Dance Dance Revolution ، فأنت بحاجة إلى إنشاء ملاحظات وفقًا للكسر الذي تحتاجه للضغط عليها ، وتحريك موقعها بالنسبة لسطر الضغط ، ثم تسجيل songPositionInBeats عند الضغط على المفتاح ، ثم قارن القيمة مع النسبة المطلوبة من الملاحظات. يناقش يو تشاو مثالاً على مثل هذا المخطط في مقاله . لكي لا أكرر نفسي ، سأنظر في التقنيات الأخرى التي يحتمل أن تكون مفيدة والتي يمكن أن تكون مبنية على أعلى فئة الموصل. اعتدت عليهم عند إنشاء يدق الذرية .

نحن نتكيف مع المشاركة الأولية


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


ومع ذلك ، إذا كنت تستخدم موسيقى جاهزة ، فهناك احتمال كبير أن يكون هناك توقف طفيف قبل بداية التكوين. إذا لم يؤخذ ذلك في الحسبان ، فستعتقد أغنية فئة Conductor classPositionInBeats أن الإيقاع الأول بدأ عند بدء تشغيل الأغنية ، وليس الإيقاع الآن. لا يتم مزامنة كل ما سيتم ربطه بقيم المشاركات مع الموسيقى.


لإصلاح ذلك ، يمكنك إضافة متغير يأخذ هذا الإزاحة في الاعتبار. أضف التالي إلى فئة الموصل:

 //The offset to the first beat of the song in seconds public float firstBeatOffset; 

في Update () ، المتغير songPosition:

 songPosition = (float)(AudioSettings.dspTime - dspSongTime); 

يحل محله:

 songPosition = (float)(AudioSettings.dspTime - dspSongTime - firstBeatOffset); 

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



الاعادة


إذا كنت تعمل بتركيبة يتم تشغيلها من البداية إلى النهاية ، فستكون فئة الموصل الموضحة أعلاه كافية لتتبع الموضع. ولكن إذا كان لديك مسار قصير محاط ورغبت في العمل مع هذه الحلقة ، فأنت بحاجة إلى إنشاء دعم Repeater في الموصل.

إذا كان لديك جزء محاط بحلقة كاملة (على سبيل المثال ، إذا كان معدل التكوين هو 120 نقطة في الدقيقة ، وكان للجزء ذي الحلقات طوله 4 نبضات ، فيجب أن يكون 8.0 ثانية تمامًا بمعدل 2.0 ثانية لكل سهم) محملًا في فئة Audio Source للموصل ، ثم حدد مربع الحلقة. سوف يعمل الموصل بالطريقة نفسها كما كان من قبل ، وينقل الوقت الإجمالي إلى songPosition بعد أول بداية للقصاص. لتحديد موضع الحلقة ، نحتاج إلى إخبار الموصل بطريقة أو بأخرى عدد المشاركات الموجودة في حلقة واحدة وعدد الحلقات التي تم تشغيلها بالفعل. أضف المتغيرات التالية إلى فئة الموصل:

 //the number of beats in each loop public float beatsPerLoop; //the total number of loops completed since the looping clip first started public int completedLoops = 0; //The current position of the song within the loop in beats. public float loopPositionInBeats; 

الآن مع كل تحديث لـ SongPositionInBeats ، يمكننا أيضًا تحديث موضع Update () من الحلقة.

 //calculate the loop position if (songPositionInBeats >= (completedLoops + 1) * beatsPerLoop) completedLoops++; loopPositionInBeats = songPositionInBeats - completedLoops * beatsPerLoop; 

هذا يعطينا علامة تخبر loopPositionInBeats عدد المشاركات التي مررنا بها ، وهي مفيدة للعديد من العناصر المتزامنة الأخرى. تذكر إدخال عدد مشاركات الحلقة في GameObject Conductor.

يجب علينا أيضا النظر بعناية في حساب الأسهم. تبدأ الموسيقى دائمًا من 1 ، لذلك يأخذ القياس المكون من 4 أجزاء النموذج 1-2-3-4 ، وفي حلقة الفصل الدراسي تبدأ حلقة loopositionInBeats من 0.0 وتكرار الحلقات 4.0. لذلك ، فإن منتصف الحلقة بالضبط ، والذي عند حساب النسب الموسيقية سيكون 3 ، في loopPositionInBeats سيكون له قيمة 2.0. يمكنك تعديل loopPositionInBeats لأخذ ذلك في الاعتبار ، ولكن هذا سيؤثر على جميع الحسابات الأخرى ، لذا كن حذرًا عند إدراج الملاحظات.

أيضًا بالنسبة للأدوات المتبقية ، سيكون من المفيد إضافة جانبين آخرين إلى فئة الموصل. أولاً ، إصدار تناظري من LoopPositionInBeats يسمى LoopPositionInAnalog ، والذي يقيس الموضع في الحلقة في النطاق من 0 إلى 1.0. والثاني هو مثيل لفئة الموصل للاتصال المريح من الفصول الأخرى. أضف المتغيرات التالية إلى فئة الموصل:

 //The current relative position of the song within the loop measured between 0 and 1. public float loopPositionInAnalog; //Conductor instance public static Conductor instance; 

في وظيفة Awake () ، أضف:

 void Awake() { instance = this; } 

وإضافة إلى وظيفة التحديث ():

 loopPositionInAnalog = loopPositionInBeats / beatsPerLoop; 

تحويل المزامنة


سيكون من المفيد للغاية مزامنة الحركة أو الدوران مع الفصوص بحيث تكون العناصر في الأماكن الصحيحة. في لعبة Atomic Beats ، استخدمت هذا لتدوير الملاحظات ديناميكيًا حول محور مركزي. في البداية ، تم وضعهم حول المحيط وفقًا لحصتهم داخل الحلقة ، ثم تم تدوير منطقة اللعب بأكملها بحيث تم مطابقة الملاحظات مع خط الاكتئاب في حصتها.

لتحقيق ذلك ، قم بإنشاء برنامج نصي جديد يسمى SyncedRotation ، وقم بإرفاقه بـ GameObject الذي تريد تدويره. أضف إلى وظيفة التحديث () للبرنامج النصي SyncedRotation:

 void Update() { this.gameObject.transform.rotation = Quaternion.Euler(0, 0, Mathf.Lerp(0, 360, Conductor.instance.loopPositionInAnalog)); } 

سيقوم هذا الرمز بتحريف دوران لعبة GameObject التي ترتبط بها هذه اللعبة في الفترة الفاصلة من 0 إلى 360 درجة ، وتحولها بحيث تكمل ثورة كاملة واحدة في نهاية كل حلقة. يعد هذا مفيدًا على سبيل المثال ، ولكن بالنسبة للرسوم المتحركة أو الرسوم المتحركة بالإطار ، سيكون من المفيد أكثر مزامنة الرسوم المتحركة للوصلات بحيث تتناسب تمامًا مع الإيقاع.

الرسوم المتحركة المزامنة


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

أولاً ، قم بإنشاء فئة جديدة تسمى SyncedAnimation ، وقم بإضافة المتغيرات التالية إليها:

 //The animator controller attached to this GameObject public Animator animator; //Records the animation state or animation that the Animator is currently in public AnimatorStateInfo animatorStateInfo; //Used to address the current state within the Animator using the Play() function public int currentState; 

قم بتثبيته على GameObject جديد أو موجود تريد تحريكه. في هذا المثال ، سنقوم ببساطة بنقل الكائن للأمام والخلف عبر الشاشة ، ولكن يمكن تطبيق نفس المبدأ على أي رسوم متحركة ، سواء كان ذلك قبل تعيين الخاصية أو الرسوم المتحركة بالإطار. إضافة عنصر Animator إلى GameObject وإنشاء وحدة تحكم Animator جديدة تسمى SyncedAnimController ، بالإضافة إلى مقطع Animation يسمى BackAndForth. نقوم بتحميل وحدة التحكم في فئة Animator المتصلة بـ GameObject ، ونضيف Animation إلى شجرة الرسوم المتحركة كرسوم متحركة افتراضية.


على سبيل المثال ، قمت بإعداد الرسوم المتحركة بحيث تقوم أولاً بنقل الكائن إلى اليمين بـ 6 وحدات ، ثم إلى اليسار بواسطة -6 ، ثم العودة إلى 0.


الآن ، لمزامنة الرسوم المتحركة ، أضف التعليمات البرمجية التالية إلى وظيفة Start () لفئة SyncedAnimation ، التي تقوم بتهيئة المعلومات حول Animator:

 void Start() { //Load the animator attached to this object animator = GetComponent<Animator>(); //Get the info about the current animator state animatorStateInfo = animator.GetCurrentAnimatorStateInfo(0); //Convert the current state name to an integer hash for identification currentState = animatorStateInfo.fullPathHash; } 

ثم أضف الكود التالي إلى Update () لضبط الرسوم المتحركة:

 void Update() { //Start playing the current animation from wherever the current conductor loop is animator.Play(currentState, -1, (Conductor.instance.loopPositionInAnalog)); //Set the speed to 0 so it will only change frames when you next update it animator.speed = 0; } 

لذلك نحن نضع الرسوم المتحركة في الإطار الدقيق للرسوم المتحركة بالنسبة إلى حلقة واحدة كاملة. على سبيل المثال ، إذا كنت تستخدم الرسوم المتحركة أعلاه ، عندما تكون في منتصف الحلقة ، فسوف يتخطى الموضع GameObject 0. ويمكن تطبيق ذلك على أي رسوم متحركة تنشئها وتريد مزامنتها مع الإيقاع Conductor.

تجدر الإشارة أيضًا إلى أنه لإنشاء حلقة سلسة من الرسوم المتحركة ، تحتاج إلى تكوين ظلال الإطارات الأساسية الفردية للرسوم المتحركة على منحنى الرسوم المتحركة. سيؤدي إعداد Linear إلى إنشاء خط مستقيم يمتد من إطار رئيسي إلى آخر ، وسيحتفظ Constant بالرسوم المتحركة في قيمة واحدة حتى الإطار الرئيسي التالي ، مما سيعطي حركة متقلبة وحادة.


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

استنتاج


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

صورة

قد يكون إنشاء لعبة إيقاعية أو عناصر لعبة متزامنة مع الموسيقى أمرًا صعبًا. للحفاظ على كل شيء بوتيرة متسقة ، قد تحتاج إلى رمز صعب ؛ والنتيجة التي تسمح لك باللعب بوتيرة ثابتة قد تكون جذابة للغاية للاعب. يمكن فعل الكثير في هذا النوع أكثر من الألعاب في نمط Dance Dance Revolution التقليدي ، وآمل أن يساعدك هذا المقال في تحقيق مثل هذه المشاريع. أوصي أيضًا ، إن أمكن ، بتقييم لعبة Atomic Beats . لقد نجحت في شهر واحد في ربيع هذا العام ، ولديه 8 مسارات قصيرة وهو مجاني!

Source: https://habr.com/ru/post/ar452168/


All Articles