كيفية بناء نظام ترجيع الوقت مثل Prince-Of-Persia، الجزء 1
Arabic (العربية/عربي) translation by Ayman Amar (you can also view the original English article)



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



سوف تحتاج إلى الإصدار الأحدث من Unity من أجل هذا الشرح، وينبغي أن يكون لديك بعض الخبرة معه. الكود المصدري أيضا متوفر للتحميل إذا كنت تريد أن تتحقق من تقدمك ضده.
مستعد؟ هيا بنا!
كيف تم استخدام هذا قبل؟
Prince of Persia: The Sands of Time واحدة من أولى الألعاب التي حقاً دمجت ميكانيكية ترجيع الوقت في اللعب. عندما تموت ليس عليك فقط إعادة اللعبة، ولكن يمكنك بدلاً من ذلك إرجاع اللعبة لبضع ثوان حيث كنت على قيد الحياة مرة أخرى، وفورا حاول مرة أخرى.



هذه الميكانيكية لم تدمج فقط في اللعب، ولكن في السرد والكون أيضا، ومذكورة في جميع أنحاء القصة.
الألعاب الأخرى التي تستخدم هذه الأنظمة هي Braid، على سبيل المثال، التي أيضا تتمحور حول لف الوقت. البطلة Tracer في Overwatch تتمتع بقوة تعيدها إلى موقعها قبل بضعت ثواني، باختصار ترجيع وقتها، حتى في لعبة متعددة اللاعبين. أيضا سلسلة GRID لألعاب السيارات لديها مكانيكية اللقطة، حيث لديك مجموعة صغيرة من الترجيعات خلال السباق، التي يمكن الوصول إليها عند حدوث اصطدام حرج. هذا يمنع الفشل الناجم عن التعطلات قرب نهاية السباق، التي يمكن أن يسبب .



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



Time-Trial Ghosts
Race-Ghosting هي تقنية حيث تسابق لأفضل وقت في مسار فارغ. ولكن في الوقت نفسه، تسابق ضد شبح، التي هي سيارة شبحية، وشفافة، التي تقود بالضبط على الطريقة التي تسابقت بها من قبل في محاولتك الأفضل. لا يمكنك أن تصطدم معها، مما يعني يمكنك التركيز للحصول على أفضل وقت.
بدلاً من القيادة وحيدا تحصل على منافسة ضد نفسك، الأمر الذي يجعل time-trials أكثر متعة. تظهر هذه الميزة في معظم العاب السباقات، من سلسلة Need for Speed إلى Diddy Kong Racing.



Multiplayer-Ghosts
Asynchronous Multiplayer-Ghosting هي طريقة أخرى لاستخدام هذا الإعداد. في هذه الميزة التي نادراً ما تستخدم، المباريات المتعددة اللاعبين تنجز عن طريق تسجيل بيانات لاعب واحد، ثم يرسل ركضه إلى لاعب آخر، الذي يمكن بعد ذلك العراك ضد أول لاعب. يتم تطبيق البيانات بنفس طريقة time-trial-ghost التي ستكون، إلا أنه يمكنك السباق ضد لاعب آخر.
شكلاً من أشكال هذا يظهر في العاب Trackmania، حيث أنه من الممكن السباق ضد بعض الصعوبات. هؤلاء المتسابقين المسجلين سوف يعطونك خصم لهزيمته مقابل جائزة معينة.
تحرير الفيلم
توفر بعض الألعاب هذا من get-go لكن اتخدامها بحق يمكن أن يكون أداة ممتعة. تقدم Team Fortress 2 محرر إعادة مدمج، الذي يمكنك من إنشاء مقاطعك الخاصة.



بمجرد تنشيط الميزة يمكنك تسجيل ومشاهدة المباريات السابقة. العنصر الحيوي هو أن يتم تسجيل كل شيء، ليس فقط منظورك. هذا يعني يمكنك التحرك حول عالم اللعبة المسجل، تنظر أين يوجد الجميع، ولديك السيطرة على الوقت.
كيفية بنائه
لاختبار هذا النظام، نحن بحاجة إلى لعبة بسيطة حيث يمكننا الاختبار عليها. لنقم بإنشاء واحدة!
اللاعب
أنشئ مكعب في المشهد الخاص بك، وسيكون هذا شخصيتنا اللاعبة. بعد ذلك أنشئ نص # C جديد وقم بتسميته Player.cs
واضبط دالة Update()
لتظهر على هذا الشكل:
1 |
void Update() |
2 |
{
|
3 |
transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical")); |
4 |
transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal")); |
5 |
}
|
هذا سوف يتكفل بحركة بسيطة عن طريق مفاتيح الأسهم. أرفق هذا النص البرمجي إلى المكعب اللاعب. عندما تضغط الآن لعب يجب أن تكون قادرا على التحرك فعلا.
ثم غير زاوية الكاميرا حتى تنظر إلى المكعب من أعلاه، مع غرفة بجانبه حيث يمكننا تحريكه. وأخيراً، أنشئ سطح منبسط بمثابة أرضية وأدرج بعض المواد المختلفة لكل كائن، بحيث أننا لا نحركه داخل فراغ. ينبغي أن تبدو مثل هذه:



جربها، ويجب أن تكون قادرا على تحريك المكعب الخاص بك باستخدام WSAD ومفاتيح الأسهم.
متحكم الوقت
الآن أنشئ نص C# جديد وقم بتسميته TimeController.cs
وأضفه إلى GameObject فارغ جديد. هذا سيقوم بالتعامل مع التسجيل الفعلي والإرجاع اللاحق للالعبة.
من أجل جعل هذا يعمل، سوف نقوم بتسجيل حركة الشخصية اللاعبة. عندما نضخط بعد ذلك على زر الترجيع سوف نضبط إحداثيات اللاعب. للقيام بذلك ابدأ بإنشاء متغير للاحتفاظ باللاعب، مثل هذا:
1 |
public GameObject player; |
وأدرج كائن اللاعب للفتحة الناتجة على TimeController، بحيث أنه يمكن الوصول إلى اللاعب والبيانات الخاصة به.



ثم نحن بحاجة إلى إنشاء مصفوفة للاحتفاظ ببيانات اللاعب:
1 |
public ArrayList playerPositions; |
2 |
|
3 |
void Start() |
4 |
{
|
5 |
playerPositions = new ArrayList(); |
6 |
}
|
الذي سوف نفعله تاليا هو تسجيل موقع اللاعب باستمرار. سيكون لدينا الموقع المخزن حيث كان اللاعب في الإطار الأخير، الموقع المخزن حيث كان اللاعب قبل 6 إطارات، والموقع المخزن حيث كان اللاعب قبل 8 ثواني (أو أي طول قمت بتعينه للتسجيل). عندما نضغط في وقت لاحق على زر سنقوم بالرجوع إلى الخلف من خلال مصفوفة المواقع الخاصة بنا وإدراجها إطارا بإطار، مما يؤدي إلى ميزة إرجاع الوقت.
أولاً، دعونا نحفظ البيانات:
1 |
void FixedUpdate() |
2 |
{
|
3 |
playerPositions.Add (player.transform.position); |
4 |
}
|
في دالة FixedUpdate()
نقوم بتسجيل البيانات. FixedUpdate()
تستخدم لتعمل بشكل ثابت على 50 دورة في الثانية (أو أي شيء قمت بتعيينه عليه)، الذي يسمح لفصل زمني ثابت بتسجيل وتعيين البيانات. دالة Update()
تعمل في الوقت نفسه اعتمادا على كم من الإطارات يقوم المعالج بإدارتها، مما سيجعل الأمور أكثر صعوبة.
الكود سوف يخزن موقع اللاعب لكل إطار في المصفوفة. الآن نحن بحاجة لتطبيقه!
سنقوم بإضافة تحقق لمعرفة إذا تم الضغط على زر الترجيع. من أجل هذا، نحن بحاجة إلى متغير boolean:
1 |
public bool isReversing = false; |
ونحقق في دالة Update()
لتعيينه وفقا لما إذا كنا نريد ترجيع اللعب:
1 |
void Update() |
2 |
{
|
3 |
if(Input.GetKey(KeyCode.Space)) |
4 |
{
|
5 |
isReversing = true; |
6 |
}
|
7 |
else
|
8 |
{
|
9 |
isReversing = false; |
10 |
}
|
11 |
}
|
لتشغيل اللعبة إلى الخلف، سوف نطبق البيانات بدلاً من التسجيل. يجب أن تبدو التعليمات البرمجية الجديدة لتسجيل وتطبيق موقع اللاعب كما يلي:
1 |
void FixedUpdate() |
2 |
{
|
3 |
if(!isReversing) |
4 |
{
|
5 |
playerPositions.Add (player.transform.position); |
6 |
}
|
7 |
else
|
8 |
{
|
9 |
player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; |
10 |
playerPositions.RemoveAt(playerPositions.Count - 1); |
11 |
}
|
12 |
}
|
والنص البرمجي TimeController
بالكامل هكذا:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
|
4 |
public class TimeController: MonoBehaviour |
5 |
{
|
6 |
public GameObject player; |
7 |
public ArrayList playerPositions; |
8 |
public bool isReversing = false; |
9 |
|
10 |
void Start() |
11 |
{
|
12 |
playerPositions = new ArrayList(); |
13 |
}
|
14 |
|
15 |
void Update() |
16 |
{
|
17 |
if(Input.GetKey(KeyCode.Space)) |
18 |
{
|
19 |
isReversing = true; |
20 |
}
|
21 |
else
|
22 |
{
|
23 |
isReversing = false; |
24 |
}
|
25 |
}
|
26 |
|
27 |
void FixedUpdate() |
28 |
{
|
29 |
if(!isReversing) |
30 |
{
|
31 |
playerPositions.Add (player.transform.position); |
32 |
}
|
33 |
else
|
34 |
{
|
35 |
player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; |
36 |
playerPositions.RemoveAt(playerPositions.Count - 1); |
37 |
}
|
38 |
}
|
39 |
}
|
أيضا، لا تنسى إضافة التحقق إلى كلاس player
لمعرفة ما إذا كان TimeController
حاليا يقوم بالترجيع أم لا، والتحرك فقط عندما لا يكون قيد العكس. وبخلاف ذلك، قد تنشئ سلوك غير سار:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
|
4 |
public class Player: MonoBehaviour |
5 |
{
|
6 |
private TimeController timeController; |
7 |
|
8 |
void Start() |
9 |
{
|
10 |
timeController = FindObjectOfType(typeof(TimeController)) as TimeController; |
11 |
}
|
12 |
|
13 |
void Update() |
14 |
{
|
15 |
if(!timeController.isReversing) |
16 |
{
|
17 |
transform.Translate (Vector3.forward * 3.0f * Time.deltaTime * Input.GetAxis ("Vertical")); |
18 |
transform.Rotate (Vector3.up * 200.0f * Time.deltaTime * Input.GetAxis ("Horizontal")); |
19 |
}
|
20 |
}
|
21 |
}
|
هذه السطور الجديدة سوف تعلثر على كائن TimeController
تلقائيا في المشهد عند بدء التشغيل والتحقق من ذلك أثناء وقت التشغيل لنرى إذا نحن حاليا نلعب اللعبة أو نقوم بترجيعها. يمكننا فقط التحكم بالشخصية عندما لا نكون حاليا نعكس الوقت.
الآن يجب أن تكون قادرا على التحرك في أنحاء العالم، وترجيع الحركة الخاصة بك عن طريق الضغط على مسافة. إذا قمت بتحميل حزمة البناء المرفقة مع هذه المقالة، وافتح TimeRewindingFunctionality01 يمكنك تجربتها!
ولكن انتظر، لماذا لاعبنا المكعب البسيط يبحث عن الإتجاه الأخير الذي تركناه فيه؟ لأننا لم نقم بفعل ذلك لتسجيل دورانه أيضا!
لذلك تحتاج مصفوفة أخرى للحفاظ على قيم الدوران، لإنشاء مثيل في البداية، وحفظ وتطبيق البيانات بنفس الطريقة التي يمكننا التعامل مع موقع البيانات.
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
|
4 |
public class TimeController: MonoBehaviour |
5 |
{
|
6 |
public GameObject player; |
7 |
public ArrayList playerPositions; |
8 |
public ArrayList playerRotations; |
9 |
public bool isReversing = false; |
10 |
|
11 |
void Start() |
12 |
{
|
13 |
playerPositions = new ArrayList(); |
14 |
playerRotations = new ArrayList(); |
15 |
}
|
16 |
|
17 |
void Update() |
18 |
{
|
19 |
if(Input.GetKey(KeyCode.Space)) |
20 |
{
|
21 |
isReversing = true; |
22 |
}
|
23 |
else
|
24 |
{
|
25 |
isReversing = false; |
26 |
}
|
27 |
}
|
28 |
|
29 |
void FixedUpdate() |
30 |
{
|
31 |
if(!isReversing) |
32 |
{
|
33 |
playerPositions.Add (player.transform.position); |
34 |
playerRotations.Add (player.transform.localEulerAngles); |
35 |
}
|
36 |
else
|
37 |
{
|
38 |
player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; |
39 |
playerPositions.RemoveAt(playerPositions.Count - 1); |
40 |
|
41 |
player.transform.localEulerAngles = (Vector3) playerRotations[playerRotations.Count - 1]; |
42 |
playerRotations.RemoveAt(playerRotations.Count - 1); |
43 |
}
|
44 |
}
|
45 |
}
|
قم بتجربتها! TimeRewindingFunctionality02 هو النسخة المحسنة. الآن لدينا اللاعب المكعب يمكن الانتقال بالخلف في الوقت المناسب، وسوف ننظر بنفس الطريقة التي فعلت عندما كان في تلك اللحظة.
خاتمة
قمنا ببناء لعبة نموذجية بسيطة مع نظام ترجيع الوقت الصالح للاستعمال، ولكن بعيداً عن الإكتمال حتى الآن. في الجزء التالي من هذه السلسلة سوف نجعله أكثر استقرارا وتنوعاً، وإضافة بعض التأثيرات المتقنة.
هنا ما علينا القيام به:
- فقط تسجيل كل ~ 12 الإطار والاقتحام بين تلك المسجلة للحفاظ على تحميل البيانات الضخمة
- فقط تسجيل المواقع ~ 75 الأخيرة للاعب والدوران للتأكد من أن لا تصبح المصفوفة غير عملية جداً واللعبة لا يحدث لها إنهيار
كما أننا سوف نلقي نظرة على كيفية توسيع هذا النظام السابق فقط شخصية اللاعب:
- سجل أكثر من مجرد اللاعب
- إضافة تأثير للدلالة على أن الترجيع يحدث (مثل تمويه VHS)
- استخدام كلاس مخصص لحفظ موقع ودوران اللاعب بدلاً من المصفوفات