على المستوى يمكن أن يكون الآلاف من الأعداء.مهمة المدافع: واجه
Valley of the Forgotten DX دائمًا مشاكل طويلة الأمد في السرعة ، وتمكنت أخيرًا من حلها. كان الحافز الرئيسي لزيادة السرعة بشكل كبير هو
منفذنا على PlayStation Vita . تم إصدار اللعبة بالفعل على جهاز الكمبيوتر وعملت بشكل جيد ، إن لم يكن بشكل مثالي ، على
Xbox One مع
PS4 . ولكن بدون تحسن كبير في اللعبة ، لن نتمكن من إطلاقها على Vita.
عندما تتباطأ اللعبة ، يلوم المعلقون على الإنترنت عادةً لغة البرمجة أو المحرك. صحيح أن لغات مثل C # و Java أغلى من C و C ++ ، والأدوات مثل Unity لديها مشاكل غير قابلة للحل ، مثل جمع القمامة. في الواقع ، يأتي الناس بمثل هذه التفسيرات لأن اللغة والمحرك هما أكثر خصائص البرمجيات وضوحًا. لكن القتلة الحقيقيين للأداء قد يكونون تفاصيل صغيرة غبية لا علاقة لها بالهندسة المعمارية.
0. أدوات التنميط
هناك طريقة حقيقية واحدة فقط لجعل اللعبة أسرع - لأداء التنميط. اكتشف ما يقضيه الكمبيوتر كثيرًا من الوقت واجعله يقضي وقتًا أقل عليه ، أو حتى أفضل ، اجعله لا يضيع الوقت
على الإطلاق .
أبسط أداة تحديد الملفات الشخصية هي مراقب نظام Windows القياسي (مراقب الأداء):
في الواقع ، هذه أداة مرنة إلى حد ما ومن السهل جدًا العمل معها. فقط اضغط على Ctrl + Alt + Delete ، افتح "إدارة المهام" وانقر على علامة التبويب "الأداء". ومع ذلك ، لا تقم بتشغيل برامج أخرى كثيرة. إذا نظرت عن كثب ، يمكنك بسهولة اكتشاف القمم في استخدام وحدة المعالجة المركزية وحتى تسرب الذاكرة. هذه طريقة غير مفيدة ، لكنها قد تكون الخطوة الأولى في إيجاد أماكن بطيئة.
تتم كتابة مهمة المدافع بلغة
Haxe عالية المستوى المترجمة إلى لغات أخرى (كان هدفي الرئيسي C ++). هذا يعني أن أي أداة قادرة على تحديد ملف تعريف C ++ يمكنها أيضًا تعريف رمز C ++ الذي أنشأته Haxe. لذلك عندما أردت أن أفهم أسباب المشاكل ، قمت بتشغيل Performance Explorer من Visual Studio:
بالإضافة إلى ذلك ، تحتوي وحدات التحكم المختلفة على أدوات التنميط الخاصة بها ، وهو أمر مريح للغاية ، ولكن بسبب اتفاقية عدم الإفشاء ، لا يمكنني أن أخبرك بأي شيء عنها. ولكن إذا كان لديك حق الوصول إليها ، ولكن تأكد من استخدامها!
بدلاً من كتابة برنامج تعليمي رهيب حول كيفية استخدام أدوات التنميط مثل Performance Explorer ، أترك فقط رابطًا
للوثائق الرسمية وانتقل إلى الموضوع الرئيسي - أشياء مذهلة أدت إلى زيادة كبيرة في الإنتاجية ، وكيف تمكنت من العثور عليها !
1. كشف المشكلة
أداء اللعبة ليس فقط تسريع نفسه ، ولكن أيضًا إدراكه. Defender's Quest هي لعبة من نوع الدفاع عن البرج يتم تقديمها بمعدل 60 إطارًا في الثانية ، ولكن مع سرعة لعب متغيرة في النطاق من 1 / 4x إلى 16x. بغض النظر عن سرعة اللعبة ، تستخدم المحاكاة طابعًا زمنيًا
ثابتًا مع 60 تحديثًا في الثانية من وقت المحاكاة 1x. أي إذا قمت بتشغيل اللعبة بسرعة 16x ، فإن منطق التحديث سيعمل بالفعل بتردد
960 FPS . بصراحة ، هذه طلبات عالية جدًا للعبة! ولكن أنا من أنشأ هذا الوضع ، وإذا اتضح أنه بطيء ، فسوف يلاحظه اللاعبون بالتأكيد.
وفي اللعبة يوجد
مثل هذا المستوى:
هذه هي معركة المكافأة النهائية "Endless 2" ، وهي أيضًا "كابوسي الشخصي". تم التقاط لقطة الشاشة في وضع لعبة جديدة + ، حيث لا يكون الأعداء أقوى بكثير فحسب ، بل لديهم أيضًا ميزات مثل استعادة الصحة. استراتيجية اللاعب المفضلة هنا هي ضخ التنين إلى أقصى مستوى هدير (هجوم AOE الذي يذهل الأعداء) ، وخلفهم وضع عدد من الفرسان مع Knockback تضخ إلى أقصى حد لدفع كل شخص يمرر التنانين إلى منطقة عمله. التأثير التراكمي هو أن مجموعة ضخمة من الوحوش تبقى بلا نهاية في مكان واحد ، لفترة أطول بكثير من بقاء اللاعبين إذا قتلوا بالفعل. نظرًا لأن اللاعبين بحاجة إلى
انتظار الموجات وعدم
قتلهم من أجل الحصول على المكافآت والإنجازات ، فإن هذه الإستراتيجية فعالة للغاية ورائعة - وهذا هو بالضبط سلوك اللاعبين الذين حفزتهم.
لسوء الحظ ، تبين أيضًا أن هذه حالة
مرضية للأداء ،
خاصة عندما يرغب اللاعبون في اللعب بسرعات 16x أو 8x. بالطبع ، فقط أكثر اللاعبين المتشددين سيحاولون الحصول على إنجاز "Hundredth Wave" في لعبة جديدة + على مستوى Endless 2 ، لكنهم فقط أولئك الذين يتحدثون اللعبة بصوت أعلى ، لذلك أردت أن يكونوا سعداء.
إنها مجرد لعبة ثنائية الأبعاد مع مجموعة من العفاريت ، فما الخطأ في ذلك؟
وبالفعل. دعنا نحصل على حق.
2. قرار التصادم
ألق نظرة على لقطة الشاشة هذه:
ترى هذا الخبز حول الحارس؟ هذه هي منطقة تأثيرها - لاحظ أن هناك أيضًا منطقة ميتة
لا يمكنها فيها ضرب الأهداف. كل فئة لها منطقة هجوم خاصة بها ، وكل مدافع لديه مساحة مختلفة الحجم ، اعتمادًا على مستوى التعزيز والمعلمات الشخصية. ويمكن لكل مدافع نظريا أن يستهدف أي عدو في مجال وصوله. وينطبق الشيء نفسه على أنواع معينة من الأعداء. يمكن أن يكون هناك ما يصل إلى 36 مدافعًا على الخريطة (وليس بما في ذلك الشخصية الرئيسية أزرو) ، ولكن لا يوجد حد أقصى لعدد الأعداء. كل مدافع وعدو لديه قائمة بالأهداف المحتملة ، التي تم إنشاؤها على أساس المكالمات للتحقق من المنطقة في كل خطوة تحديث (باستثناء القطع المنطقي لأولئك الذين لا يستطيعون الهجوم في الوقت الحالي ، وما إلى ذلك).
اليوم ، تعد معالجات الفيديو سريعة جدًا - إذا لم تجهدها كثيرًا ، فيمكنها معالجة أي عدد من المضلعات تقريبًا. ولكن حتى أسرع وحدات المعالجة المركزية لديها بسهولة "اختناقات" في الإجراءات البسيطة ، وخاصة تلك التي تنمو بشكل كبير. هذا هو السبب في أن لعبة ثنائية الأبعاد يمكن أن تكون أبطأ من لعبة ثلاثية الأبعاد أكثر جمالًا - ليس لأن المبرمج لا يستطيع التعامل (ربما هذا أيضًا ، على الأقل في حالتي) ، ولكن من حيث المبدأ لأن المنطق يمكن أن يكون في بعض الأحيان أكثر تكلفة ، من الرسم! السؤال ليس عدد الأشياء التي تظهر على الشاشة ، ولكن ماذا
يفعلون .
دعونا استكشاف وتسريع التعرف على التصادم. للمقارنة ، سأقول أنه قبل التحسين ، استغرق التعرف على التصادم ما يصل إلى 50٪ تقريبًا من وقت وحدة المعالجة المركزية في دورة المعركة الرئيسية. بعد التحسين ، أقل من 5٪.
الأمر كله يتعلق بالأشجار الرباعية
الحل الرئيسي لمشكلة التعرف على التصادم البطيء هو
تقسيم المساحة - ومنذ البداية استخدمنا تنفيذًا عالي الجودة
للشجرة الرباعية . بشكل أساسي ، فإنه يفصل المساحة بشكل فعال بحيث يمكن تخطي العديد من اختبارات التصادم الاختيارية.
في كل إطار ، نقوم بتحديث شجرة الأرباع بأكملها (QuadTree) من أجل تتبع موضع كل كائن ، وعندما يريد العدو أو المدافع التصويب على شخص ما ، فإنه يسأل QuadTree للحصول على قائمة بالأشياء المجاورة. لكن المحلل أخبرنا أن كلتا العمليتين أبطأ بكثير مما ينبغي.
ما هو الخطأ هنا؟
كما اتضح - الكثير.
سلسلة الكتابة
بما أنني احتفظت بالأعداء والمدافعين في شجرة رباعية ، كان علي أن أشير إلى ما كنت أبحث عنه ، وقد تم ذلك على النحو التالي:
var things:Array<XY> = _qtree.queryRange(zone.bounds, "e"); //"e" - "enemy"
في لغة المبرمجين ، يسمى هذا رمز
كتابة السلسلة ، ومن بين أسباب أخرى ، هذا سيئ لأن مقارنات السلسلة أبطأ دائمًا من المقارنات الصحيحة.
التقطت الثوابت الصحيحة بسرعة واستبدلت الرمز بهذا:
var things:Array<XY> = _qtree.queryRange(zone.bounds, QuadTree.ENEMY);
(نعم ، ربما كان الأمر يستحق استخدام
Enum Abstract لأقصى قدر من الأمان ، لكنني كنت في عجلة من أمري ، وكنت بحاجة للقيام بالعمل أولاً.)
قدم هذا التغيير وحده مساهمة
كبيرة ، لأن هذه الوظيفة تسمى
باستمرار وبشكل متكرر ، في كل مرة يحتاج فيها شخص ما إلى قائمة أهداف جديدة.
Array vs Vector
ألق نظرة على هذا:
var things:Array<XY>
صفائف Haxe تشبه إلى حد كبير صفائف ActionScript و JS من حيث أنها مجموعات من الكائنات القابلة لتغيير الحجم ، ولكن في Haxe يتم كتابتها بقوة.
ومع ذلك ، هناك بنية بيانات أخرى أكثر كفاءة مع لغات الهدف الثابتة مثل cpp ، وهي
haxe.ds.Vector . متجهات Haxe هي في الأساس نفس المصفوفات ، باستثناء أنه عندما يتم إنشاؤها فإنها تحصل على حجم ثابت.
نظرًا لأن أشجاري الرباعية تحتوي بالفعل على حجم ثابت ، فقد قمت باستبدال الصفيفات بنواقل لتحقيق زيادة ملحوظة في السرعة.
اطلب فقط ما تحتاجه
في السابق ،
queryRange
دالة
queryRange
بإرجاع قائمة بالكائنات ، مثيلات
XY
. احتوت على إحداثيات س / ص لكائن اللعبة المشار إليه ومعرفه الصحيح الفريد (فهرس البحث في الصفيف الرئيسي). تلقى كائن اللعبة الذي يقوم بتنفيذ الطلب هذه XYs ، واستخرج معرفًا صحيحًا للحصول على هدفه ، ثم نسي الباقي.
فلماذا يجب أن أنقل كل هذه المراجع إلى كائنات XY لكل عقدة QuadTree
بشكل متكرر ، وحتى
960 مرة لكل إطار؟ يكفي أن أعيد قائمة المعرفات الصحيحة.
تلميح احترافي: الأعداد الصحيحة أسرع بكثير في الإرسال من جميع أنواع البيانات الأخرى تقريبًا!بالمقارنة مع التصحيحات الأخرى ، كان هذا بسيطًا جدًا ، لكن نمو الأداء كان لا يزال ملحوظًا ، لأن هذه الحلقة الداخلية تم استخدامها بنشاط كبير.
تحسين العودية الذيل
هناك شيء أنيق يسمى
تحسين المكالمة الذيل . من الصعب شرح ذلك ، لذا سأريكم مثالاً أفضل.
كان:
nw.queryRange(Range, -1, result);
ne.queryRange(Range, -1, result);
sw.queryRange(Range, -1, result);
se.queryRange(Range, -1, result);
return result;
أصبح:
return se.queryRange(Range, filter, sw.queryRange(Range, filter, ne.queryRange(Range, filter, nw.queryRange(Range, filter, result))));
يعيد الرمز نفس النتائج المنطقية ، ولكن وفقًا للملف الشخصي ، يكون الخيار الثاني أسرع ، على الأقل عند الترجمة إلى cpp. يؤدي كلا المثالين نفس المنطق تمامًا - حيث يُجريان تغييرات على بنية البيانات "الناتجة" ويمررونها إلى الوظيفة التالية قبل العودة. عندما نقوم بذلك بشكل متكرر ، يمكننا تجنب المترجم إنشاء مراجع مؤقتة ، لأنه يمكنه ببساطة إرجاع نتيجة الوظيفة السابقة على الفور ، بدلاً من الالتزام بها في خطوة إضافية. أو شيء من هذا القبيل. أنا لا أفهم تمامًا كيف يعمل هذا ، لذا اقرأ المنشور على الرابط أعلاه.
(بناءً على ما أعرفه ، لا يحتوي الإصدار الحالي من مترجم Haxe على وظيفة تحسين العودية ، أي أنه من المحتمل أن يكون عمل مترجم C ++ - لذلك لا تفاجأ إذا لم تعمل هذه الحيلة عند ترجمة كود Haxe ليس في cpp.)
تجمع الكائنات
إذا كنت بحاجة إلى نتائج دقيقة ، فيجب علي تدمير وإعادة بناء QuadTree مرة أخرى مع كل مكالمة تحديث. يعد إنشاء مثيلات QuadTree جديدة مهمة شائعة إلى حد ما ، ولكن مع وجود أعداد كبيرة من كائنات AABB و XY الجديدة ، أدت QuadTrees التي تعتمد عليها إلى زيادة الذاكرة بشكل كبير. نظرًا لأن هذه كائنات بسيطة جدًا ، سيكون من المنطقي تخصيص الكثير من هذه الأشياء مقدمًا وإعادة استخدامها باستمرار. وهذا ما يسمى
تجمع الكائنات .
كنت أفعل شيئًا كهذا:
nw = new QuadTree( new AABB( cx - hs2x, cy - hs2y, hs2x, hs2y) );
ne = new QuadTree( new AABB( cx + hs2x, cy - hs2y, hs2x, hs2y) );
sw = new QuadTree( new AABB( cx - hs2x, cy + hs2y, hs2x, hs2y) );
se = new QuadTree( new AABB( cx + hs2x, cy + hs2y, hs2x, hs2y) );
ولكن بعد ذلك استبدلت الرمز بهذا:
nw = new QuadTree( AABB.get( cx - hs2x, cy - hs2y, hs2x, hs2y) );
ne = new QuadTree( AABB.get( cx + hs2x, cy - hs2y, hs2x, hs2y) );
sw = new QuadTree( AABB.get( cx - hs2x, cy + hs2y, hs2x, hs2y) );
se = new QuadTree( AABB.get( cx + hs2x, cy + hs2y, hs2x, hs2y) );
نستخدم إطار عمل
HaxeFlixel مفتوح المصدر ، لذلك قمنا
بتنفيذ ذلك باستخدام فئة FlxPool HaxeFlixel. في حالة مثل هذه التحسينات المتخصصة للغاية ، غالبًا ما أستبدل بعض عناصر Flixel الأساسية (على سبيل المثال ، التعرف على التصادم) بالتنفيذ الخاص بي (كما فعلت مع QuadTrees) ، لكن FlxPool أفضل من كل شيء كتبته بنفسي ويفعل ما يحتاجه بالضبط.
التخصص إذا لزم الأمر
كائن
XY
هو فئة بسيطة لها خصائص
x
و
y
و
int_id
. نظرًا لأنه تم استخدامه في حلقة داخلية مستخدمة بشكل خاص ، يمكنني حفظ الكثير من أوامر وعمليات تخصيص الذاكرة عن طريق نقل كل هذه البيانات إلى بنية بيانات خاصة توفر نفس الوظيفة مثل
Vector<XY>
. اتصلت
XYVector
الجديدة ويمكن رؤية النتيجة
هنا . هذا تطبيق متخصص للغاية وغير مرن في نفس الوقت ، لكنه قدم لنا بعض التحسينات في السرعة.
وظائف مدمجة
الآن ، بعد أن أكملنا المرحلة الواسعة من التعرف على التصادم ، نحتاج إلى إجراء الكثير من الفحوصات لمعرفة أي الأشياء تتصادم بالفعل. حيثما أمكن ، أحاول مقارنة النقاط والأرقام ، وليس الأرقام والأرقام ، ولكن في بعض الأحيان يجب أن أقوم بالأخيرة. على أي حال ، كل هذا يتطلب فحوصات خاصة به:
private static function _collide_circleCircle(a:Zone, b:Zone):Bool { var dx:Float = a.centerX - b.centerX; var dy:Float = a.centerY - b.centerY; var d2:Float = (dx * dx) + (dy * dy); var r2:Float = (a.radius2) + (b.radius2); return d2 < r2; }
يمكن تحسين كل هذا
inline
واحدة:
private static inline function _collide_circleCircle(a:Zone, b:Zone):Bool { var dx:Float = a.centerX - b.centerX; var dy:Float = a.centerY - b.centerY; var d2:Float = (dx * dx) + (dy * dy); var r2:Float = (a.radius2) + (b.radius2); return d2 < r2; }
عندما نضيف مضمنة إلى دالة ، فإننا نطلب من المترجم نسخ ولصق هذا الرمز ولصق المتغيرات عند استخدامها ، وعدم إجراء مكالمة خارجية لوظيفة منفصلة ، مما يؤدي إلى تكاليف غير ضرورية. لا يكون التضمين قابلاً للتطبيق دائمًا (على سبيل المثال ، يؤدي إلى تضخيم مقدار التعليمات البرمجية) ، ولكنه مثالي للحالات التي يتم فيها استدعاء الوظائف الصغيرة مرارًا وتكرارًا.
نعيد إلى الذهن الصراعات
الدرس الحقيقي هنا هو أنه في العالم الحقيقي ، التحسينات ليست دائمًا من نفس النوع. مثل هذه الإصلاحات هي مزيج من التقنيات المتقدمة والاختراق الرخيص وتطبيق التوصيات المنطقية والقضاء على الأخطاء الغبية. كل هذا بشكل عام يمنحنا دفعة في الأداء.
ولكن لا يزال -
قياس سبع مرات ، وقطع واحدة!ساعتان من التحسين الأمثل للوظيفة ، التي تسمى مرة واحدة كل ستة إطارات وأخذ 0.001 مللي ثانية ، لا تستحق الجهد ، على الرغم من قبيحة وغباء الكود.
3. فرز كل شيء
في الواقع ، كان هذا أحد التحسينات الأخيرة التي قمت بها ، ولكن تبين أنها مفيدة جدًا لدرجة أنها تستحق لقبها الخاص. بالإضافة إلى ذلك ، كانت أبسط وأثبتت نفسها مرارًا وتكرارًا. أظهر لي المحلل إجراءات لم أتمكن من تحسينها على الإطلاق - حلقة الرسم الرئيسية () ، والتي استغرقت الكثير من الوقت. كان السبب هو الوظيفة التي قامت بفرز جميع عناصر الشاشة قبل التقديم - أي أن
فرز جميع النقوش المتحركة استغرق وقتًا أطول بكثير من رسمها!
إذا نظرت إلى لقطات الشاشة من اللعبة ، فسترى أن جميع الأعداء والمدافعين يتم فرزهم أولاً حسب
y
، ثم حسب
x
، بحيث تتداخل العناصر مع بعضها البعض من الخلف إلى الأمام ، من اليسار إلى اليمين ، عندما نتحرك من أعلى اليسار إلى الزاوية اليمنى السفلية من الشاشة.
إحدى طرق التفوق على الفرز هي ببساطة تمرير الفرز التقديم عبر الإطار. هذه خدعة مفيدة لبعض الوظائف المكلفة ، لكنها أدت على الفور إلى أخطاء بصرية ملحوظة للغاية ، لذلك لم تناسبنا.
وأخيرًا ، جاء القرار من أحد مشرفي HaxeFlixel
Jens Fisher . سأل: "هل تأكدت من استخدام خوارزمية فرز سريعة للصفائف التي تم فرزها تقريبًا؟"
لا! اتضح أن لا. استخدمت فرز الصفيف من مكتبة Haxe القياسية (أعتقد أنه كان
دمجًا للفرز - خيار جيد للحالات العامة. ولكن كان لدي حالة
خاصة جدًا. عند الفرز في كل إطار ، لا يغير موضع الفرز سوى عدد صغير جدًا من النقوش المتحركة ، حتى إذا كان هناك الكثير منها. لذلك لقد استبدلت مكالمة الفرز القديمة بالفرز
حسب المدخلات ،
وازدهار! - زادت السرعة على الفور.
4. مشاكل فنية أخرى
كان التعرف على الاصطدام والفرز انتصارات كبيرة في منطق
update()
draw()
، ولكن تم إخفاء العديد من المزالق في الحلقات الداخلية المستخدمة بنشاط.
Std.is () والممثلين
في الحلقات الداخلية "الساخنة" المختلفة ، كان لدي رمز مشابه:
if(Std.is(something,Type)) { var typed:Type = cast(something,Type); }
في لغة
Std.is()
،
Std.is()
ما إذا كان الكائن ينتمي إلى نوع معين (نوع) أو فئة (فئة) ، ويحاول إرساله إلى نوع معين أثناء تنفيذ البرنامج.
هناك إصدارات آمنة وغير محمية من
cast
- تؤدي الجبيرة الآمنة إلى انخفاض الأداء ، لكن الجبيرة غير المحمية لا تفعل ذلك.
آمن:
cast(something, Type);
غير محمي:
var typed:Type = cast something;
عندما تفشل محاولة الإلقاء غير الآمنة ، نحصل على قيمة فارغة ، بينما يلقي الإرسال الآمن استثناءً. ولكن إذا كنا لن نلتقط استثناءً ، فما الفائدة من إلقاء فريق آمن؟ بدون الصيد ، لا تزال العملية تفشل ، لكنها تعمل بشكل أبطأ.
بالإضافة إلى ذلك ، من غير المجدي أن تسبق عملية
Std.is()
آمنة باستخدام علامة
Std.is()
. السبب الوحيد لاستخدام الجبيرة الآمنة هو استثناء مضمون ، ولكن إذا تحققنا من النوع قبل الجبيرة ، فنحن نضمن بالفعل أن الجبيرة لن تفشل!
يمكنني تسريع
Std.is()
قليلاً مع
Std.is()
بعد التحقق من
Std.is()
. ولكن لماذا نحتاج إلى إعادة كتابة نفس الشيء إذا لم أكن بحاجة إلى التحقق من نوع الصف على الإطلاق؟
افترض أن لدي
CreatureSprite
، والذي يمكن أن يكون
EnemySprite
فرعية من
DefenderSprite
أو
EnemySprite
. بدلاً من استدعاء
Std.is(this,DefenderSprite)
يمكننا إنشاء حقل عدد صحيح في
CreatureSprite
بقيم مثل
CreatureType.DEFENDER
أو
CreatureType.ENEMY
، والتي يتم تحديدها بشكل أسرع.
أكرر ، يجدر إصلاحه فقط في تلك الأماكن التي يتم فيها تسجيل تباطؤ كبير بشكل واضح.
بالمناسبة ، يمكنك قراءة المزيد حول القالب
الآمن وغير
المحمي في
دليل Haxe .
تسلسل / إلغاء تسلسل الكون
كان من المزعج العثور على مثل هذه الأماكن في الرمز:
function copy():SomeClass { return SomeClass.fromXML(this.toXML()); }
أجل. لنسخ كائن ، نقوم
بتسلسله إلى XML ، ثم نقوم
بتحليل كل هذا XML ، وبعد ذلك نتخلص على الفور من XML ونعيد كائنًا جديدًا. ربما تكون هذه هي أبطأ طريقة لنسخ كائن ، بالإضافة إلى أنها تزيد من الذاكرة. في البداية ، كتبت مكالمات XML للحفظ والتحميل من القرص ، وأعتقد أنني كنت كسولًا جدًا لكتابة إجراءات النسخ الصحيحة.
ربما ، سيكون كل شيء على ما يرام إذا كانت هذه الوظيفة نادرًا ما تستخدم ، ولكن هذه المكالمات نشأت في أماكن غير مناسبة في منتصف اللعبة. لذلك جلست وبدأت في كتابة واختبار وظيفة النسخ الصحيحة.
قل لا لـ Null
غالبًا ما يتم استخدام التحقق من المساواة للصفر ، ولكن عند ترجمة Haxe إلى cpp ، فإن الكائن الذي يسمح بقيمة غير محددة يؤدي إلى تكاليف غير ضرورية لا تنشأ إذا كان المترجم يمكن أن يفترض أن الكائن لن يكون فارغًا أبدًا. وينطبق هذا بشكل خاص على الأنواع الأساسية مثل
Int
- Haxe التي تنفذ صلاحية قيمة غير محددة لها في نظام الهدف الثابت بواسطة "
var myVar:Null<Int>
" الخاصة بها ، والتي لا تحدث فقط للمتغيرات التي تم الإعلان عنها صراحة على أنها فارغة (
var myVar:Null<Int>
) ، ولكن أيضًا لأشياء مثل خيارات المساعد (
?myParam:Int
). بالإضافة إلى ذلك ، تتسبب الشيكات الفارغة نفسها في إهدار غير ضروري.
تمكنت من إصلاح بعض هذه المشاكل بمجرد النظر إلى الشفرة والتفكير في البدائل - هل يمكنني إجراء فحص أبسط ، والذي سيكون دائمًا صحيحًا عندما يكون الكائن فارغًا؟ هل يمكنني الحصول على قيمة فارغة قبل ذلك بكثير في سلسلة المكالمات الوظيفية وتمرير علامة عددية أو منطقية بسيطة إلى مكالمات الأطفال؟ هل يمكنني بناء كل شيء بحيث لا تكون القيمة مضمونة
مطلقًا لتصبح فارغة؟ وهكذا دواليك. لا يمكننا القضاء تمامًا على عمليات التحقق الفارغة والقيم القابلة للإلغاء ، ولكن إخراجها من الوظائف ساعدني كثيرًا.
5. تنزيل الوقت
على PSVita ، واجهتنا مشاكل خطيرة خاصة في وقت تحميل بعض المشاهد. عند التنميط ، اتضح أن الأسباب تتلخص بشكل أساسي في تنقيط النص ، وعرض البرامج غير الضروري ، وعرض الزر المكلفة ، وأشياء أخرى.
نص
يعتمد
HaxeFlixel على
OpenFL ، الذي يحتوي على TextField رائع وموثوق. لكنني استخدمت كائنات FlxText بطريقة غير كاملة - تحتوي كائنات FlxText على حقل نص OpenFL داخلي يتم تنقيطه. ومع ذلك ، اتضح أنني لم أكن بحاجة إلى معظم وظائف النص المعقدة هذه ، ولكن بسبب الطريقة الغبية لإعداد نظام واجهة المستخدم الخاص بي ، كان لا بد من تقديم حقول النص قبل تحديد جميع الكائنات الأخرى. أدى ذلك إلى قفزات صغيرة ولكن ملحوظة ، على سبيل المثال ، عند تحميل نافذة منبثقة.
لقد أجريت هنا ثلاثة تصحيحات - أولاً ، استبدلت أكبر قدر ممكن من النص بخطوط نقطية. يحتوي Flixel على دعم مدمج لتنسيقات الخطوط النقطية المختلفة ، بما في ذلك
AngelCode's BMFont ، مما يجعل من السهل العمل مع Unicode والأسلوب
وتقنين الأحرف ، ولكن API النقطية تختلف قليلاً عن API للنص العادي ، لذلك كان علي أن أكتب فئة غلاف صغيرة ل تبسيط الانتقال. (أعطيتها اسمًا مناسبًا
FlxUITextHack
).
أدى هذا إلى تحسين العمل قليلاً - حيث يتم عرض الخطوط النقطية بسرعة كبيرة - ولكن زاد التعقيد قليلاً: كان علي إعداد مجموعات أحرف منفصلة بشكل خاص وإضافة منطق التحويل اعتمادًا على اللغة ، بدلاً من مجرد إعداد مربع نص يقوم بكل العمل.
كان الإصلاح الثاني هو إنشاء كائن واجهة مستخدم جديد كان
عنصرًا نائبًا بسيطًا للنص ولكن له نفس الخصائص العامة للنص. دعوتها "منطقة النص" وأنشأت فئة جديدة لها في مكتبة واجهة المستخدم الخاصة بي بحيث يمكن لنظام واجهة المستخدم الخاص بي استخدام مناطق النص هذه بنفس طريقة حقول النص الحقيقية ، لكنه لا يقدم أي شيء حتى يحسب الحجم والموضع لكل شيء آخر. بعد ذلك ، عندما تم تحضير المشهد الخاص بي ، بدأت في إجراء استبدال مناطق النص هذه بحقول نصية حقيقية (أو حقول نصية لخطوط نقطية).
التصحيح الثالث يتعلق بالإدراك. إذا كان هناك توقف مؤقت بين المدخلات ورد الفعل حتى في غضون نصف ثانية ، فإن اللاعب يرى أن هذا هو الكبح. لذلك ، حاولت العثور على جميع المشاهد التي يوجد فيها تأخير في الإدخال حتى الانتقال التالي ، وأضفت إما طبقة شفافة بكلمة "تحميل ..." أو مجرد طبقة بدون نص. حسن هذا التصحيح البسيط إلى حد كبير
تصور استجابة اللعبة ، لأن شيئًا ما يحدث فورًا بعد أن يلمس اللاعب التحكم ، حتى لو استغرق الأمر بعض الوقت لعرض القائمة.
تقديم البرمجيات
تستخدم معظم القوائم مجموعة من برامج التحجيم والتركيب المكون من 9 شرائح. حدث هذا لأنه في إصدار الكمبيوتر الشخصي كان هناك واجهة مستخدم مستقلة عن الدقة يمكنها العمل مع نسبة عرض إلى ارتفاع 4: 3 و 16: 9 ، تم قياسها وفقًا لذلك. ولكن في PSVita ،
نعرف بالفعل الدقة ، أي أننا لا نحتاج إلى كل هذه الموارد الإضافية عالية الدقة وخوارزميات القياس في الوقت الفعلي. يمكننا ببساطة تقديم الموارد بدقة الدقة ووضعها على الشاشة.
أولاً ، دخلت في ترميز واجهة المستخدم لظروف Vita التي حولت اللعبة إلى استخدام مجموعة موازية من الموارد. ثم احتجت إلى إنشاء هذه الموارد المعدة لإذن واحد. تبين أن
مصحح HaxeFlixel مفيد جدًا
هنا - لقد أضفت البرنامج النصي إليه بحيث يقوم ببساطة بمسح ذاكرة التخزين المؤقت النقطية إلى القرص. ثم أنشأت تكوينًا خاصًا للبناء لنظام Windows يحاكي الإذن لـ Vita ، وفتح جميع قوائم اللعبة بدوره ، وبدّل إلى المصحح ، وأطلق أمر التصدير للإصدارات المتدرجة من الموارد كملفات PNG جاهزة. ثم أعدت تسميتها واستخدمتها كموارد لـ Vita.
عرض زر
واجه نظام واجهة المستخدم الخاص بي مشكلة حقيقية مع الأزرار - عندما تم إنشاؤها ، أعطت الأزرار مجموعة الموارد الافتراضية ، وبعد لحظة قاموا بتغيير حجم (وإعادة عرض) رمز تمهيد واجهة المستخدم ، وأحيانًا حتى في المرة
الثالثة ، قبل تحميل واجهة المستخدم بأكملها . لقد قمت بحل هذه المشكلة عن طريق إضافة الخيارات التي أخرت عرض الأزرار إلى المرحلة الأخيرة.
مسح نص اختياري
تم تحميل المجلة ببطء شديد. اعتقدت في البداية أن المشكلة تكمن في الحقول النصية ، ولكن لا. يمكن أن يحتوي نص المجلة على روابط لصفحات أخرى ، والتي تمت الإشارة إليها بأحرف خاصة مضمنة في النص الخام نفسه. تم قطع هذه الأحرف لاحقًا واستخدامها لحساب موقع الرابط.
اتضح. أنني قمت بمسح
كل حقل نصي للعثور على هذه الأحرف واستبدالها بروابط منسقة بشكل صحيح ، حتى دون التحقق أولاً مما إذا كان هناك أي حرف خاص في هذا الحقل النصي! الأسوأ ، وفقًا للتصميم ، تم استخدام الروابط
فقط في صفحة المحتوى ، لكنني راجعتها في كل مربع نص في كل صفحة.
تمكنت من الالتفاف على جميع عمليات التحقق هذه باستخدام الصيغة if من النموذج "هل يستخدم مربع النص هذا الروابط على الإطلاق". كان الجواب على هذا السؤال عادة لا. أخيرًا ، تبين أن الصفحة التي استغرق أطول وقت للتحميل هي صفحة الفهرس. نظرًا لأنها لا تتغير أبدًا في قائمة دفتر اليومية ، فلماذا لا نقوم بتخزينها مؤقتًا؟
6. التنميط الذاكرة
السرعة ليست فقط وحدة المعالجة المركزية. يمكن أن تكون الذاكرة أيضًا مشكلة ، خاصة على الأنظمة الأساسية الضعيفة مثل Vita. حتى عندما تمكنت من التخلص من آخر تسرب للذاكرة ، فقد لا تزال تواجه مشاكل في استخدام ذاكرة سن المنشار في بيئة جمع القمامة.ما هو استخدام الذاكرة سن المنشار؟ يعمل جامع القمامة على النحو التالي: تتراكم البيانات والكائنات التي لا تستخدمها بمرور الوقت ويتم مسحها بشكل دوري. ولكن ليس لديك تحكم واضح في وقت حدوث ذلك ، لذا فإن الرسم البياني لاستخدام الذاكرة يشبه المنشار:أخرج القمامة
نظرًا لأن التنظيف ليس فوريًا ، فإن إجمالي كمية ذاكرة الوصول العشوائي التي تستخدمها عادة ما تكون أكبر مما تحتاجه حقًا. ولكن إذا تجاوزت إجمالي حجم ذاكرة الوصول العشوائي للنظام ، فقد يحدث أحد أمرين - على جهاز الكمبيوتر ، ربما تستخدم ملف صفحة فقط ، أي تحويل جزء من مساحة القرص الثابت مؤقتًا إلى ذاكرة الوصول العشوائي الافتراضية. البديل في بيئات الذاكرة المحدودة (مثل وحدات التحكم) هو تعطيل التطبيق ، حتى لو لم يكن هناك ما يكفي من زوج من البايتات البائسة. وسيحدث هذا حتى إذا لم تستخدم هذه البايت وسيتم إجراء جمع القمامة فيها قريبًا!الشيء الجيد في Haxe هو أنها مفتوحة المصدر تمامًا ، أي أنك لست مؤمنًا في صندوق أسود لا يمكنك إصلاحه ، كما هو الحال مع Unity. وتوفر الواجهة الخلفية hxcpp إدارة شاملة لجمع القمامة مباشرة من واجهة برمجة التطبيقات!استخدمناها لمسح الذاكرة على الفور بعد مستوى كبير من أجل البقاء ضمن الحدود المحددة:cpp.vm.Gc.run(false); // (true/false - / )
لا يجب استخدامها بشكل لا إرادي إذا كنت لا تعرف ما تفعله ، ولكن من المناسب وجود مثل هذه الأدوات عند الحاجة إليها.7. حل خلال التصميم
كانت جميع تحسينات الأداء هذه أكثر من كافية لتحسين اللعبة للكمبيوتر الشخصي ، لكننا حاولنا أيضًا إصدار إصدار لـ PSVita ، وكان لدينا خطط طويلة المدى لـ Nintendo Switch ، لذلك كان علينا أن نخرج كل شيء من الكود.ولكن غالبًا ما تكون هناك "رؤية نفق" عندما تركز فقط على الاختراق التقني وتنسى أن التغيير البسيط للتصميم يمكن أن يحسن الوضع بشكل كبير .تسريع الآثار بسرعة عالية
في 16x ، تحدث العديد من التأثيرات بسرعة كبيرة بحيث لا يراها اللاعب حتى. لقد استخدمنا بالفعل حيلة واحدة - أصبح برق أزرا أسهل مع سرعة اللعبة ، وعدد الجسيمات لهجمات AOE أقل. لقد استكملنا هذه التقنية عن طريق تعطيل أرقام الأضرار عالية السرعة والحيل الأخرى المماثلة.لقد أدركنا أيضًا أنه في مرحلة ما ، قد تكون سرعة 16x أبطأ من سرعة 8x عندما يكون هناك الكثير من الأشياء على الشاشة ، لذلك عندما زاد عدد الأعداء إلى حد معين ، قمنا تلقائيًا بتقليل سرعة اللعبة إلى 8x أو 4x. من الناحية العملية ، من المحتمل أن يرى اللاعب هذا فقط في Endless Battle 2. وهذا يسمح للأداء السلس والعرض دون زيادة التحميل على وحدة المعالجة المركزية.استخدمنا أيضًا قيودًا خاصة بالمنصة. في Vita ، نتخطى تأثير البرق عندما تقوم Azra بتشغيل أو تسريع الشخصية ، واستخدام حيل أخرى مماثلة.إخفاء الجسم
وماذا عن الكومة الضخمة من الأعداء في الزاوية اليمنى السفلية من Endless Battle 2 - هناك مئات أو حتى الآلاف من الأعداء يرسمون واحدًا فوق الآخر. لماذا لا نتخطى عرض تلك التي لا نستطيع حتى رؤيتها؟هذه خدعة تصميم ماكرة تتطلب برمجة ماكرة ، لأننا نحتاج إلى خوارزمية ذكية تحدد الأشياء المخفية.يتم رسم معظم هذه الألعاب باستخدام خوارزمية الفنان - يتم حظر الكائنات السابقة في قائمة الرسم بواسطة كل شيء يأتي بعدها.من خلال عكس ترتيب عرض خوارزمية الفنان ، يمكنك إنشاء "خريطة غلاف" ومعرفة ما يجب إخفاؤه. لقد صنعت "قماش" مزيفًا مع 8 مستويات من "الظلام" (مجموعة ثنائية البايت ثنائية الأبعاد) بدقة أقل بكثير من ساحة المعركة الحقيقية. بدءًا من نهاية قائمة العرض ، نأخذ المربع المحيط لكل كائن ونرسمه على اللوحة ، ونزيد "قتامة" النقطة بمقدار 1 لكل "بكسل" يغطيها الصندوق المحيط منخفض الدقة. في نفس الوقت ، نقرأ متوسط "الظلمة" في المنطقة التي سنرسم فيها. في الواقع ، نتوقع عدد عمليات إعادة الرسم التي سيختبرها كل كائن بمكالمة سحب حقيقية.إذا كان العدد المتوقع لعمليات إعادة الرسم مرتفعًا بما فيه الكفاية ، فعندئذ أقوم بتمييز العدو بأنه "مدفون" ، مع عتبتين - مدفونة تمامًا ، غير مرئية تمامًا ، أو مدفونة جزئيًا ، أي أنه سيتم رسمها ، ولكن دون تقديم شريط صحي.(بالمناسبة ، هذه هي وظيفة التحقق من عمليات إعادة الرسم.)لكي يعمل هذا بشكل صحيح ، يجب عليك تكوين دقة خريطة الإخفاء بشكل صحيح. إذا كانت كبيرة جدًا ، فسيتعين علينا إجراء مجموعة إضافية من مكالمات السحب المبسطة ، إذا كانت صغيرة جدًا ، فسوف نخفي الأشياء بقوة شديدة ونحصل على أخطاء بصرية. إذا قمت بتحديد البطاقة بشكل صحيح ، فإن التأثير بالكاد يمكن ملاحظته ، ولكن زيادة السرعة ملحوظة للغاية - لا توجد طريقة لرسم شيء أسرع من عدم رسمها على الإطلاق !التحميل المسبق أفضل من الفرامل
في منتصف المعارك ، لاحظت وجود كبح متكرر ، والذي ، على يقين ، كان سببه توقف في جمع القمامة. ومع ذلك ، أظهر التنميط أن الأمر ليس كذلك. كشفت اختبارات أخرى أن هذا يحدث في بداية موجة تفرخ الأعداء ، وبعد ذلك اكتشفت أن هذا يحدث فقط عندما تكون موجة من الأعداء لم تكن موجودة من قبل. من الواضح أن بعض كودات تكوين العدو هي التي تسببت في المشكلة ، وبالطبع عند تحديد ملف التعريف ، تم العثور على وظيفة "ساخنة" في إعدادات الرسومات. بدأت العمل على إعداد تنزيل معقد متعدد الخيوط ، ولكن بعد ذلك أدركت أنه يمكنني فقط وضع جميع إجراءات تحميل رسومات العدو في التحميل المسبق للمعركة. بشكل منفصل ، كانت هذه تنزيلات صغيرة جدًا ، حتى على أبطأ المنصات تضيف أقل من ثانية إلى إجمالي وقت تحميل المعركة ، لكنها تجنبت الكبح الملحوظ جدًا أثناء اللعب.نحن نحتفظ بالأسهم لوقت لاحق
إذا كنت تعمل في بيئة محدودة الذاكرة ، فيمكنك استخدام الحيلة القديمة في صناعتنا - لتخصيص جزء كبير من الذاكرة بهذه الطريقة ، ثم نسيانها حتى نهاية المشروع. في نهاية المشروع ، بعد أن أهدرت ميزانية الذاكرة المتاحة بالكامل ، يمكنك حفظها بفضل هذه "البيضة العش".لقد وجدنا أنفسنا في مثل هذه الحالة - كنا بحاجة فقط إلى عشرة بايتات لإنقاذ تجميع PSVita ، لكن الجحيم - نسينا هذه الحيلة وبالتالي علقنا! كانت الخيارات المتبقية الوحيدة هي أسابيع من جراحة الشفرة اليائسة والمؤلمة!لكن انتظر لحظة! كان أحد التحسينات (غير الناجحة) التي أجريتها هو تحميل أكبر عدد ممكن من الموارد ودائمًاتخزينها في الذاكرة ، لأنني افترضت خطأً أن وقت التحميل الكبير كان ناتجًا عن قراءة الموارد أثناء تنفيذ البرنامج. اتضح أن هذا لم يكن كذلك ، لذلك يمكن إزالة جميع هذه المكالمات الإضافية تقريبًا للتحميل المسبق والتخزين الأبدي تمامًا ، ولا يزال لدي ذاكرة خالية!التخلص من الأشياء التي لا نستخدمها
أثناء العمل على بناء PSVita ، كنا واضحين بشكل خاص أن هناك مجموعة من الأشياء التي لا نحتاجها. نظرًا للدقة المنخفضة ، كان وضع الرسومات المصدر ووضع الرسومات عالية الدقة لا يمكن تمييزهما ، لذلك استخدمنا الرسومات الأصلية لجميع العفاريت. تمكنا أيضًا من تحسين وظيفة استبدال اللوحة بمساعدة تظليل بكسل خاص (استخدمنا في وقت سابق وظيفة عرض البرنامج).مثال آخر كان خريطة المعركة نفسها - على الكمبيوتر وأجهزة التحكم المنزلية ، قمنا بتجميع مجموعة من بطاقات البلاط فوق بعضها البعض لإنشاء خريطة متعددة الطبقات. ولكن نظرًا لأن الخريطة لا تتغير أبدًا ، في Vita ، يمكننا فقط خبز كل شيء في صورة واحدة منتهية بحيث يتم استدعاؤها في مكالمة سحب واحدة.بالإضافة إلى الموارد الإضافية ، كانت اللعبة تحتوي على العديد من المكالمات الإضافية ، على سبيل المثال ، المدافعون والأعداء الذين يرسلون إشارة تجديد في كل إطار ، حتى عندما لا يكون لديهم القدرة على التجدد . إذا كانت واجهة المستخدم مفتوحة لمثل هذا المخلوق ، فقد أعيد رسمه في كل إطار .هناك ستة أمثلة أخرى من الخوارزميات الصغيرة التي تحسب شيئًا ما داخل دالة "ساخنة" ، لكنها لا تُرجع النتائج في أي مكان. عادة ما كانت هذه نتائج إنشاء الهيكل في المراحل الأولى من التطوير ، لذلك قمنا فقط بقصها.NaNopocalypse
هذه القضية كانت مضحكة. أفاد المحلل أن حساب الزوايا يستغرق الكثير من الوقت. فيما يلي كود Haxe C ++ الذي تم إنشاؤه في ملف التعريف:هذه إحدى الدالات التي تأخذ قيمًا مثل -90
وتحول إلى 270
. في بعض الأحيان تحصل على قيم مثل -724
، والتي يتم تقليلها في بضع دورات 4
.لسبب ما ، تم تمرير قيمة لهذه الوظيفة -2147483648
.دعونا نقوم بالحسابات. إذا أضفنا في كل دورة 360 إلى -2147483648 ، فستستغرق حوالي 5،965،233 تكرارًا حتى تصبح أكبر من 0 وتكمل الدورة. بالمناسبة ، تم تنفيذ هذه الدورة مع كل تحديث (ليس في كل إطار - في كل تحديث !) - في كل مرة عندما تغير المقذوف (أو أي شيء آخر) زاويته.بالطبع ، كان خطئي ، لأنني أدخلت قيمة NaN
- قيمة خاصة تعني "ليس رقمًا" (وليس رقمًا) ، والتي تشير عادةً إلى خطأ حدث سابقًا في الرمز. إذا أحضرته إلى عدد صحيح دون التحقق أولاً ، فإن مثل هذه الأشياء الغريبة تحدث.كحل مؤقت ، أضفت شيكًاMath.isNan()
، التي تعيد ضبط الزاوية عند حدوث مثل هذا الحدث (نادرًا إلى حد ما ، ولكن لا مفر منه) في الوقت نفسه ، واصلت البحث عن السبب الجذري للخطأ ، ووجدته ، واختفى التأخير على الفور. اتضح أنه إذا لم تقم بإجراء 6 ملايين تكرار لا معنى له ، فيمكنك الحصول على زيادة كبيرة في السرعة!(تم إدراج إصلاح لهذا الخطأ في HaxeFlixel نفسه).لا تفوق نفسك
يعتمد كل من OpenFL و HaxeFlixel على التخزين المؤقت للموارد. هذا يعني أنه عند تحميل مورد ، في المرة التالية التي يتم فيها تلقي هذا المورد ، يتم أخذه من ذاكرة التخزين المؤقت ، ولا يتم إعادة تحميله من القرص. يمكن تجاوز هذا السلوك ، وفي بعض الأحيان يكون منطقيًا.ومع ذلك ، دخلت في أشياء غريبة بعيدة المنال: لقد قمت بتنزيل المورد ، وأخبرت النظام بشكل صريح بعدم تخزين النتائج مؤقتًا ، لأنني كنت متأكدًا تمامًا مما كنت أفعله ولا أريد "إهدار الذاكرة" على ذاكرة التخزين المؤقت. بعد سنوات ، جعلتني هذه المكالمات "الذكية" تحمّل نفس المورد مرارًا وتكرارًا ، مما يبطئ اللعبة ويضيع الذاكرة الثمينة ، التي "حفظتها" من خلال التخلي عن ذاكرة التخزين المؤقت.8. بالإضافة إلى ذلك ، قد لا يكون من المفيد القيام بمستويات مثل Endless Battle 2
نعم ، من الرائع أن ننفذ كل هذه الحيل الصغيرة لزيادة السرعة. بصراحة ، لم نلاحظ معظمها حتى بدأنا في نقل اللعبة إلى أنظمة أقل قوة ، عندما أصبحت المشكلات في بعض المستويات غير محتملة تمامًا. أنا سعيد لأننا تمكننا في النهاية من زيادة السرعة ، ولكن أعتقد أنه يجب أيضًا تجنب تصميم المستوى المرضي. وضعت Endless Battle 2 الكثير من الضغط على النظام ، خاصة مقارنة بجميع المستويات الأخرى للعبة .حتى بعد كل هذه التغييرات ، لا يزال إصدار PSVita غير قادر على التعامل مع تصميم Endless 2 الأصلي ، ولم أكن أرغب في المخاطرة بالسرعة في الطرازين الأساسيين XB1 و PS4 ، لذلك قمت بتغيير التوازن لإصدارات وحدة التحكم في Endless 2. لقد قمت بتقليل عدد الأعداء ، لكني قمت بزيادة خصائصها بحيث يكون للمستوى نفس الصعوبة تقريبًا. بالإضافة إلى ذلك ، في PSVita قمنا بتحديد عدد الموجات إلى مائة لتجنب خطر فشل الذاكرة ، لكننا لم نضيف قيودًا على PS4 و XB1. وبفضل هذا ، لا يزال تحقيق التحمل صعبًا بنفس القدر على جميع وحدات التحكم. في إصدار الكمبيوتر الشخصي ، ظل تصميم Endless Batlte 2 على حاله.كل هذا كان درسًا لنا ، سنأخذه في الاعتبار عند إنشاء Defender's Quest II - سنكون منتبهًا جدًا للمستويات دون حد أعلى لعدد الأعداء على الشاشة! بالطبع ، المهمات "اللامتناهية" جذابة للغاية لمحبي برج الدفاع ، لذلك لن أتخلص منها تمامًا ، ولكن ماذا عن المستويات بنقاط التفتيش التي يجب على اللاعب فيها تدمير كل شيء على الشاشة قبل الانتقال إلى الموجات التالية؟ لن يسمح لنا هذا فقط بتحديد عدد الأعداء على الشاشة ، ولكن أيضًا ندرك التوفير في منتصف المستوى دون إثارة ضجيج متسلسل لحالة الأشياء المجنونة في معركة شديدة - سيكون كافيًا بالنسبة لنا ببساطة حفظ إحداثيات المدافعين ، وتعزيز المستويات ، وما إلى ذلك.9. الأفكار في الختام
يعد أداء اللعبة موضوعًا معقدًا لأن اللاعبين غالبًا لا يفهمون ما هو ، ولا ينبغي أن نتوقع مثل هذا الفهم منهم. لكن آمل أن تكون هذه المقالة قد أوضحت لك قليلاً كيف يبدو كل شيء في الداخل ، وقد تعلمت المزيد حول كيفية إبطاء التصميم والمبادلات التقنية والقرارات الغبية ببساطة الألعاب.خلاصة القول هي أنه حتى في لعبة ذات تصميم جيد تم تطويره من قبل فريق موهوب ، يمكن العثور على أجزاء رمز "صدئة" صغيرة في كل مكان . ولكن من الناحية العملية ، فإن جزءًا صغيرًا منهم فقط يؤثر في الواقع على الأداء. القدرة على اكتشافها والقضاء عليها هي على حد سواء الفن والعلوم.أنا سعيد لأننا سنستفيد من كل هذه المزايا في تطوير لعبة Defender's Quest II. بصراحة ، إذا لم نقم بإنشاء منفذ لـ PSVita ، فربما لم أجرب حتى نصف هذه التحسينات. وحتى إذا لم تشترِ اللعبة لـ PSVita ، فيمكنك شكر وحدة التحكم الصغيرة هذه ، التي حسنت بشكل كبير من سرعة مهمة Defender.