كيفية بناء نظام وقت الترجيعترجيع الوقت مثل Prince-of-Persia، الجزء 2

() translation by (you can also view the original English article)



آخر مرة قمنا بإنشاء لعبة بسيطة حيث يمكننا ترجيع الوقت لنقطة سابقة. الآن سوف نقوم بترسيخ هذه الميزة وجعلها أكثر متعة للاستخدام.
كل شيء شيء نقوم به هنا مبني على الجزء السابق، إذا إذهب للتحقق منه! كالمرة السابقة، ستحتاج إلى Unity وفهم بسيط له.
مستعد؟ لنذهب!
تسجيل بيانات أقل واقحام
والآن نقوم بتسجيل مواقع ودوران اللاعب 50 مرة في الثانية. سوف تصبح هذه الكمية من البيانات بسرعة لا يمكن الدفاع عنها، وهذا سوف يصبح ملاحظا خاصة مع إعدادات اللعبة الأكثر تعقيدا والأجهزة النقالة مع أقل قوة معالجة.
لكن ما يمكننا القيام به بدل من ذلك هو التسجيل فقط 4 مرات في الثانية والاقحام بين هؤلاء الإطارات الرئيسة. بهذه الطريقة نحفظ 92% حجم المعالجة، والحصول على النتائج لايمكن تمييزها من تسجيلات 50-إطار، كما أنها تلعب في غضون أجزاء من الثانية.
سوف نبدأ بتسجيل فقط إطار مفتاحي كل x من الإطارات. للقيام بذلك، نحن بحاجة أولاً إلى هذه المتغيرات الجديدة:
1 |
public int keyframe = 5; |
2 |
private int frameCounter = 0; |
المتغير keyframe
هو الإطار في method FixedUpdate
التي سوف يقوم بتسجيل بيانات اللاعب. حاليا، تم تعيينها لـ 5، مما يعني أنه كل مرة خامسة method FixedUpdate
تدور خلالها، البيانات سوف تسجل كما FixedUpdate
تعمل 50 مرة في الثانية، هذا يعني سوف تسجل 10 إطارات في الثانية، مقارنة بـ 50 من قبل. سيتم استخدام المتغير frameCounter
لعد الإطارات حتى الإطار المفتاحي التالي.
الآن قم بتكييف مجموعة التسجيل في دالة FixedUpdate
لتبدو مثل هذا:
1 |
if(!isReversing) |
2 |
{
|
3 |
if(frameCounter < keyframe) |
4 |
{
|
5 |
frameCounter += 1; |
6 |
}
|
7 |
else
|
8 |
{
|
9 |
frameCounter = 0; |
10 |
playerPositions.Add (player.transform.position); |
11 |
playerRotations.Add (player.transform.localEulerAngles); |
12 |
}
|
13 |
}
|
إذا قمت بتجريبه الآن، سترى أن الترجيع يأخذ جزء في وقت أقصر بكثير من قبل. هذا بسبب أننا سجلنا بيانات أقل، لكن تشغيلها بالسرعة العادية. الآن نحن بحاجة إلى تغيير ذلك.
أولاً، نحن بحاجة إلى متغير frameCounter
اخر للاحتفاظ بتعقب ليس لتسجيل البيانات، لكن لبدئه مرة أخرى.
1 |
private int reverseCounter = 0; |
قم بضبط الكود الذي قوم باستعادة موقع اللاعب للاستفادة منه بنفس الطريقة التي نسجل بها البيانات. دالة FixedUpdate
ينبقي بعد ذلك أن تظهر هكذا:
1 |
void FixedUpdate() |
2 |
{
|
3 |
if(!isReversing) |
4 |
{
|
5 |
if(frameCounter < keyframe) |
6 |
{
|
7 |
frameCounter += 1; |
8 |
}
|
9 |
else
|
10 |
{
|
11 |
frameCounter = 0; |
12 |
playerPositions.Add (player.transform.position); |
13 |
playerRotations.Add (player.transform.localEulerAngles); |
14 |
}
|
15 |
}
|
16 |
else
|
17 |
{
|
18 |
if(reverseCounter > 0) |
19 |
{
|
20 |
reverseCounter -= 1; |
21 |
}
|
22 |
else
|
23 |
{
|
24 |
player.transform.position = (Vector3) playerPositions[playerPositions.Count - 1]; |
25 |
playerPositions.RemoveAt(playerPositions.Count - 1); |
26 |
|
27 |
player.transform.localEulerAngles = (Vector3) playerRotations[playerRotations.Count - 1]; |
28 |
playerRotations.RemoveAt(playerRotations.Count - 1); |
29 |
|
30 |
reverseCounter = keyframe; |
31 |
}
|
32 |
}
|
33 |
}
|
عندما تقوم بترجيع الوقت الان، اللاعب سوق يقفز بالخلف إلى أماكنه السابقة، في نفس الوقت!
هذا ليس تماما ما نريده، أعتقد. نحتاج إلى الإقتحام بين هذه الإطارات المفتاحية، التي ستكون أكثر صعوبة قليلا. أولا، نحتاج إلى هذه المتغيرات الأربعة:
1 |
private Vector3 currentPosition; |
2 |
private Vector3 previousPosition; |
3 |
private Vector3 currentRotation; |
4 |
private Vector3 previousRotation; |
هؤلاء سوف يحفظون بيانات اللاعب وواحد من الإطار المفتاحي مسجل قبل ذلك حيث يمكننا أن الاقحام بين هاتين.
بعد ذلك نحن بحاجة إلى هذه الدالة:
1 |
void RestorePositions() |
2 |
{
|
3 |
int lastIndex = keyframes.Count - 1; |
4 |
int secondToLastIndex = keyframes.Count - 2; |
5 |
|
6 |
if(secondToLastIndex >= 0) |
7 |
{
|
8 |
currentPosition = (Vector3) playerPositions[lastIndex]; |
9 |
previousPosition = (Vector3) playerPositions[secondToLastIndex]; |
10 |
playerPositions.RemoveAt(lastIndex); |
11 |
|
12 |
currentRotation = (Vector3) playerRotations[lastIndex]; |
13 |
previousRotation = (Vector3) playerRotations[secondToLastIndex]; |
14 |
playerRotations.RemoveAt(lastIndex); |
15 |
}
|
16 |
}
|
هذا سوف يدرج المعلومات المقابلة إلى متغيرات الموقع والدوران التي سوف نقحم بينها. نحتاج هذا في دالة منفصلة، كما نستدعيها في منطقتين مختلفتين.
جزء استعادة البيانات الخاص بنا ينبغي أن يبدو مثل هذا:
1 |
if(reverseCounter > 0) |
2 |
{
|
3 |
reverseCounter -= 1; |
4 |
}
|
5 |
else
|
6 |
{
|
7 |
reverseCounter = keyframe; |
8 |
RestorePositions(); |
9 |
}
|
10 |
|
11 |
if(firstRun) |
12 |
{
|
13 |
firstRun = false; |
14 |
RestorePositions(); |
15 |
}
|
16 |
|
17 |
float interpolation = (float) reverseCounter / (float) keyframe; |
18 |
player.transform.position = Vector3.Lerp(previousPosition, currentPosition, interpolation); |
19 |
player.transform.localEulerAngles = Vector3.Lerp(previousRotation, currentRotation, interpolation); |
نقوم باستدعاء الدالة للحصول على مجموعات المعلومات الأخيرة وقبل الأخيرة من المصفوفات الخاصة بنا كلما يصل العداد إلى فترة الإطار المفتاحي الذي وضعناه (في حالتنا 5)، لكن نحن بحاجة أيضا الى استدعائها في الدورة الأولى عندما تحدث الاستعادة. ولهذا السبب لدينا هذا الجزء.
1 |
if(firstRun) |
2 |
{
|
3 |
firstRun = false; |
4 |
RestorePositions(); |
5 |
}
|
من أجل جعل هذا يعمل، تحتاج أيضا إلى المتغير firstRun
:
1 |
private bool firstRun = true; |
ولإعادة التعيين عندما يتم رفع الضغط عن زر space:
1 |
if(Input.GetKey(KeyCode.Space)) |
2 |
{
|
3 |
isReversing = true; |
4 |
}
|
5 |
else
|
6 |
{
|
7 |
isReversing = false; |
8 |
firstRun = true; |
9 |
}
|
هنا كيف يعمل الإقحام:
بدلا من فقط من استعمال الإطار المفتاحي الأخير الذي قمنا بحفظه، هذا النظام يحصل على الأخير وما قبل الأخير ويقحم بينهم. مقدار الإقحام راجع إلى أي مدى بين الإطارات المتوفرة حاليا.
كل هذا يحصل من خلال دالة الـ Lerp، حيث أضفنا الموقع الحالي (أو الدوران) والسابق. بعد ذلك يحسب كسر الإقحام، التي يمكن أن يصل من 0 إلى 1. بعد ذلك يتم وضع اللاعب في الموقع المساوي بين هاتين النقطتين المحفوظتين، على سبيل المثال، 40% على الطريقة إلى الإطار المفتاحي الأخير.
عندما تقوم بتبطيئها وتشعيلها إطار بإطار، يمكنك فعليا رؤية شخصية اللاعب تتحرك بين هاتين الإطارين المفتاحيين، لكن خلال اللعب، إنها غير ملاحظة.
وهكذا لدينا إلى حد كبير تخفيض تعقيد إعداد ترجيع الوقت وجعله أكثر استقرارا.
فقط تسجيل عدد محدد من الإطارات المفتاحية
الآن قد قلصنا إلى حد كبير عدد الإطارات إننا فعلا من بأمان، يمكننا أن نتأكد أننا لا نقم بحفظ الكثير من البيانات.
الآن للتو قمنا بتكويم البيانات المسجلة داخل المصفوفة، التي لن تفعل على المدى الطويل. كما تنمو المصفوفة، سوف تصبح غير عملية أكثر، سوف يستغرق الوصول كميات كبيرة من الوقت،
من أجل حل هذه المشكلة، يمكننا إنشاء كود الذي يتحقق إذا كانت المصفوفة تنمو أكثر من حجم معين. إذا كنا نعرف كم عدد الإطارات في الثانية يمكننا حفظه، يمكننا تحديد عدد الثواني من الوقت المرجع ينبغي علينا حفظه، وما يناسب لعبتنا وتعقيدها. Prince of Persia معقدة إلى حد ما يسمح لربما 15 ثانية من وقت المرجع، في حين يسمح الإعداد الأبسط الخاص في Braid بالترجيع الغير محدود.
1 |
if(playerPositions.Count > 128) |
2 |
{
|
3 |
playerPositions.RemoveAt(0); |
4 |
playerRotations.RemoveAt(0); |
5 |
}
|
ما يحدث أنه ما إن تنمو المصفوفة أكبر من حجم معين، نقوم بإزالة الإدخال الأول منه. بالتالي فإنها تبقى فقط طالما أننا نريد اللاعب يقوم بالترجيع، وليس هناك خطر من أن تصبح كبيرة جداً بحيث تستخدم بكفاءة. ضع هذا في الدالة FixedUpdate
بعد التسجيل واعادة التعليمات البرمجية.
استخدام كلاس مخصص للاحتفاظ ببيانات لاعب
حاليا قمنا بتسجيل موقع ودوران اللاعب داخل مصفوفات منفصلة. في حين أن هذا يعمل، يجب علينا تذكر أنه دائما تسجيل والوصول إلى البيانات في مكانين في نفس الوقت،
ما يمكن أن نفعله، على أية حال. هو إنشاء كلاس منفصل لإجراء كل هذه الأشياء، وربما أكثر من ذلك (إذا كان هذا ضروري في المشروع الخاص بك).
التعليمات البرمجية لكلاس مخصص للعمل كحاوية للبيانات تبدو مثل هذا:
1 |
public class Keyframe |
2 |
{
|
3 |
public Vector3 position; |
4 |
public Vector3 rotation; |
5 |
|
6 |
public Keyframe(Vector3 position, Vector3 rotation) |
7 |
{
|
8 |
this.position = position; |
9 |
this.rotation = rotation; |
10 |
}
|
11 |
}
|
يمكنك إضافته إلى ملف TimeController.cs، قبل بدء إعلان الكلاس مباشرة. ما يفعله هو توفير حاوية لحفظ كل من موقع ودوران اللاعب على حد سواء. يسمح method المنشئ بالإنشاء مباشرة مع المعلومات الضرورية.
باقي الخوارزمية سوف تحتاج إلى التكيف للعمل مع النظام الجديد. في method Start، يجب تهيئة المصفوفة:
1 |
keyframes = new ArrayList(); |
وبدلاً من قول:
1 |
playerPositions.Add (player.transform.position); |
2 |
playerRotations.Add (player.transform.localEulerAngles); |
يمكننا حفظه مباشرة داخل كائن الإطار المفتاحي:
1 |
keyframes.Add(new Keyframe(player.transform.position, player.transform.localEulerAngles)); |
الذي سوف نفعله الان هو أن نقوم بإضافة موقع ودوران اللاعب داخل نفس الكائن، الذي يضاف بعد ذلك داخل مصفوفة واحدة، مما يقلل كثيرا من تعقيد هذا الإعداد.
إضافة تأثير التشويه لتحديد إذا كان الترجيع يحدث
نحن بحاجة إلى نوعا من الدليل يقول لنا أنه حاليا يجري إرجاع اللعبة جذريا. حالا، نحن نعلم هذا، لكن قد يكون اللاعب محتار. في مثل هذه الحالات من جيد أن تكون العديد من الأشياء التي تقول لللاعب أن الترجيع يحدث، مثل بصريا (عبر الشاشة بأكملها تشوش قليلاً) والصوت (بواسطة الموسيقى تبطئ وتعكس).
دعونا نفعل شيئا مماثل إلى كيف يفعل Prince of Persia، وإضافة بعض الضبابية.



