
تحية. لقد حدث منذ ثلاث سنوات متتالية أنني صنعت لعبة كهدية للعام الجديد لبعض الناس. في عام 2018 ، كان منهاج مع عناصر اللغز ، والتي كتبت حول محور في عام 2019 - شبكة RTS لاثنين من اللاعبين ، والتي لم أكتب أي شيء. وأخيراً ، في 2020 - قصة قصيرة بصرية ، والتي سيتم مناقشتها لاحقًا ، تم إنشاؤها في وقت محدود جدًا.
في هذا المقال:
- تصميم وتنفيذ محرك للقصص القصيرة البصرية ،
- لعبة مؤامرة غير خطية في 8 ساعات ،
- إزالة منطق اللعبة في البرامج النصية في لغتهم الأم.
المهتمة؟ ثم مرحبا بكم في القط.
تحذير: يوجد الكثير من النصوص والصور ~ 3.5 ميجابايت
المحتويات:
0. مبرر لتطوير المحرك.
- اختيار منصة.
- بنية المحرك وتنفيذه:
2.1. بيان المشكلة.
2.2. العمارة والتنفيذ. - لغة النص:
3.1. اللغة.
3.2. مترجم. - تطوير اللعبة:
4.1. تاريخ وتطوير منطق اللعبة.
4.2. الرسومات. - الاحصائيات والنتائج.
ملاحظة: إذا كنت غير مهتم بالتفاصيل التقنية لسبب ما ، فيمكنك تخطي الخطوة 4 "تطوير اللعبة" مباشرةً ، ومع ذلك فإنك ستتخطى الجزء الأكبر من المحتوى
0. مبرر لتطوير المحرك
بالطبع ، هناك عدد كبير من المحركات الجاهزة للقصص القصيرة البصرية ، والتي ، بلا شك ، أفضل من الحل الموصوف أدناه. ومع ذلك ، بغض النظر عن نوع المبرمج الذي كنت عليه ، إذا لم أكن قد كتبت أخرى. لذلك ، دعونا نتظاهر بأن تطوره كان له ما يبرره.
في الواقع ، كان الاختيار صغيراً: إما Java أو C ++. دون التفكير مرتين ، قررت تنفيذ خطتي في جافا ، لأنه للتطوير السريع ، يوفر كل الاحتمالات (أي الإدارة التلقائية للذاكرة وبساطة أكبر مقارنةً بـ C ++ ، التي تخفي الكثير من التفاصيل ذات المستوى المنخفض ، ونتيجة لذلك ، فهي تركز بدرجة أقل على اللغة نفسها وتفكر فقط في منطق الأعمال) ، ويوفر أيضًا دعمًا للنوافذ والرسومات والصوت خارج الصندوق.
تم اختيار Swing لتطبيق الواجهة الرسومية ، حيث أنني استخدمت Java 13 ، حيث لم تعد JavaFX جزءًا من المكتبة ، وكانت إضافة عشرات ميغابايت من OpenJFX تبعًا لذلك كسولة للغاية. ربما لم يكن هذا هو الحل الأفضل ، لكن مع ذلك.
السؤال الذي يطرح نفسه ربما: ما هو نوع محرك اللعبة ، ولكن بدون تسريع الأجهزة؟ تكمن الإجابة في ضيق الوقت للتعامل مع OpenGL ، فضلاً عن عدم وجود معنى مطلق له: FPS ليس مهمًا للرواية المرئية (في أي حال ، مع وجود قدر كبير من الرسوم المتحركة والرسومات كما في هذه الحالة).
2. هيكل المحرك وتنفيذه
2.1 بيان المشكلة
من أجل تحديد كيفية القيام بشيء ما ، عليك أن تقرر السبب. هذا أنا عن بيان المشكلة ، لأنه البنية ليست عالمية ، ولكن المحرك "الخاص بمجال محدد" ، بحكم التعريف ، يعتمد بشكل مباشر على اللعبة المقصودة.
من خلال محرك عالمي ، أفهم المحرك الذي يدعم مفاهيم منخفضة المستوى نسبيًا ، مثل "كائن اللعبة" ، "المشهد" ، "المكون". تقرر جعله ليس محركًا عالميًا ، لأن هذا من شأنه أن يقلل بشكل كبير من وقت التطوير.
كما هو مخطط له ، يجب أن تتكون اللعبة من الأجزاء التالية:

