لعبة NES الحديثة مكتوبة بلغة مثل Lisp

What Remains هي لعبة مغامرة سردية لوحدة ألعاب الفيديو NES ذات 8 بت ، والتي تم إصدارها في مارس 2019 كذاكرة قراءة ROM مجانية يتم تشغيلها في المحاكي. تم إنشاؤه بواسطة فريق صغير من Iodine Dynamics لمدة عامين بشكل متقطع. في الوقت الحالي ، أصبحت اللعبة في مرحلة التنفيذ في الأجهزة: نقوم بإنشاء مجموعة محدودة من الخراطيش من الأجزاء المعاد تدويرها.


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


في هذه المقالة سأتحدث عن بعض التفاصيل الفنية للأجزاء الفردية من محرك اللعبة. آمل أن يجدها مطورو البرامج الآخرون مفيدة أو غريبة على الأقل.

معدات متنوعه


قبل بدء تشغيل الرمز ، سوف أخبركم قليلاً عن مواصفات المعدات التي نعمل بها. NES هي وحدة ألعاب تم إصدارها عام 1983 (اليابان ، 1985 - أمريكا). يوجد بداخلها وحدة المعالجة المركزية (CPU) المكونة من 8 بتات 6502 [1] بتردد 1.79 ميجاهرتز. نظرًا لأن وحدة التحكم تنتج 60 إطارًا في الثانية ، فهناك حوالي 30 ألف دورة من وحدات المعالجة المركزية لكل إطار ، وهذا صغير جدًا لحساب كل ما يحدث في دورة اللعب الرئيسية.

بالإضافة إلى ذلك ، تحتوي وحدة التحكم على ما مجموعه 2048 بايت من ذاكرة الوصول العشوائي (والتي يمكن توسيعها إلى 10240 بايت باستخدام ذاكرة وصول عشوائي إضافية ، وهو ما لم نفعله). يمكنه أيضًا معالجة 32 كيلو بايت من ROM في وقت واحد ، والتي يمكن توسيعها عن طريق تبديل البنوك (ما تبقى من 512 كيلو بايت من ROM). يعد تبديل البنوك موضوعًا معقدًا [2] لا يتعامل معه المبرمجون الحديثون. باختصار ، تكون مساحة العنوان المتاحة لوحدة المعالجة المركزية أقل من البيانات الموجودة في ذاكرة الوصول العشوائي (ROM) ، وهذا يعني أنه عند التبديل يدويًا ، تظل كتل الذاكرة بالكامل غير قابلة للوصول. هل تريد استدعاء بعض الوظائف؟ لن يتم استبدال البنك عن طريق استدعاء أمر التحويل البنكي. إذا لم يتم ذلك ، فعندما يتم استدعاء الوظيفة ، فسوف يتعطل البرنامج.

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

CO2


ولكن في حالتنا ، لم يكن الأمر كذلك. بدلاً من ذلك ، كان سيتم تطوير ترادف مع اللعبة لغته الخاصة. Co2 هي لغة تشبه Lisp تم بناؤها على نظام Racket وتم تجميعها في أداة تجميع 6502. في البداية ، تم إنشاء اللغة بواسطة Dave Griffiths لإنشاء العرض التوضيحي لـ Remains ، وقررت استخدامه في المشروع بأكمله.

يسمح لك Co2 بكتابة رمز المجمّع المدمج إذا لزم الأمر ، ولكن لديه أيضًا إمكانات عالية المستوى تعمل على تبسيط بعض المهام. ينفذ المتغيرات المحلية الفعالة على حد سواء من حيث استهلاك ذاكرة الوصول العشوائي وسرعة الوصول [2]. يحتوي على نظام ماكرو بسيط للغاية يسمح لك بالكتابة في نفس الوقت وشفرة فعالة [3]. والأهم من ذلك أنه بسبب التماثل المخروطي لـ Lisp ، فإنه يبسط بشكل كبير عرض البيانات مباشرة في المصدر.

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

فيما يلي نموذج لشفرة ثاني أكسيد الكربون يرسم الخلفية لمشهد تم تحميله قبل تعتيمه:

; Render the nametable for the scene at the camera position (defsub (create-initial-world) (camera-assign-cursor) (set! camera-cursor (+ camera-cursor 60)) (let ((preserve-camera-v)) (set! preserve-camera-v camera-v) (set! camera-v 0) (loop i 0 60 (set! delta-v #xff) (update-world-graphics) (when render-nt-span-has (set! render-nt-span-has #f) (apply-render-nt-span-buffer)) (when render-attr-span-has (set! render-attr-span-has #f) (apply-render-attr-span-buffer))) (set! camera-v preserve-camera-v)) (camera-assign-cursor)) 

نظام الكيان



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

التنفيذ نموذجي تمامًا: تحتوي مجموعة كبيرة على قائمة بالكيانات في المشهد ، ويحتوي كل سجل على بيانات متعلقة بالكيان مع تسمية كتابة. تتجاوز وظيفة التحديث في دورة اللعب الرئيسية جميع الكيانات وتنفذ السلوك المقابل وفقًا لنوعها.

 ; Called once per frame, to update each entity (defsub (update-entities) (when (not entity-npc-num) (return)) (loop k 0 entity-npc-num (let ((type)) (set! type (peek entity-npc-data (+ k entity-field-type))) (when (not (eq? type #xff)) (update-single-entity k type))))) 

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

 (bytes npc-diner-a 172 108 prop-palette 1 prop-hflip prop-picture picture-smoker-c prop-animation simple-cycle-animation prop-anim-limit 6 prop-head hair-flip-head-tile 2 prop-dont-turn-around prop-dialog-a (2 progress-stage-4 on-my-third my-dietician) prop-dialog-a (2 progress-stage-3 have-you-tried-the-pasta the-real-deal) prop-dialog-a (2 progress-diner-is-clean omg-this-cherry-pie its-like-a-party) prop-dialog-a (2 progress-stage-1 cant-taste-food puff-poof) prop-dialog-b (1 progress-stage-4 tea-party-is-not) prop-dialog-b (1 progress-stage-3 newspaper-owned-by-dnycorp) prop-dialog-b (1 progress-stage-2 they-paid-a-pr-guy) prop-dialog-b (1 progress-stage-1 it-seems-difficult) prop-customize (progress-stage-2 stop-smoking) 0) 

في هذا الرمز ، تعيّن prop-palette الألوان المستخدم للكيان ، ويحدد prop-anim-limit عدد إطارات الرسوم المتحركة ، ويمنع prop-dont-turn-around الدوران NPC من الدوران إذا كان اللاعب يحاول التحدث إليه من الجانب الآخر. كما أنه يحدد بضعة إشارات من الشروط التي تغير سلوك الكيان في عملية تمرير اللعبة من قبل اللاعب.

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

بوابات



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

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

 ; City A (bytes city-a-scene #x50 #x68 look-up portal-customize (progress-stage-5 remove-self) ; to Diner diner-scene #xc0 #xa0 look-down portal-width #x20 0) 

كما أنه سهل عملية إضافة "نقاط النقل عن بعد" ، والتي كانت تستخدم غالبًا في التدوينات السينمائية ، حيث كان على اللاعب الانتقال إلى آخر في المشهد ، اعتمادًا على ما كان يحدث في المؤامرة.

إليك ما يشبه النقل الفضائي في بداية المستوى 3:

 ; Jenny's home (bytes jenny-home-scene #x60 #xc0 look-up portal-teleport-only jenny-back-at-home-teleport 0) 

انتبه إلى قيمة البحث ، والتي تشير إلى اتجاه "المدخل" إلى هذه البوابة. عند مغادرة البوابة ، سيبحث اللاعب في الاتجاه الآخر ؛ في هذه الحالة ، جيني (الشخصية الرئيسية للعبة) في المنزل ، بينما تنظر لأسفل.

كتلة النص


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


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

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

بعد ذلك ، يتم رسم كتلة نصية مع الحفاظ على الحالة سطراً سطراً وتستخدم الحسابات من المرحلة الأولى حتى لا تعقد الشفرة.

 ; Called once per frame as the text box is being rendered (defsub (text-box-update) (when (or (eq? tb-text-mode 0) (eq? tb-text-mode #xff)) (return #f)) (cond [(in-range tb-text-mode 1 4) (if (not is-paused) ; Draw text box for dialog. (text-box-draw-opening (- tb-text-mode 1)) ; Draw text box for pause. (text-box-draw-pausing (- tb-text-mode 1))) (inc tb-text-mode)] [(eq? tb-text-mode 4) ; Remove sprites in the way. (remove-sprites-in-the-way) (inc tb-text-mode)] [(eq? tb-text-mode 5) (if (not is-paused) ; Display dialog text. (when (not (crawl-text-update)) (inc tb-text-mode) (inc tb-text-mode)) ; Display paused text. (do (create-pause-message) (inc tb-text-mode)))] [(eq? tb-text-mode 6) ; This state is only used when paused. Nothing happens, and the caller ; has to invoke `text-box-try-exiting-pause` to continue. #t] [(and (>= tb-text-mode 7) (< tb-text-mode 10)) ; Erase text box. (if (is-scene-outside scene-id) (text-box-draw-closing (- tb-text-mode 7)) (text-box-draw-restoring (- tb-text-mode 7))) (inc tb-text-mode)] [(eq? tb-text-mode 10) ; Reset state to return to game. (set! text-displaying #f) (set! tb-text-mode 0)]) (return #t)) -way) ; Called once per frame as the text box is being rendered (defsub (text-box-update) (when (or (eq? tb-text-mode 0) (eq? tb-text-mode #xff)) (return #f)) (cond [(in-range tb-text-mode 1 4) (if (not is-paused) ; Draw text box for dialog. (text-box-draw-opening (- tb-text-mode 1)) ; Draw text box for pause. (text-box-draw-pausing (- tb-text-mode 1))) (inc tb-text-mode)] [(eq? tb-text-mode 4) ; Remove sprites in the way. (remove-sprites-in-the-way) (inc tb-text-mode)] [(eq? tb-text-mode 5) (if (not is-paused) ; Display dialog text. (when (not (crawl-text-update)) (inc tb-text-mode) (inc tb-text-mode)) ; Display paused text. (do (create-pause-message) (inc tb-text-mode)))] [(eq? tb-text-mode 6) ; This state is only used when paused. Nothing happens, and the caller ; has to invoke `text-box-try-exiting-pause` to continue. #t] [(and (>= tb-text-mode 7) (< tb-text-mode 10)) ; Erase text box. (if (is-scene-outside scene-id) (text-box-draw-closing (- tb-text-mode 7)) (text-box-draw-restoring (- tb-text-mode 7))) (inc tb-text-mode)] [(eq? tb-text-mode 10) ; Reset state to return to game. (set! text-displaying #f) (set! tb-text-mode 0)]) (return #t)) 

إذا اعتدت على نمط Lisp ، فسيتم قراءة الرمز بشكل ملائم.

العفريت z طبقات


في النهاية ، سأتحدث عن تفاصيل صغيرة لا تؤثر بشكل خاص على طريقة اللعب ، لكنني أضيفت لمسة لطيفة أفتخر بها. NES له مكونان رسوميان فقط: جدول الأسماء (nametable) ، والذي يستخدم للخلفيات الثابتة والمتحيزة مع الشبكة ، والعفاريت - كائنات بحجم 8 × 8 بكسل ، والتي يمكن وضعها في أماكن عشوائية. عادة ما يتم إنشاء عناصر مثل شخصية اللاعب و NPCs كعفاريت إذا كانت يجب أن تكون أعلى رسومات جدول الأسماء.

ومع ذلك ، توفر معدات NES أيضًا القدرة على تحديد جزء من العفاريت التي يمكن وضعها بالكامل تحت جدول الاسم. هذا يتيح لك جهد لتحقيق تأثير 3D بارد.


يعمل على النحو التالي: اللوحة المستخدمة للمشهد الحالي تتعامل مع اللون في الموضع 0 بطريقة خاصة: إنه لون الخلفية العام. يتم رسم جدول الاسم فوقه ، ويتم رسم العفاريت بطبقة z بين طبقتين أخريين.

هذه هي لوحة هذا المشهد:


لذلك ، يتم استخدام اللون الرمادي الداكن في الزاوية اليسرى للغاية كخلفية خلفية عامة.

يعمل تأثير الطبقات كما يلي:


في معظم الألعاب الأخرى ، ينتهي كل هذا ، ومع ذلك ، فإن ما تبقى قد اتخذ خطوة أخرى إلى الأمام. لا تضع اللعبة جيني تمامًا أمام أو تحت رسومات جدول الأسماء - يتم تقسيم شخصيتها بينهما حسب الحاجة. كما ترون ، يبلغ حجم العفاريت 8 × 8 وحدات ، وتتكون رسومات الحرف بالكامل من عدة عفاريت (من 3 إلى 6 ، اعتمادًا على إطار الرسوم المتحركة). يمكن لكل عفريت تعيين طبقة z الخاصة بها ، أي أن بعض العفاريت ستكون أمام جدول الاسم ، والبعض الآخر خلفها.

فيما يلي مثال لهذا التأثير في العمل:


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


باستخدام مختلف الأساليب البحثية ، يتم دمجها لإنشاء "نقطة مرجعية" وقناع بت من أربعة بتات. أربعة أرباع بالنسبة للنقطة المرجعية تتوافق مع أربعة بتات: 0 يعني أن اللاعب يجب أن يكون أمام جدول الأسماء ، 1 - الذي يقف خلفه.


عند وضع العفاريت الفردية لتقديم المشغل ، تتم مقارنة موضعها بنقطة مرجعية لتحديد الطبقة العازلة لهذه العفريتة المعينة. بعضها في الطبقة الأمامية ، والبعض الآخر في الظهر.


استنتاج


تحدثت لفترة وجيزة عن الجوانب المختلفة لأعمالنا الداخلية في لعبة الرجعية الحديثة. هناك أكثر إثارة للاهتمام في قاعدة الشفرة ، لكني حددت جزءًا مهمًا مما يجعل اللعبة تعمل.

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

أتمنى أن تستمتعوا بالمقال!



الملاحظات


[1] بالمعنى الدقيق للكلمة ، تم تثبيت نوع من وحدة المعالجة المركزية 6502 يسمى Ricoh 2A03 في NES.

[2] في الواقع ، أقنعني هذا المشروع أن تبديل البنوك / إدارة ROM هو القيد الرئيسي لأي مشروع NES يتجاوز حجم معين.

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

[4] تمت إضافة وحدات ماكرو في مراحل متأخرة من التطور ، وبصراحة ، لم نتمكن من الاستفادة منها.

[5] يمكنك قراءة المزيد حول رسومات NES في سلسلة مقالاتي . تنتج تعارضات الألوان عن السمات الموضحة في الجزء الأول.

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


All Articles