Unity يسمح لك بإضافة مؤثرات كاميرا متعددة في أعلى بعضهم البعض، ومع بعض التجريب يمكنك صنع واحد الذي يناسب مشروعك تماما.
قبل أن يمكننا استخدام التأثيرات الأساسية، نحن بحاجة إلى استيرادها. للقيام بهذا، انتقل إلى Assets > Import Package > Effects، واستيراد كل ما يتاح لك.



يمكن إضافة التأثيرات المرئية مباشرة إلى الكاميرا الرئيسية. انتقل إلى Components > Image Effects وأضف تأثير Blur وBloom. الجمع بين هذين ينبغي أن يوفر تأثير جميل للذي سوف نقوم به لأجله.



عندما تجربها الآن، اللعبة ستعمل هذا التأثير في كل الوقت.



الان نحن في حاجة إلى تفعيله وإلغاء تفعيله على التوالي. لأجل هذا، الـ TimeController
يحتاج إلى استيراد صور التأثيرات. أضف هذا السطر إلى البداية القصوى:
1 |
using UnityStandardAssets.ImageEffects; |
للوصول إلى الكاميرا من TimeController
، أضف هذا المتغير:
1 |
private Camera camera; |
وأنسبه في دالة Start
:
1 |
camera = Camera.main; |
بعد ذلك أضف هذه التعليمة البرمجية لتفعيل التأثيرات خلال وقت الترجيع، وجعلهم غير مفعلين خلاف ذلك:
1 |
void Update() |
2 |
{
|
3 |
if(Input.GetKey(KeyCode.Space)) |
4 |
{
|
5 |
isReversing = true; |
6 |
camera.GetComponent<Blur>().enabled = true; |
7 |
camera.GetComponent<Bloom>().enabled = true; |
8 |
}
|
9 |
else
|
10 |
{
|
11 |
isReversing = false; |
12 |
firstRun = true; |
13 |
camera.GetComponent<Blur>().enabled = false; |
14 |
camera.GetComponent<Bloom>().enabled = false; |
15 |
}
|
16 |
}
|
عند الضغط على زر space، يمكنك الآن ليس فقط ترجيع المشهد، ولكن يمكنك أيضا تنشيط تأثير الترجيع على الكاميرا، والقول لللاعب أن شيئا ما يحدث.
التعليمة البرمجية TimeController
بأكملها يجب أن تبدو هكذا:
1 |
using UnityEngine; |
2 |
using System.Collections; |
3 |
using UnityStandardAssets.ImageEffects; |
4 |
|
5 |
public class Keyframe |
6 |
{
|
7 |
public Vector3 position; |
8 |
public Vector3 rotation; |
9 |
|
10 |
public Keyframe(Vector3 position, Vector3 rotation) |
11 |
{
|
12 |
this.position = position; |
13 |
this.rotation = rotation; |
14 |
}
|
15 |
}
|
16 |
|
17 |
public class TimeController: MonoBehaviour |
18 |
{
|
19 |
public GameObject player; |
20 |
public ArrayList keyframes; |
21 |
public bool isReversing = false; |
22 |
|
23 |
public int keyframe = 5; |
24 |
private int frameCounter = 0; |
25 |
private int reverseCounter = 0; |
26 |
|
27 |
private Vector3 currentPosition; |
28 |
private Vector3 previousPosition; |
29 |
private Vector3 currentRotation; |
30 |
private Vector3 previousRotation; |
31 |
|
32 |
private Camera camera; |
33 |
|
34 |
private bool firstRun = true; |
35 |
|
36 |
void Start() |
37 |
{
|
38 |
keyframes = new ArrayList(); |
39 |
camera = Camera.main; |
40 |
}
|
41 |
|
42 |
void Update() |
43 |
{
|
44 |
if(Input.GetKey(KeyCode.Space)) |
45 |
{
|
46 |
isReversing = true; |
47 |
camera.GetComponent<Blur>().enabled = true; |
48 |
camera.GetComponent<Bloom>().enabled = true; |
49 |
}
|
50 |
else
|
51 |
{
|
52 |
isReversing = false; |
53 |
firstRun = true; |
54 |
camera.GetComponent<Blur>().enabled = false; |
55 |
camera.GetComponent<Bloom>().enabled = false; |
56 |
}
|
57 |
}
|
58 |
|
59 |
void FixedUpdate() |
60 |
{
|
61 |
if(!isReversing) |
62 |
{
|
63 |
if(frameCounter < keyframe) |
64 |
{
|
65 |
frameCounter += 1; |
66 |
}
|
67 |
else
|
68 |
{
|
69 |
frameCounter = 0; |
70 |
keyframes.Add(new Keyframe(player.transform.position, player.transform.localEulerAngles)); |
71 |
}
|
72 |
}
|
73 |
else
|
74 |
{
|
75 |
if(reverseCounter > 0) |
76 |
{
|
77 |
reverseCounter -= 1; |
78 |
}
|
79 |
else
|
80 |
{
|
81 |
reverseCounter = keyframe; |
82 |
RestorePositions(); |
83 |
}
|
84 |
|
85 |
if(firstRun) |
86 |
{
|
87 |
firstRun = false; |
88 |
RestorePositions(); |
89 |
}
|
90 |
|
91 |
float interpolation = (float) reverseCounter / (float) keyframe; |
92 |
player.transform.position = Vector3.Lerp(previousPosition, currentPosition, interpolation); |
93 |
player.transform.localEulerAngles = Vector3.Lerp(previousRotation, currentRotation, interpolation); |
94 |
}
|
95 |
|
96 |
if(keyframes.Count > 128) |
97 |
{
|
98 |
keyframes.RemoveAt(0); |
99 |
}
|
100 |
}
|
101 |
|
102 |
void RestorePositions() |
103 |
{
|
104 |
int lastIndex = keyframes.Count - 1; |
105 |
int secondToLastIndex = keyframes.Count - 2; |
106 |
|
107 |
if(secondToLastIndex >= 0) |
108 |
{
|
109 |
currentPosition = (keyframes[lastIndex] as Keyframe).position; |
110 |
previousPosition = (keyframes[secondToLastIndex] as Keyframe).position; |
111 |
|
112 |
currentRotation = (keyframes[lastIndex] as Keyframe).rotation; |
113 |
previousRotation = (keyframes[secondToLastIndex] as Keyframe).rotation; |
114 |
|
115 |
keyframes.RemoveAt(lastIndex); |
116 |
}
|
117 |
}
|
118 |
}
|
قم بتنزيل حزمة البناء المرفقة وجربها!
خاتمة
لعبة ترجيع الوقت الخاصة بنا الآن أفضل بكثير من قبل. الخوارزمية في تحسن ملحوظ وتستخدم 90% أقل من قوة المعالجة، وأكثر استقرارا، ولدينا دليل جميل يقول لنا أننا حاليا قيد ترجيع الوقت.
الآن اذهب وانشئ لعبة باستخدام هذا!