أي أن هناك خلفية لكل مشهد ، والنص الرئيسي ، بالإضافة إلى حقل نص لإدخال المستخدم (رُوِّيت الرواية المرئية بدقة بإدخال المستخدم التعسفي ، وليس خيارًا من الخيارات المقترحة ، كما هو الحال في كثير من الأحيان. المقرر). يوضح الرسم البياني أيضًا أنه يمكن أن يكون هناك العديد من المشاهد في اللعبة ، ونتيجة لذلك ، يمكن إجراء انتقالات بينها.
ملاحظة: حسب المشهد أعني الجزء المنطقي للعبة. يمكن أن يكون المعيار للمشهد هو نفس الخلفية طوال هذا الجزء بالذات.
ومن بين متطلبات المحرك أيضًا القدرة على تشغيل الرسائل الصوتية وعرض الرسائل (مع وظيفة إدخال المستخدم الاختيارية).
ربما كانت أهم رغبة هي كتابة منطق اللعبة ليس في Java ، ولكن بلغة تعريفية بسيطة.
كانت هناك أيضًا رغبة في إدراك إمكانية وجود رسوم متحركة إجرائية ، أي الحركة الأولية للصور ، بحيث يكون من الممكن على مستوى Java تحديد الوظيفة التي يتم بها النظر في سرعة الحركة الحالية (على سبيل المثال ، أن يكون الرسم البياني للسرعة مباشرًا أو مخططًا جيبيًا أو أي شيء آخر).
كما هو مخطط له ، كان كل تفاعل المستخدم يتم من خلال نظام الحوارات. في هذه الحالة ، لا يعتبر الحوار بالضرورة حوارًا مع NPC أو شيئًا مشابهًا ، ولكنه عمومًا رد فعل على أي إدخال مستخدم تم تسجيل المعالج المقابل له. غير واضح سوف تصبح أكثر وضوحا قريبا.
2.2. العمارة والتنفيذ
في ضوء كل ما سبق ، يمكنك تقسيم المحرك إلى ثلاثة أجزاء كبيرة نسبيًا تتوافق مع حزم java نفسها:
display
- يحتوي على كل ما يتعلق بإخراج أي معلومات للمستخدم (الرسم والنص والصوت) ، وكذلك تلقي المدخلات منه. نوع (عرض) ، إذا تحدثنا عن MVC / MVP / إلخ.- أداة
initializer
- تحتوي على فئات يتم فيها تهيئة المحرك وتشغيله. sl
- يحتوي على أدوات للعمل مع لغة البرمجة النصية (المشار إليها فيما يلي - SL).
في هذه الفقرة سأنظر في أول جزأين. سأبدأ مع الثانية.
فئة مهيئ طريقتين رئيسيتين: initialize()
run()
. في البداية ، يأتي التحكم إلى فئة المشغِّل ، حيث يُطلق على initialize()
. بعد المكالمة ، يقوم المُهيِّر بتحليل المعلمات التي تم تمريرها إلى البرنامج (المسار إلى الدليل باستخدام المهام واسم البحث المطلوب تشغيله) ، ويقوم بتحميل بيان المهمة المحددة (حوله لاحقًا بقليل) ، وتهيئة العرض ، والتحقق مما إذا كان إصدار اللغة (SL) المطلوب بواسطة البحث مدعومًا بواسطة البيانات مترجم أخيرًا ، يقوم بتشغيل مؤشر ترابط منفصل لوحدة تحكم المطور.
بعد ذلك مباشرة ، إذا سارت الأمور على نحو سلس ، يقوم المشغل باستدعاء طريقة run()
التي تعمل على بدء التحميل الفعلي للمسعى. أولاً ، هناك جميع النصوص المتعلقة بالبحث الذي تم تنزيله (حول بنية ملف السعي - أدناه) ، ويتم إطعامهم للمحلل ، وترد نتيجة للمترجم الفوري. بعد ذلك ، يتم بدء تهيئة كل المشاهد ويكمل المُهيئ تنفيذ دفقه ، في النهاية يعلق معالج مفتاح Enter على الشاشة. وهكذا ، عندما يضغط المستخدم على Enter ، يتم تحميل المشهد الأول ، لكن المزيد عن ذلك لاحقًا.
هيكل ملف البحث هو كما يلي:

يوجد مجلد منفصل للبحث ، في جذره البيان ، بالإضافة إلى ثلاثة مجلدات إضافية: audio
- للصوت ، graphics
- الجزء المرئي scenes
- للبرامج النصية التي تصف المشاهد.
أود أن أصف بإيجاز البيان. يحتوي على الحقول التالية:
sl_version_req
- إصدار SL ضروري لبدء البحث ،init_scene
- اسم المشهد الذي يبدأ منه البحث ،quest_name
- اسم بحث جميل يظهر في عنوان النافذة ،resolution
- دقة الشاشة التي يهدف البحث إليها (بضع كلمات حول هذا لاحقًا) ،font_size
- حجم الخط لجميع النصوص ،font_name
هو اسم الخط لجميع النصوص.
تجدر الإشارة إلى أنه أثناء تهيئة الشاشة ، من بين أشياء أخرى ، تم إجراء حساب دقة العرض: بمعنى أنه تم اتخاذ القرار المطلوب من البيان واضغط في المساحة المتاحة للإطار بحيث:
- ظلت نسبة الارتفاع كما هي في القرار من البيان ،
- تم احتلال كل المساحة المتاحة سواء في العرض أو الارتفاع.
بفضل هذا ، يمكن لمطوري البحث التأكد من أن صوره ، على سبيل المثال 16: 9 ، سيتم عرضها على أي شاشة بهذه النسبة.
أيضًا ، عند تهيئة الشاشة ، يكون المؤشر مخفيًا ، نظرًا لعدم مشاركته في طريقة اللعب.
باختصار حول وحدة تحكم المطور. تم تطويره للأسباب التالية:
- لتصحيح.
- إذا حدث خطأ ما أثناء اللعبة ، يمكن إصلاحه من خلال وحدة تحكم المطور.
لقد نفذت بضعة أوامر فقط ، وهي: إخراج واصفات من نوع معين وحالتها ، وإخراج مؤشرات ترابط العمل ، وإعادة تشغيل الشاشة ، وأهم الأوامر - exec
، التي سمحت بتنفيذ أي كود SL في المشهد الحالي.
يؤدي هذا إلى إنهاء وصف المُهيئ والأشياء ذات الصلة ، ويمكننا متابعة وصف الشاشة.
هيكلها النهائي على النحو التالي:

