
بالنسبة
لألعابي في فن ASCII ، كتبت
مكتبة bear_hug مع قائمة انتظار للأحداث ، ومجموعة من الأدوات المصغرة ، ودعم ECS ، وأشياء صغيرة مفيدة أخرى. في هذه المقالة سوف نرى كيفية استخدامها لصنع الحد الأدنى من لعبة العمل.
تنصل- أنا المطور الوحيد للمكتبة ، لذلك يمكن أن أكون متحيزًا.
- bear_hug عبارة عن غلاف حول بيرتيلترمينال في الأساس ، لذلك لن تكون هناك عمليات منخفضة المستوى نسبيًا باستخدام الحروف الرسومية.
- توجد وظائف مماثلة في clubandwich ، لكنني لم أستخدمها ولا يمكنني مقارنتها.
تحت غطاء محرك الأقراص من bear_hug يوجد
bearlibterminal ، مكتبة SDL لإنشاء نافذة وحدة التحكم الزائفة. وهذا هو ، في TTY النقي ، مثل بعض ncurses ، فإنه لن ينجح. لكن الصورة هي نفسها في نظام Linux ، على نظام Windows ، ولا تعتمد على إعدادات محطة المستخدم. هذا مهم ، خاصة بالنسبة للألعاب ، لأنه عند تغيير الخط ASCII-art ، قد يتحول الله إلى ما يلي:
نفس الرسم في شكله الأصلي وبعد نسخ لصق لبرامج مختلفةبالطبع ، كانت المكتبة مكتوبة لمشاريع واسعة النطاق نسبيا. ولكن حتى لا يصرف انتباهك تصميم اللعبة وهندستها ، سننشئ في هذا المقال شيئًا بسيطًا. مشروع مدته ليلة واحدة ، يوجد فيه شيء لإظهار الوظائف الأساسية للمكتبة. وهي - استنساخ مبسط لتلك الدبابات نفسها مع داندي (وهي أيضا مدينة المعركة). سيكون هناك دبابة للاعب ، ودبابات العدو ، والجدران القابلة للتدمير ، والصوت وسجل. لكن القائمة الرئيسية والمستويات والمكافآت المحددة لن تكون. ليس لأنه سيكون من المستحيل إضافتهم ، لكن لأن هذا المشروع ليس أكثر من عالم هالوور.
سيكون هناك gameover ، ولكن لن يكون هناك نصر. لأن الحياة ألم.جميع المواد المستخدمة في المقالة
على جيثب . المكتبة نفسها موجودة
أيضًا على PyPI (بموجب ترخيص MIT).
بادئ ذي بدء ، نحن بحاجة إلى الأصول. من أجل رسم فن ASCII ، أستخدم
REXpaint من Josh Ge (المعروف أيضًا باسم كيزراتي) ، مطوِّر
البيغل الخيال العلمي
Cogmind . المحرر مجاني ، رغم أنه ليس مفتوح المصدر. النسخة الرسمية مخصصة لنظام التشغيل Windows فقط ، ولكن كل شيء يعمل بشكل جيد تحت النبيذ. الواجهة واضحة ومريحة للغاية:

نقوم بحفظ .xp بالتنسيق الثنائي المحلي ونسخه من
/path/to/rexpaint/images
إلى المجلد باستخدام اللعبة المستقبلية. من حيث المبدأ ، يتم دعم تحميل الصور من ملفات .txt أيضًا ، لكن من الواضح أنه من المستحيل حفظ ألوان الأحرف الفردية في ملف نصي. نعم ، وتحرير فن ASCII في دفتر ملاحظات ليس مناسبًا لي شخصيًا. لكي لا يتم تشفير إحداثيات وحجم كل عنصر ، يتم تخزين هذه البيانات في ملف JSON منفصل:
battlecity.json [ { "name": "player_r", "x": 1, "y": 2, "xsize": 6, "ysize": 6 }, { "name": "player_l", "x": 1, "y": 8, "xsize": 6, "ysize": 6 }, ... ]
يبدو تحت التراخيص المجانية تحميل من الإنترنت. حتى الآن ، يتم دعم .wav فقط. هذا كل شيء مع الأصول ، يمكنك البدء في الترميز. بادئ ذي بدء ، تحتاج إلى تهيئة المحطة وقائمة الانتظار الحدث.
المحطة هي النافذة الفعلية للعبة. يمكنك وضع عناصر واجهة مستخدم عليها ، ويلقي
أحداث الإدخال حسب الضرورة. كمفاتيح عند إنشاء محطة ، يمكنك استخدام جميع
خيارات المحطة الطرفية bearlibterminal . في هذه الحالة ، قمنا بتعيين الخط وحجم النافذة (بالأحرف) وعنوان النافذة وطرق الإدخال التي تهمنا.
بالنسبة لقائمة انتظار الأحداث ، فإن لديها واجهة بسيطة للغاية: يضيف dispatcher.add_event (الحدث) الحدث إلى قائمة الانتظار ، ويتيح لك dispatcher.register_listener (مستمع ، event_types) الاشتراك فيه. يجب أن يكون لدى المُوقِّع (على سبيل المثال ، عنصر واجهة مستخدم أو عنصر) رد اتصال on_event ، والذي يأخذ حدثًا كوسيطة واحدة ولا يُرجع أي شيء أو يُرجع حدثًا آخر أو مجموعة من الأحداث. الحدث نفسه يتكون من النوع والقيمة. النوع هنا ليس بمعنى str أو int ، ولكن بمعنى "التنوع" ، على سبيل المثال ، "key_down" أو "tick". لا تقبل قائمة الانتظار سوى الأحداث من الأنواع المعروفة لها (المضمنة أو التي تم إنشاؤها من قبل المستخدم) وترسلها إلى on_event جميع أولئك المشتركين في هذا النوع. لا يتحقق من القيم بأي طريقة ، ولكن هناك اصطلاحات داخل المكتبة حول القيمة الصالحة لكل نوع من الأحداث.
أولاً ، نحن في طابور اثنين من المستمعين. هذه هي الفئة الأساسية للكائنات التي يمكنها الاشتراك في الأحداث ، ولكنها ليست عناصر واجهة مستخدم أو مكونات. من حيث المبدأ ، ليس من الضروري استخدامها ، طالما أن الموقع كان لديه طريقة on_event.
قائمة كاملة من أنواع الأحداث المضمنة
في الوثائق . من السهل أن نرى أن هناك أحداث لإنشاء وتدمير الكيانات ، ولكن ليس للأضرار. نظرًا لأن لدينا أشياء لا تنفصل عن طلقة واحدة (جدران اللاعب وخزانته) ، فإننا سننشئها:
نوافق على أنه ، كقيمة ، سيكون لهذا الحدث تلميح من معرّف الكيان الذي تعرض للتلف ، وقيمة الضرر. LoggingListener هو مجرد أداة تصحيح لطباعة جميع الأحداث المستلمة أينما يقولون ، في هذه الحالة في stderr. في هذه الحالة ، أردت التأكد من أن التلف يمر بشكل صحيح ، وأن الصوت مطلوب دائمًا عندما ينبغي.
مع المستمعين الآن ، يمكنك إضافة القطعة الأولى. لدينا هذا الملعب فئة ECSLayout. هذا تخطيط ، يمكنه وضع عناصر واجهة تعامل مستخدم على الكيانات ونقلها استجابة لأحداث ecs_move ، وفي الوقت نفسه ينظر في التصادمات. مثل معظم التطبيقات المصغرة ، يحتوي على وسيطين مطلوبين: قائمة متداخلة من الأحرف (ربما تكون خالية - مساحة أو لا شيء) وقائمة متداخلة من الألوان لكل حرف. يتم قبول الألوان المسماة كألوان ، RGB بالتنسيق `0xAARRGGBB` (أو` 0xARGB` ، `0xRGB` ،` 0xRRGGBB`) وفي التنسيق '#fff'. يجب أن تتطابق أحجام كلتا القائمتين ؛ خلاف ذلك ، يتم طرح استثناء.
نظرًا لأن لدينا الآن ما نضعه في اللعبة ، يمكننا البدء في إنشاء كيانات. يتم نقل كل كود الكيانات والمكونات إلى ملف منفصل. أبسطها هو جدار من الطوب المدمر. إنها تعرف كيف تكون في مكان معين ، وتعرض عنصر واجهة المستخدم الخاص بها ، وتعمل ككائن للتصادم وتتلقى الضرر. بعد أضرار كافية ، يختفي الجدار.
entities.py def create_wall(dispatcher, atlas, entity_id, x, y):
بادئ ذي بدء ، يتم إنشاء كائن الكيان نفسه. يحتوي فقط على اسم (والذي يجب أن يكون فريدًا) ومجموعة من المكونات. يمكن إما نقلها دفعة واحدة عند الإنشاء ، أو ، كما هو الحال هنا ، يتم إضافتها مرة واحدة في كل مرة. ثم يتم إنشاء جميع المكونات اللازمة. كعنصر واجهة مستخدم ، يتم استخدام SwitchWidget ، والذي يحتوي على العديد من الرسومات من نفس الحجم ويمكن تغييرها حسب الأمر. بالمناسبة ، يتم تحميل الرسومات من الأطلس عند إنشاء عنصر واجهة مستخدم. وأخيراً ، فإن الإعلان عن إنشاء الكيان وترتيب رسمه على الإحداثيات الضرورية ، يدخل في قائمة الانتظار.
من المكونات غير المضمنة ، هناك صحة فقط. قمت بإنشاء الفئة الصحية "مكون الصحة" ورثت منه "عنصر تغيير عنصر الصحة" (لإظهار الجدار على حالته وفي عدة مراحل من التدمير).
فئة HealthComponent class HealthComponent(Component): def __init__(self, *args, hitpoints=3, **kwargs): super().__init__(*args, name='health', **kwargs) self.dispatcher.register_listener(self, 'ac_damage') self._hitpoints = hitpoints def on_event(self, event): if event.event_type == 'ac_damage' and event.event_value[0] == self.owner.id: self.hitpoints -= event.event_value[1] @property def hitpoints(self): return self._hitpoints @hitpoints.setter def hitpoints(self, value): if not isinstance(value, int): raise BearECSException( f'Attempting to set hitpoints of {self.owner.id} to non-integer {value}') self._hitpoints = value if self._hitpoints < 0: self._hitpoints = 0 self.process_hitpoint_update() def process_hitpoint_update(self): """ Should be overridden by child classes. """ raise NotImplementedError('HP update processing should be overridden') def __repr__(self):
عند إنشاء مكون ، يتم تمرير مفتاح "الاسم" إلى super () .__ init__. عندما تتم إضافة مكون إلى كيان ، تحت الاسم من هذا المفتاح ، ستتم إضافته إلى __dict__ للكيان ويمكن الوصول إليه من خلال entity_object.health. بالإضافة إلى راحة الواجهة ، يعد هذا النهج جيدًا لأنه يحظر إصدار كيانات من عدة مكونات متجانسة. وحقيقة أنه تم ترميزها داخل المكون لا تسمح لك بإدخال خطأ ، على سبيل المثال ، WidgetComponent في فتحة المكون الصحة. فور الإنشاء ، يشترك المكون في فئات الأحداث التي تهمه ، في هذه الحالة ac_damage. بعد تلقي مثل هذا الحدث ، ستتحقق طريقة on_event مما إذا كان الأمر يتعلق بمالكها لمدة ساعة. إذا كان الأمر كذلك ، فسيقوم بطرح القيمة المطلوبة من نقاط الدخول وسحب رد الاتصال لتغيير الحالة الصحية ، فالفئة الأساسية مجردة. هناك أيضًا طريقة __repr__ ، والتي تُستخدم
للتسلسل في JSON (على سبيل المثال ، للحفظ). ليس من الضروري إضافته ، لكن جميع المكونات المدمجة ومعظم الأدوات المصغرة المدمجة بها.
الوراثة من مكون الصحة الأساسي في VisualDamageHealthComponent يتجاوز رد الاتصال على التغيير الصحي:
فئة VisualDamageHealthComponent class VisualDamageHealthComponent(HealthComponent): """ . HP=0 """ def __init__(self, *args, widgets_dict={}, **kwargs): super().__init__(*args, **kwargs) self.widgets_dict = OrderedDict() for x in sorted(widgets_dict.keys()): self.widgets_dict[int(x)] = widgets_dict[x] def process_hitpoint_update(self): if self.hitpoints == 0 and hasattr(self.owner, 'destructor'): self.owner.destructor.destroy() for x in self.widgets_dict: if self.hitpoints >= x: self.owner.widget.switch_to_image(self.widgets_dict[x])
بينما تكون الصحة أعلى من 0 ، يسأل المكون المسؤول عن عنصر واجهة المستخدم لرسم الجدار في الحالة المطلوبة. هنا ، يتم استخدام المكالمة الموضحة أعلاه من خلال سمة كائن الكيان. بمجرد انتهاء نقاط الوصول ، سيتم استدعاء المكون المسؤول عن التدمير الصحيح للكيان وجميع المكونات بالطريقة نفسها.
بالنسبة للكيانات الأخرى ، كل شيء مشابه ، فقط مجموعة المكونات مختلفة. يتم إضافة الدبابات مع وحدات التحكم (إدخال اللاعب ، AI للمعارضين) والحاجيات الدورانية ، للأصداف - مكون تصادم يتسبب في أضرار لمن أصابهم. لن أقوم بتحليل كل منهم ، لأنه ضخم وباهت إلى حد ما ؛ انظر فقط إلى مصادم القذيفة. يحتوي على طريقة collided_into ، تسمى عند اصطدام الكيان المضيف بشيء:
مصادم مكون الرصاصة def collided_into(self, entity): if not entity: self.owner.destructor.destroy() elif hasattr(EntityTracker().entities[entity], 'collision'): self.dispatcher.add_event(BearEvent(event_type='ac_damage', event_value=( entity, self.damage))) self.owner.destructor.destroy()
للتأكد من أنه من الممكن حقًا الحصول على ضحية (والتي قد تكون خاطئة ، على سبيل المثال ، لعناصر الخلفية) ، يستخدم المقذوف EntityTracker (). هذا هو المفرد الذي يتتبع جميع الكيانات التي تم إنشاؤها وتدميرها ؛ من خلاله ، يمكنك الحصول على كائن كيان بالاسم والقيام بشيء مع مكوناته. في هذه الحالة ، يتم التحقق من وجود الكيان. الحل (معالج تصادم الضحية) على الإطلاق.
الآن في الملف الرئيسي للعبة ، ندعو ببساطة جميع الوظائف اللازمة لإنشاء كيانات:
عدادات نقطة ونقطة الوصول ليست كيانات وليست في ساحة المعركة. لذلك ، لا يتم إضافتها إلى ECSLayout ، ولكن مباشرةً إلى المحطة على يمين الخريطة. ترث عناصر واجهة المستخدم ذات الصلة من Label (أداة إخراج النص) ولديها طريقة on_event لمعرفة ما يهمهم. بخلاف Layout ، لا يقوم الجهاز تلقائيًا بتحديث عناصر واجهة المستخدم لكل علامة ، لذلك بعد تغيير النص ، تخبره الأدوات المصغّرة بالقيام بذلك:
listeners.py class ScoreLabel(Label): """ """ def __init__(self, *args, **kwargs): super().__init__(text='Score:\n0') self.score = 0 def on_event(self, event): if event.event_type == 'ecs_destroy' and 'enemy' in event.event_value and 'bullet' not in event.event_value:
لا يتم عرض مولد العدو والكائن المسؤول عن إخراج "GAME OVER" على الإطلاق ، لذلك يرثان من المستمع. المبدأ هو نفسه: تستمع الكائنات إلى قائمة الانتظار ، وتنتظر اللحظة المناسبة ، ثم تنشئ كيانًا أو عنصر واجهة مستخدم.
الآن أنشأنا كل ما نحتاجه ، ويمكننا بدء اللعبة.
تتم إضافة الحاجيات إلى الشاشة فقط بعد أن تبدأ. يمكن إضافة الكيانات إلى الخريطة من قبل - يتم تجميع أحداث الإنشاء (التي يتم فيها تخزين الكيان بأكمله ، بما في ذلك عنصر واجهة المستخدم) في قائمة الانتظار وحلها عند العلامة الأولى. لكن يمكن أن تضيف المحطة الطرفية عناصر واجهة المستخدم فقط بعد إنشاء نافذة لها بنجاح.
في هذه المرحلة ، لدينا نموذج أولي عملي ، يمكنك
إصدار ميزة "
الوصول المبكر" لـ 20 دولارات لإضافة ميزات وتلميع طريقة اللعب. ولكن هذا بالفعل خارج نطاق halloworld ، وبالتالي المقالة. سأضيف فقط أنه يمكن بناء
البنية المستقلة عن نظام بيثون باستخدام برنامج
pyinstaller .