
Untuk permainan saya dalam seni ASCII, saya menulis
perpustakaan bear_hug dengan antrian acara, koleksi widget, dukungan ECS, dan hal-hal kecil bermanfaat lainnya. Pada artikel ini kita akan melihat bagaimana menggunakannya untuk membuat game yang berfungsi minimal.
Penafian- Saya adalah satu-satunya pengembang perpustakaan, jadi saya bisa menjadi bias.
- bear_hug pada dasarnya adalah pembungkus di sekitar bearlibterminal , sehingga tidak akan ada operasi tingkat rendah dengan mesin terbang.
- Ada fungsi serupa di Clubsandwich , tapi saya tidak menggunakannya dan saya tidak bisa membandingkannya.
Di bawah kap bear_hug adalah
bearlibterminal , perpustakaan SDL untuk membuat jendela pseudo-konsol. Artinya, dalam TTY murni, seperti beberapa ncurses, itu tidak akan berfungsi. Tapi kemudian gambarnya sama di Linux, di Windows, dan tidak tergantung pada pengaturan terminal pengguna. Ini penting, terutama untuk game, karena ketika Anda mengubah font ASCII-art, Tuhan mungkin berubah menjadi apa:
Gambar yang sama dalam bentuk aslinya dan setelah salin-tempel ke program yang berbedaTentu saja, perpustakaan itu ditulis untuk proyek-proyek berskala relatif besar. Tetapi agar tidak terganggu oleh desain dan arsitektur game, dalam artikel ini kita akan membuat sesuatu yang sederhana. Proyek satu malam, di mana ada sesuatu untuk menunjukkan fungsi dasar perpustakaan. Yaitu - tiruan sederhana dari tank yang sama dengan Dandy (mereka juga Battle City). Akan ada tank pemain, tank musuh, dinding yang dapat dirusak, suara dan skor. Tetapi menu utama, level dan bonus yang dipilih tidak akan. Bukan karena tidak mungkin menambahkannya, tetapi karena proyek ini tidak lebih dari Halloworld.
Akan ada gameover, tetapi tidak akan ada kemenangan. Karena hidup adalah rasa sakit.Semua bahan yang digunakan dalam artikel ada
di github ; perpustakaan itu sendiri
juga di PyPI (di bawah lisensi MIT).
Pertama-tama, kita membutuhkan aset. Untuk menggambar seni ASCII, saya menggunakan
REXpaint dari Josh Ge (alias Kyzrati), pengembang bagel fiksi ilmiah
Cogmind . Editor gratis, meskipun bukan open source; versi resmi hanya untuk Windows, tetapi semuanya berfungsi dengan baik di bawah anggur. Antarmuka cukup jelas dan nyaman:

Kami menyimpan dalam format biner lokal .xp dan menyalin dari
/path/to/rexpaint/images
ke folder dengan game masa depan. Pada prinsipnya, memuat gambar dari file .txt juga didukung, tetapi jelas tidak mungkin untuk menyimpan warna masing-masing karakter dalam file teks. Ya, dan mengedit seni ASCII di notebook tidak nyaman bagi saya secara pribadi. Agar tidak melakukan hardcode koordinat dan ukuran setiap elemen, data ini disimpan dalam file JSON yang terpisah:
battlecity.json [ { "name": "player_r", "x": 1, "y": 2, "xsize": 6, "ysize": 6 }, { "name": "player_l", "x": 1, "y": 8, "xsize": 6, "ysize": 6 }, ... ]
Suara di bawah lisensi unduhan gratis dari Internet. Sejauh ini, hanya .wav yang didukung. Itu semua dengan aset, Anda dapat mulai mengkodekan. Pertama-tama, Anda perlu menginisialisasi terminal dan antrian acara.
Terminal adalah jendela sebenarnya dari game. Anda dapat menempatkan widget di atasnya, dan ia melempar
acara yang diperlukan. Sebagai kunci saat membuat terminal, Anda dapat menggunakan semua
opsi terminal bearlibterminal ; dalam hal ini, kami mengatur font, ukuran jendela (dalam karakter), judul jendela, dan metode input yang menarik bagi kami.
Adapun antrian acara, ia memiliki antarmuka yang sangat sederhana: dispatcher.add_event (event) menambahkan acara ke antrian, dan dispatcher.register_listener (listener, event_types) memungkinkan Anda untuk berlangganan. Penanda tangan (misalnya, widget atau komponen) harus memiliki panggilan balik on_event, yang menganggap suatu peristiwa sebagai argumen tunggal dan tidak mengembalikan apa pun atau mengembalikan acara atau rangkaian acara lainnya. Acara itu sendiri terdiri dari jenis dan nilai; jenis di sini bukan dalam arti str atau int, tetapi dalam arti "variasi", misalnya 'key_down' atau 'centang'. Antrean hanya menerima peristiwa dari tipe yang diketahui itu (bawaan atau dibuat oleh pengguna) dan mengirimkannya ke on_event semua orang yang berlangganan jenis ini. Itu tidak memeriksa nilai dengan cara apa pun, tetapi ada konvensi dalam perpustakaan tentang apa nilai yang valid untuk setiap jenis acara.
Pertama, kami mengantri beberapa pendengar. Ini adalah kelas dasar untuk objek yang dapat berlangganan acara, tetapi bukan widget atau komponen. Pada prinsipnya, tidak perlu untuk menggunakannya, selama penandatangan memiliki metode on_event.
Daftar lengkap tipe acara bawaan ada
di dokumentasi . Sangat mudah untuk melihat bahwa ada peristiwa untuk penciptaan dan penghancuran entitas, tetapi tidak untuk kerusakan. Karena kita akan memiliki objek yang tidak terlepas dari satu tembakan (dinding dan tangki pemain), kita akan membuatnya:
Kami setuju bahwa, sebagai nilai, acara ini akan memiliki tuple dari ID entitas yang menderita kerusakan, dan nilai kerusakan. LoggingListener hanyalah alat debugging yang mencetak semua acara yang diterima di mana pun mereka mengatakan, dalam hal ini di stderr. Dalam hal ini, saya ingin memastikan bahwa kerusakan lewat dengan benar, dan bahwa suara diminta selalu ketika seharusnya.
Dengan Pendengar untuk saat ini, Anda dapat menambahkan widget pertama. Kami memiliki lapangan bermain kelas ECSLayout ini. Layout seperti ini, yang dapat menempatkan widget pada entitas dan memindahkannya sebagai respons terhadap peristiwa ecs_move, dan pada saat yang sama mempertimbangkan tabrakan. Seperti kebanyakan widget, ia memiliki dua argumen yang diperlukan: daftar karakter bersarang (mungkin kosong - ruang atau Tidak ada) dan daftar warna untuk setiap karakter. Warna yang dinamai diterima sebagai warna, RGB dalam format `0xAARRGGBB` (atau` 0xARGB`, `0xRGB`,` 0xRRGGBB`) dan dalam format '#fff'. Ukuran kedua daftar harus cocok; jika tidak, pengecualian dilemparkan.
Karena sekarang kita memiliki objek untuk ditempatkan di dalam game, kita dapat mulai membuat entitas. Semua kode entitas dan komponen dipindahkan ke file terpisah. Yang paling sederhana adalah dinding bata yang bisa dirusak. Dia tahu bagaimana berada di tempat tertentu, menampilkan widgetnya, berfungsi sebagai objek tabrakan dan menerima kerusakan. Setelah cukup banyak kerusakan, dinding menghilang.
entitas.py def create_wall(dispatcher, atlas, entity_id, x, y):
Pertama-tama, objek entitas itu sendiri dibuat. Ini hanya berisi nama (yang harus unik) dan satu set komponen. Mereka dapat ditransfer sekaligus pada saat penciptaan, atau, seperti di sini, ditambahkan satu per satu. Kemudian semua komponen yang diperlukan dibuat. Sebagai widget, SwitchWidget digunakan, yang berisi beberapa gambar dengan ukuran yang sama dan dapat mengubahnya dengan perintah. Omong-omong, gambar dimuat dari atlas saat membuat widget. Dan, akhirnya, pengumuman penciptaan entitas dan perintah untuk menggambarnya pada koordinat yang diperlukan masuk ke antrian.
Dari komponen non-built-in, hanya ada kesehatan. Saya membuat komponen dasar "Komponen kesehatan" dan mewarisi "widget perubahan komponen Kesehatan" (untuk menunjukkan dinding utuh dan pada beberapa tahap kehancuran).
komponen 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):
Saat membuat komponen, kunci 'nama' dilewatkan ke super () .__ init__. Ketika komponen ditambahkan ke entitas, di bawah nama dari kunci ini akan ditambahkan ke __dict__ entitas dan dapat diakses melalui entity_object.health. Selain kenyamanan antarmuka, pendekatan ini bagus karena melarang penerbitan entitas dari beberapa komponen yang homogen. Dan fakta bahwa hardcoded di dalam komponen tidak memungkinkan Anda untuk memasukkan, misalnya, WidgetComponent ke dalam slot komponen kesehatan. Segera setelah pembuatan, komponen berlangganan ke kelas acara yang menarik untuknya, dalam hal ini ac_damage. Setelah menerima peristiwa semacam itu, metode on_event akan memeriksa apakah itu tentang pemiliknya selama satu jam. Jika demikian, ia akan mengurangi nilai yang diinginkan dari poin hit dan menarik panggilan balik untuk mengubah kesehatan, kelas dasar abstrak. Ada juga metode __repr__, yang digunakan untuk
serialisasi di JSON (misalnya, untuk menyimpan). Tidak perlu menambahkannya, tetapi semua komponen bawaan dan sebagian besar widget bawaan memilikinya.
Mewarisi dari komponen kesehatan yang mendasari VisualDamageHealthComponent mengesampingkan panggilan balik pada perubahan kesehatan:
kelas 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])
Sementara kesehatan di atas 0, ia meminta komponen yang bertanggung jawab atas widget untuk menggambar dinding dalam kondisi yang diinginkan. Di sini, panggilan yang dijelaskan di atas digunakan melalui atribut objek entitas. Setelah hitpoint selesai, komponen yang bertanggung jawab atas penghancuran entitas yang benar dan semua komponen akan dipanggil dengan cara yang sama.
Untuk entitas lain, semuanya serupa, hanya komponen yang berbeda. Tank ditambahkan dengan pengontrol (input untuk pemain, AI untuk lawan) dan widget yang berputar, untuk kerang - komponen tabrakan yang menyebabkan kerusakan pada yang mereka pukul. Saya tidak akan menganalisis masing-masing, karena besar dan agak sepele; hanya melihat collider proyektil. Ini memiliki metode collided_into, dipanggil ketika entitas host menabrak sesuatu:
collider komponen peluru 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()
Untuk memastikan bahwa sangat mungkin untuk mendapatkan korban (yang mungkin salah untuk, misalnya, elemen latar belakang), proyektil tersebut menggunakan EntityTracker (). Ini adalah singleton yang melacak semua entitas yang dibuat dan dihancurkan; melaluinya, Anda bisa mendapatkan objek entitas dengan nama dan melakukan sesuatu dengan komponennya. Dalam hal ini, diverifikasi bahwa entitas.collision (korban collision handler) ada sama sekali.
Sekarang di file utama gim, kami cukup memanggil semua fungsi yang diperlukan untuk membuat entitas:
Counter point dan hit point bukan entitas dan tidak di medan perang. Oleh karena itu, mereka tidak ditambahkan ke ECSLayout, tetapi langsung ke terminal di sebelah kanan peta. Widget yang relevan mewarisi dari Label (widget keluaran teks) dan memiliki metode on_event untuk mengetahui minat mereka. Tidak seperti Layout, terminal tidak secara otomatis memperbarui widget setiap tick, jadi setelah mengubah teks, widget memberitahunya untuk melakukan ini:
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:
Generator musuh dan objek yang bertanggung jawab untuk output "GAME OVER" tidak ditampilkan sama sekali, sehingga mereka mewarisi dari Listener. Prinsipnya sama: objek mendengarkan antrian, menunggu saat yang tepat, dan kemudian membuat entitas atau widget.
Sekarang kami telah menciptakan semua yang kami butuhkan, dan kami dapat memulai permainan.
Widget ditambahkan ke layar hanya setelah dimulai. Entitas dapat ditambahkan ke peta sebelum - acara penciptaan (di mana seluruh entitas disimpan, termasuk widget) hanya diakumulasikan dalam antrian dan diselesaikan pada centang pertama. Tetapi terminal dapat menambahkan widget hanya setelah jendela berhasil dibuat untuk itu.
Pada titik ini, kami memiliki prototipe yang berfungsi, Anda dapat
mengeluarkan di Early Access selama dua puluh dolar menambahkan fitur dan memoles gameplay. Tapi ini sudah di luar ruang lingkup halloworld, dan karenanya artikel. Saya hanya akan menambahkan bahwa build independent dari sistem python dapat dibangun menggunakan
pyinstaller .