من بيان المشكلة ، يمكننا أن نستنتج أن كل ما يجب القيام به هو رسم الصور ورسم النص وتشغيل الصوت.
كيف يتم رسم النص / الصورة عادة في محركات عالمية وخارجها؟ هناك طريقة update()
الكتابة update()
، والتي تسمى كل علامة / خطوة / إطار / تجسيد / إطار / وما إلى ذلك والتي هناك استدعاء لطريقة نوع drawText()
/ drawImage()
- وهذا يضمن ظهور النص / الصورة في هذا الإطار. ومع ذلك ، بمجرد توقف استدعاء هذه الأساليب ، يتوقف عرض الأشياء المقابلة.
في حالتي ، فقد تقرر أن تفعل مختلفة قليلا. نظرًا للروايات المرئية ، فإن النصوص والصور دائمة نسبيًا ، وهي أيضًا كل شيء يراه المستخدم تقريبًا (أي أنها مهمة بما فيه الكفاية) ، وقد تم تصنيعها ككائنات لعبة - أي أشياء تحتاج فقط إلى تفرخها ولن تختفي حتى تسألهم. بالإضافة إلى ذلك ، تبسيط هذا الحل التنفيذ.
يسمى الكائن (من وجهة نظر OOP) الذي يصف النص / الصورة واصفًا. بمعنى أنه بالنسبة لمستخدم محرك API ، هناك فقط واصفات يمكن إضافتها إلى حالة العرض وإزالتها منه. وبالتالي ، يوجد في الإصدار الأخير من الشاشة الواصفات التالية (تتوافق مع الفئات التي تحمل نفس الاسم):
تحتوي الشاشة أيضًا على حقول لمستقبل الإدخال الحالي (واصف الإدخال) وحقل يشير إلى واصف النص الذي لديه التركيز الآن والذي سيتم تمرير نصه ضمن الإجراءات المقابلة من جانب المستخدم.
تبدو دورة اللعبة مثل هذا:
- معالجة الصوت - استدعاء أسلوب
update()
على واصفات الصوت ، والتي تتحقق من الحالة الحالية للصوت ، وتحرر الذاكرة (إذا لزم الأمر) ، وتقوم بأعمال فنية أخرى. - معالجة ضغطات المفاتيح - نقل الأحرف التي تم إدخالها إلى واصف لتلقي المدخلات ومعالجة ضغطات المفاتيح لمفاتيح التمرير (لأعلى ولأسفل) و Backspace.
- تجهيز الرسوم المتحركة.
- مسح الخلفية في المخزن المؤقت
BufferedImage
(كان BufferedImage
بمثابة المخزن المؤقت). - رسم الصور.
- تقديم النص.
- رسم الحقول للمدخلات.
- إخراج المخزن المؤقت إلى الشاشة.
- التعامل مع
PostWorkDescriptor
. - يعمل البعض على استبدال حالات العرض ، والتي سأناقشها لاحقًا (في القسم الخاص بمترجم SL).
- أوقف التدفق للوقت المحسوب ديناميكيًا بحيث تساوي FPS القيمة المحددة (30 افتراضيًا).
ملاحظة: ربما يكون السؤال الذي يطرح نفسه ، "لماذا يتم تقديم حقول الإدخال إذا تم إنشاء واصفات نصية مناسبة لهم والتي سيتم تقديمها في وقت مبكر؟" في الواقع ، لا يحدث العرض في الفقرة 7 - تتم مزامنة معلمات InputDescriptor
فقط مع معلمات InputDescriptor
- مثل رؤية الشاشة InputDescriptor
وحجمها وغيرها. تم ذلك ، كما هو موضح أعلاه ، لسبب أن المستخدم لا يتحكم مباشرة في واصف الإدخال المناظر باستخدام واصف نص وعمومًا لا يعرف أي شيء عنه.
تجدر الإشارة إلى أن حجم العناصر وموضعها على الشاشة ليسوا بالبكسل ، ولكن بالأحجام النسبية - الأرقام من 0 إلى 1 (الرسم البياني أدناه). وهذا يعني أن العرض بالكامل للعرض هو 1 ، والارتفاع كله هو 1 (وهم ليسوا متساوين ، وقد نسيت عدة مرات ثم ندمت عليه لاحقًا). سيكون من المفيد أيضًا جعل (0،0) هو المركز ، ويجب أن يكون العرض / الارتفاع مساويا لاثنين ، لكن لسبب ما نسيت / لم أفكر فيه. ومع ذلك ، حتى الخيار الذي يبلغ عرضه / ارتفاعه يساوي 1 يبسط حياة مطور البحث.

