لماذا القمر يلاحقني, لا شك أنك فكرت بهذا في صغرك عند مسيرك ليلا في إحدى الطرقات ورؤيتك للقمر كانه دائما خلفك مهما تحركت, او حتي عند رؤيتك لإحدى الطائرات في السماء والتي تبدو كأنها تتحرك أبطأ من سرعتك في المشي حتى, وبالتأكيد هذه السرعة ليست كافية لتطير بأي شيكل.
ولكن بتقدمك في العمر أصبحت تدرك أن هذا يرجع لبعد هذه الأجسام عنا بشكل كبير جدا, وبسبب طريقة معالجة العين للأبعاد ترى الأجسام تتحرك بسرعة تتناسب عكسيا مع مدي بعدها عنا,
ولكننا لسنا هنا لنحلل العين البشرية وإنما لنتعرف علي هذا التأثير والذي كما يحدث في الواقع فهو كذلك أيضا في الألعاب وتحديدا ثلاثية الالعاب.
ولكن ماذا عن الألعاب ثنائية الأبعاد, في هذه الحالة قد خسرنا البعد الثالث المعبر عن مدى بعد هذه الأجسام عنا, وينتج عن هذا أن كل العناصر تبعد نفس المسافة عن الكاميرا والتي تمثل عين الإنسان في اللعبة, مما يعني تحركها كلها بنفس المقدار.
وبالنسبه لعينك فهذا يبدو خاطئا وربما لن تشعر بالراحة عند رؤيتك لهذا بداخل الالعاب, ولهذا لابد من وجود حل وهو ما يدعي بالتزيّح – Parallax.
التزيّح هو عبارة عن تقنية تستعمل في مثل حالتنا وهي عدم توافر بيانات لبعد الاجسام عنا, لهذا نقوم بتحريكها بطريقة تتناسب مع بعدها المتخيل تبعا لنوع الجسم نفسه, فسرعة تحرك السماء تختلف عن الجبال وكلاهما يختلفان عن الشجرة التي بجانبك, ولهذا سننفذ هذا التأثير هذه المرة في محرك يونتي.
العمل بواسطة Nicola Guzowska
عناصر المقال
فكرة العمل
لكي يعمل هذا التأثير فهو بحاجة لوجود عدة طبقات, بحيث أن كل طبقة عبارة عن مجموعة من العناصر المختلفة, والتي تشترك مع بعضها في مسافة بعدها عن الكاميرا.
ومن ثم يأتي دور تحريك كل من هذه الطبقات بشكل مستقل, وان فكرت بالطبقة الأقرب للكاميرا, والتي سنفترض أن بعدها صفر, فهذا يعني أنها ستتحرك بنفس سرعة الكاميرا وكأنها مرتبطة بها, وإن اردنا التعبير عن موقعها رياضيا, فهي دائما تكون في الموقع الذي يساوي موقعها الابتدائي اضافة الى موقع الكاميرا.
ولكن هذا بالنسبه للطبقه الاقرب, اما بالنسبه للطبقه الابعد والتي نفترض ان بعدها لا نهائي فهذا يعني انها لن تتحرك علي الاطلاق, ولهذا تظل في موقعها الابتدائي.
وبهاتين الفرضيتين يمكننا ملاحظة ان الموقع الابتدائي دائما موجود, ومن ثم تجمع عليه قيمة معينة مرتبطة بموقع الكاميرا, فإن كان بعدها صفرا فهذا يدل ان موقع الكاميرا يضاف كاملا, اما ان كان البعد لا نهائيا فلن يضاف موقع الكاميرا مطلقا, او بمعني اصح يضاف لكن مضروبا بالصفر.
مما يعني أن الطبقات التي بين الكاميرا والمالانهاية تتحرك بقيمة تساوي موقعها الابتدائي مضافا إليه قيمة موقع الكاميرا مضروبا في قيمة معينة بين الصفر والواحد لتتناسب مع بعد هذه الطبقة, بحيث يمثل الواحد حركة كاملة, والصفر انعدام الحركة.
تجهيز المشروع
في هذا المشروع استعنت باحدي الخلفيات والتي تم تصميمها بواسطة vnitti ويمكنك ان تجدها هنا,ومن ثم وضعت الطبقات بالترتيب المناسب باستعمال طبقات الفرز – Sorting Layer.
ولا تنس التأكد من أن وضع الاسقاط – Projection للكاميرا Orthographic, ومن ثم اضفت لها شيفرة – Script لتحريكها بلوحة المفاتيح.
المسافة بين الطبقات هنا لتوضيح الترتيب
إعداد البنية
ستبدأ اولا بانشاء الشيفرة ومن ثم تسميتها بما يناسبك, ولكن قبل العمل علي الشيفرة نفسها تحتاج لاضافة بنية – Struct اولا, والتي يمكنك اعتبارها تماما مثل الصنف – Class ولكن بدون دوال – Function, ووظيفة هذه البنية أنها تمثل نموذجا يحوي مجموعة من المتغيرات, والتي نحتاجها لكل طبقة بشكل منفرد.
وبصورة أساسية, فان البيانات التي نحتاجها هي الطبقة التي تحركها لاحقا, والموقع الابتدائي لهذه الطبقة والذي تستطيع ملاحظة ان نوعه هنا عائم – Float وليس متجه ثنائي – Vector 2, وسبب هذا أن حركة هذه الطبقات افقيه, لذا لن نحتاج سوى قيمة واحدة لتعبر عن الـ X الابتدائية, وبما ان لا حاجة لرؤيتها في المحرر فقد اضفت لها صفة الاخفاء في لوحة الخصائص – HideInInspector.
واخر شيء وهو قيمة لتعبر عن مدى تأثر الطبقة بحركة الكاميرا, وبما أنها لن تخرج عن الصفر والواحد فقد اضفت لها صفة المدى Range.
ويمكنك التعرف أكثر على نظام الصفات في يونتي من هذه المقالة.
using UnityEngine;
public class Parallax : MonoBehaviour
{
}
public struct ParallaxStruct
{
public Transform Layer;
[HideInInspector]public float StartPosition;
[Range(0,1)]public float ParallaxFactor;
}
والان بالعودة للصنف الخاص بالتأثير, تبدأ باضافة المتغيرات التي تحتاجها, وهما اثنان, احداهما للكاميرا من نوع الكاميرا – Camera, والاخر من نوع البنية التي انشأتها توا مع تحديده ليكون مصفوفة لتصبح قادرا علي إضافة البيانات الخاصة بكل طبقة.
لكن كونه بنية فلن يمكنك رؤيته في لوحة الخصائص – Inspector, ولحل هذه المشكلة ما عليك سوى اضافة صفة [System.Serializable] قبل البنية.
using UnityEngine;
public class Parallax : MonoBehaviour
{
public Camera Cam;
public ParallaxStruct[] ParallaxObjects;
}
[System.Serializable]
public struct ParallaxStruct
{
public Transform Layer;
[HideInInspector]public float StartPosition;
[Range(0,1)]public float ParallaxFactor;
}
اضافة الحركة
والان حان وقت اضافة الحركه للطبقات, وكما رأيت في فكرة العمل فأول ما تحتاج لتنفيذه هو الحصول علي الموقع الابتدائي لكل طبقة.
وبما انك الان نتعامل مع مصفوفة فعليك استعمال حلقة تكرار لها – For Loop, وفي كل دورة منها تعين قيمة الموقع الابتدائي ليساوي قيمة مركبة X من موقع الطبقة نفسها, وبما أننا نحتاج تنفيذ هذه المهمة مرة واحدة فما عليك سوى إضافتها في دالة البدء – Start.
public class Parallax : MonoBehaviour
{
public Camera Cam;
public ParallaxStruct[] ParallaxObjects;
void Start()
{
for (int i = 0; i < ParallaxObjects.Length; i++)
{
ParallaxObjects[i].StartPosition = ParallaxObjects[i].Layer.transform.position.x;
}
}
}
ومن ثم في دالة التحديث – Update تضيف الجزء الخاص بتعيين موقع الطبقه وهو ما تحتاج فيه حلقة تكرار اخرى لنفس السبب السابق, وبداخل هذه الحلقة تنشيء متغيرا من النوع العائم ليعبر عن القيمة الأفقية الجديدة للطبقة, والتي تساوي المعادلة التي ذكرناها في فكرة العمل, وهي القيمة الابتدائية + موقع الكاميرا * معامل التزيّح.
والان تبقى أن تعين موقع الطبقة ليساوي متجها ثنائيا, بحيث المركبة الافقية منه تساوي قيمة المتغير العائم الذي انشأته توا, أما الرأسية فتساوي القيمة الرأسية لنفس الطبقة بما اننا لا نريد تحريكها.
وبانتهائك من هذا يمكنك العودة للمحرك ووضع كل من الطبقات في المصفوفة وتعيين قيمة معامل التزيّح المناسبة لكل منها ورؤية النتيجة.
public class Parallax : MonoBehaviour
{
public Camera Cam;
public ParallaxStruct[] ParallaxObjects;
void Start()
{
for (int i = 0; i < ParallaxObjects.Length; i++)
{
ParallaxObjects[i].StartPosition = ParallaxObjects[i].Layer.transform.position.x;
}
}
void Update()
{
for (int i = 0; i < ParallaxObjects.Length; i++)
{
float PositionX = ParallaxObjects[i].StartPosition + Cam.transform.position.x * ParallaxObjects[i].ParallaxFactor;
ParallaxObjects[i].Layer.transform.position = new Vector2(PositionX,ParallaxObjects[i].Layer.transform.position.y);
}
}
}
ولكن كون هنالك طبقات تتحرك بسرعة أقل من الكاميرا, فهذا يعني أنها في النهاية تخرج تماما خارج نطاق الرؤية في لحظة ما, ولهذا فهذه مشكله لابد من حلها.
التكرار اللانهائي
هناك عدة طرق لتنفيذ تأثير التكرار اللانهائي, وبالنسبه للتي سنستعملها هنا ففكرتها ببساطة أنه عند خروج الطبقة تماما من نطاق رؤية الكاميرا فسيعاد تعيين موقعها لتصبح بنفس موقع الكاميرا, او بمعني اخر تضاف قيمة معينة للمتغير الخاص بالموقع الابتدائي للكاميرا, وهذه القيمة تعبر عن عرض الطبقه نفسها, ولهذا ستحتاج اضافة متغير جديد في البنية ليمثل عرض الطبقه.
ومن ثم تضيف متغيرا جديدا من النوع العائم بداخل حلقة التكرار المتواجدة بدالة التحديث, وهذا المتغير يعبر عن مدى إزاحة الطبقة, وهو ما تحتاجه بعد قليل.
وبالنسبة لقيمته فهي تساوي قيمة المركبة الأفقية لموقع الطبقة, ولكن هذا يعني انه يعمل فقط مع الطبقه الابعد او التي معامل الإزاحة لها صفر, لهذا نحتاج ضربها في جزء آخر للمعادلة بحيث يتناسب مع قيمة معامل التزيّح للطبقه.
وان فكرت بضرب مركبة الموقع الأفقي في معامل التزيّح فهذا صحيح تقريبا, عدا ان هذا يعني أن هذا لن يعمل بطريقة مناسبة للطبقه الابعد, كونك تضرب موقع الطبقه بالصفر, وما نريده عكس ذلك وهو موقع الطبقة كاملا.
ولهذا يمكنك تعديل الطرف الخاص بمعامل التزيّح في المعادلة, فبدلا من ضرب الموقع الأفقي مباشره به, تضربه في واحد ناقص معامل التزيّح, وهو ما يساوي 1 في حالة الطبقة الأبعد وهو ما نريده.
والان تضيف جملة شرطية – If Statement والتي بداخلها تتحقق ما إن كانت الطبقة قد خرجت تماما خارج نطاق الكاميرا أم لا بمقارنة قيمة متغير قيمة الازاحة مع حاصل جمع طول الطبقة وموقعها الابتدائي.
فان كانت قيمة متغير الازاحة اكبر منها فهذا يعني أنها خرجت تماما خارج نطاق الكاميرا, وهو ما يعني تحقق الشرط, ولهذا لتحريك الطبقة لتصبح في نطاق الكاميرا مجددا تضيف قيمة عرض الطبقه الي موقعها الابتدائي, وهذا لأن قيمة الموقع الابتدائي جزء من المعادلة التي أنشأتها في البداية الخاصة بتعيين موقع الطبقة.
ولكن هذا يعمل فقط في حالة تحركت الكاميرا خارج نطاق الخلفية في الاتجاه الأيمن, ولتنفيذ نفس المنطق في الاتجاه الأيسر ما عليك سوى اضافة الجملة الشرطية الأخرى مع عكس الشرط ليصبح أصغر من, وتغيير العملية بداخلها للطرح بدلا من الجمع.
public class Parallax : MonoBehaviour
{
void Update()
{
for (int i = 0; i < ParallaxObjects.Length; i++)
{
float OffsetThreshold = Cam.transform.position.x * (1- ParallaxObjects[i].ParallaxFactor);
if(OffsetThreshold > ParallaxObjects[i].StartPosition + ParallaxObjects[i].LayerWidth)
{
ParallaxObjects[i].StartPosition += ParallaxObjects[i].LayerWidth;
}
else if(OffsetThreshold < ParallaxObjects[i].StartPosition - ParallaxObjects[i].LayerWidth)
{
ParallaxObjects[i].StartPosition -= ParallaxObjects[i].LayerWidth;
}
}
}
}
[System.Serializable]
public struct ParallaxStruct
{
public float LayerWidth;
}
والان تبقى أن تعود للمحرك وتجربها من جديد, ولكن قبل هذا لا تنس تعيين قيمه عرض الطبقه, ويمكنك حسابها بعمليه بسيطه, وهي قسمة عرض الصورة نفسها على قيمة البكسلات لكل وحده – Pixels Per Unit, اي انها هنا تساوي قسمة 384 علي 100 مما يساوي 3.84.
والان تتبقي خطوه اخيره, وهي نسخ جميع الطبقات مرتين واضافة كل نسخة كابن للطبقة الرئيسيه, ومن ثم إزاحة المجموعتين مرة للجانب الأيمن, ومره للايسر.
وبهذا تكون قد انتهيت من اضافة تقنية التزيّح للخلفيه, والتي تعتبر من التقنيات الأساسية في تطوير الألعاب, حيث تضفي طابع الحركة الديناميكية للخلفيات, وتعطيها حيوية وواقعية.
وعلى الرغم من أنه قد يبدو معقدا في البداية, إلا أن تطبيقه في المحركات الحديثة أصبح أسهل وأكثر فعالية,مما يمكنك من التركيز على الجوانب الأخرى من تطوير اللعبة بدرجة أكبر.
وباستخدام هذه التقنية يصبح بمقدورك تحويل المشاهد ثنائية الأبعاد البسيطة إلى عوالم ثلاثية الابعاد تتنفس وتتحرك, فكل طبقة جديدة تضيف بعدا جديدا وتعمق من تجربة اللعبة, وهو ما يشكل عاملا مهما في الألعاب التي تعتمد بشكل كبير على التصميم البصري والقصة، مثل ألعاب المغامرات والألعاب التي تحكي قصصا بصرية.