بضع كلمات عن النظام لتحرير الذاكرة.
كان لكل واصف طريقة setDoFree(boolean)
، والتي كان على المستخدم الاتصال بها إذا أراد تدمير واصف معين. حدث تجميع البيانات المهملة للواصفات من نوع ما مباشرةً بعد معالجة جميع الواصفات من هذا النوع. أيضًا ، تم حذف الصوت الذي تم تشغيله مرة واحدة تلقائيًا بعد انتهاء التشغيل. بالضبط نفس الرسوم المتحركة غير الحلقية.
وبالتالي ، في الوقت الحالي ، يمكنك رسم أي شيء تريده ، لكن هذه ليست الصورة أعلاه ، حيث توجد فقط الخلفية والنص الرئيسي وحقل الإدخال. وهنا يأتي المجمع على الشاشة ، والذي يتوافق مع فئة DefaultDisplayToolkit
.
عندما تتم تهيئته ، فإنه يضيف فقط واصفات للخلفية والنص ، وما إلى ذلك إلى الشاشة ، ويعرف أيضًا كيفية عرض الرسائل باستخدام الرمز الاختياري وحقل الإدخال والاتصال.
ثم ظهر خطأ صغير ، يتطلب التصحيح الكامل إعادة نصف نظام العرض: إذا نظرت إلى ترتيب العرض في حلقة اللعبة ، يمكنك أن ترى أن الصور يتم رسمها أولاً ثم النص فقط. في الوقت نفسه ، عندما تعرض مجموعة الأدوات الصورة ، فإنها تضعها في منتصف الشاشة من حيث العرض والارتفاع . وإذا كان هناك الكثير من النص في الرسالة ، فينبغي أن يتداخل جزئيًا مع النص الرئيسي للمشهد. ومع ذلك ، نظرًا لأن خلفية الرسالة هي صورة (سوداء تمامًا ولكن مع ذلك) ، ويتم رسم الصور قبل النص ، يتم تثبيت النص على آخر (لقطة شاشة أدناه). تم حل المشكلة جزئيًا عن طريق التمركز العمودي ليس على الشاشة ، ولكن في المنطقة أعلى النص الرئيسي. يتضمن الحل الكامل إدخال معلمة عمق وإعادة تقديم العارضين من كلمة "بالكامل".
ربما هذا هو حول العرض ، وأخيرا ، كل شيء. يمكنك الانتقال إلى اللغة ، حيث توجد واجهة برمجة التطبيقات (API) بالكامل للعمل بها في الحزمة sl
.
3. لغة البرمجة
ملاحظة: إذا قرأها٪ USERNAME٪ المحترمين هنا ، فسيكون حسنًا ، وسأطلب منه ألا يتوقف عن فعل ذلك: الآن سيكون الأمر أكثر إثارة للاهتمام من ذي قبل.
3.1. لغة
في البداية ، أردت أن أبني لغة تعريفية يكون من الضروري فيها فقط الإشارة إلى جميع المعلمات اللازمة للمشهد ، وهذا كل شيء. سيأخذ المحرك كل المنطق. ومع ذلك ، في النهاية ، توصلت إلى اللغة الإجرائية ، حتى مع وجود عناصر OOP (بالكاد يمكن تمييزها) ، وكان هذا حلاً جيدًا ، لأنه ، بالمقارنة مع الإصدار التعريفي ، أعطى الفرصة لمزيد من المرونة في منطق اللعبة.
تم التفكير في بناء جملة اللغة بحيث يكون بسيطًا قدر الإمكان في التحليل ، وهو منطقي ، بالنظر إلى مقدار الوقت المتاح.
لذلك ، يتم تخزين رمز في ملفات نصية مع ملحق SSF ؛ يحتوي كل ملف على وصف لمشهد واحد أو أكثر ؛ كل مشهد يحتوي على صفر أو أكثر من الإجراءات ؛ يحتوي كل إجراء على صفر أو أكثر من العوامل.
شرح قليلا على الشروط. الإجراء هو مجرد إجراء دون إمكانية تمرير الحجج (لا يمنع بأي حال تطوير اللعبة). يبدو أن المشغل لا يعني تمامًا ما تعنيه هذه الكلمة باللغات العادية (+ ، - ، / ، *) ، ولكن النموذج هو نفسه: المشغل هو إجمالي اسمها وجميع وسائطها.
ربما تكون حريصًا على رؤية الكود المصدري لـ SL في النهاية ، وهنا:
scene dungeon { action init { load_image "background" "dungeon/background.png" load_image "key" "dungeon/key.png" load_audio "background" "dungeon/background.wav" load_audio "got_key" "dungeon/got_key.wav" } action first_come { play "background" loop set_background "background" set_text "some text" add_dialog "(||(|) (||-))" "dial_look_around" dial_look_around on } //some comment action dial_look_around { play "got_key" once show "some text 2" "key" none tag "key" switch_dialog "dial_look_around" off } }
الآن يصبح من الواضح ما هو المشغل. يُلاحظ أيضًا أن كل إجراء عبارة عن كتلة من البيانات (يمكن أن يكون البيان عبارة عن كتلة من البيانات) ، بالإضافة إلى حقيقة أن التعليقات ذات السطر الواحد مدعومة (لم يكن من المنطقي إدخال تعليقات متعددة الأسطر ، إضافة إلى ذلك ، لم أستخدم أي سطر مفرد).
من أجل التبسيط ، لم يتم إدخال مفهوم مثل "المتغير" في اللغة ؛ نتيجة لذلك ، كل القيم المستخدمة في الكود هي حرفية. اعتمادًا على النوع ، يتم تمييز القيم الحرفية التالية:
بضع كلمات عن تحليل اللغة. هناك عدة مستويات من "تحميل" الكود (الرسم البياني أدناه):
- الرمز المميز هو فئة معيارية لتقسيم شفرة المصدر إلى رموز (الحد الأدنى من الوحدات الدلالية للغة). يرتبط كل نوع من الرموز برقم - نوعه. لماذا وحدات؟ لأن تلك الأجزاء من الرمز المميز التي تتحقق مما إذا كان أي جزء من التعليمات البرمجية المصدر هو رمز مميز من نوع معين يتم عزلها عن الرمز المميز وتنزيلها من الخارج (من الفقرة الثانية).
- الوظيفة الإضافية tokenizer هي فئة تحدد مظهر كل نوع من الرموز المميزة في SL؛ في المستوى السفلي يستخدم الرمز المميز. أيضا هنا هو فحص الرموز الفضائية وإغفال التعليقات سطر واحد. الإخراج يعطي تيار نظيف من الرموز ، والذي يستخدم في ...
- ... محلل (وهو أيضا وحدات) ، والذي ينتج شجرة بناء جملة مجردة في الإخراج. وحدات - لأنه في حد ذاته ، يمكنه فقط تحليل المشاهد والإجراءات ، لكنه لا يعرف كيفية تحليل العوامل. لذلك ، يتم تحميل الوحدات النمطية فيه (في الواقع ، يقوم هو نفسه بتحميلها في المُنشئ ، وهي ليست جيدة جدًا) ، والتي يمكنها تحليل كل من مشغليها.

الآن ، باختصار عن المشغلين ، بحيث تظهر فكرة عن وظيفة اللغة. في البداية ، كان هناك 11 مشغلًا ، ثم في عملية التفكير من خلال اللعبة ، تم دمج بعضهم في واحد ، وبعضهم تغير ، وأضيف 9 آخرون. فيما يلي جدول الملخصات:
عوامل تشغيل للعمل مع العدادات - متغيرات عدد صحيح خاصة بالمشهد.
كان هناك أيضًا فكرة عن تقديم return
(تم إضافة الوظيفة المقابلة في المستوى الأساسي للمترجم الفوري) ، لكنني نسيت ، ولم تكن مفيدة.
, , : show_motion
(, , 0.01) duration
.
, (lookup) ( ): ///, load_audio
/ load_image
/ counter_set
/ add_dialog
. , , , , — . . , . , : " scene_coast.dialog_1
" — dialog_1
scene_coast
.
SL-, . , , , — . : (-, ), , lookup
', , , . , goto
lookup
', .
- — - , , n
( ) . , , n
. , .
. :
add_dialog "regexp" "dialog_name" callback on/off
, . , : , , , ( ).
, , ( ) ( ) , ( ). : , , , "" "".

, ( , )
, "":
(||((|||) ( )?(||)?)|(||)( )?| )
***
, : — , , — .
, :
3.2.
: , — "" ( ). .
SL , - . :
init
— , ( , , , ).first_come
— , . , , .- , :
come
— , ( ).
: init
first_come
— , .
. : , , init
-. , ( ) .
, n
, first_come
- ( - - ). . , : , , first_come
come
, come
( ). : , , , .
(, "", " ", " " . .). , , - - . , ( ), .
(, , ). : ? , , . provideState
, ; , .
, , , , ( , ), (, , , ).
4.
. 2019- 2018-, , , .
4.1.
, , , — . , . ( ), , - , 9 (), - ( , ( , , ) .
, : , , , . , , .
, 25% (5) , : , ; ( animate
), ( call_extern
).
, - ( ), (, , — , "You won").

4.2.
, :

, , - - " ". :
- (4x2.23''), .
- : , , — .
- ////etc.


5.
( 11 ) 30 40 . 9 4 55 . ( ) 7 41 . — ~4-6 ( 45 ).
: "Darkyen's Time Tracker" JetBrains ( ).
: 2 , — . 45 8 .
: 4777, ( ) — 637.
: cloc
.
30 . ( ) : — ~8 , — ~24 , ( ) — ~8 . .
— 232 ( - , WAV).
WAV?javax.sound.sampled.AudioSystem
, WAV AU , WAV.
28 ( 3 ). — 17 /.
- : , . , , " ", " ". (, ), ( ""/"" - ).
?— , . : . . , , "" : NPC, , (, — ..).
, : , .
— . , : , , , . . , , , , , .
. ( ), :
, , .
GitHub .
(assets) "Releases" "v1.0" .