
Salam Kebetulan selama tiga tahun berturut-turut saya menjadikan permainan sebagai hadiah untuk Tahun Baru bagi orang-orang tertentu. Pada tahun 2018, itu adalah platformer dengan elemen teka-teki, yang saya tulis di hub Pada 2019 - RTS jaringan untuk dua pemain, tentang yang saya tidak menulis apa pun. Dan akhirnya, pada tahun 2020 - sebuah cerita pendek visual, yang akan dibahas nanti, dibuat dalam waktu yang sangat terbatas.
Dalam artikel ini:
- desain dan implementasi mesin untuk cerita pendek visual,
- permainan dengan plot non-linear dalam 8 jam,
- penghapusan logika game dalam skrip dalam bahasa mereka sendiri.
Menarik? Kemudian selamat datang di kucing.
Perhatian: ada banyak teks dan ~ 3.5mb gambar
Konten:
0. Justifikasi untuk pengembangan mesin.
- Pilihan platform.
- Arsitektur mesin dan implementasinya:
2.1. Pernyataan masalah.
2.2. Arsitektur dan implementasi. - Bahasa skrip:
3.1. Bahasa
3.2. Penerjemah - Pengembangan game:
4.1. Sejarah dan perkembangan logika game.
4.2. Grafik - Statistik dan hasil.
Catatan: Jika karena alasan tertentu Anda tidak tertarik pada detail teknis, Anda dapat langsung beralih ke langkah 4 βPengembangan Gameβ, namun Anda akan melewatkan sebagian besar konten
0. Justifikasi untuk pengembangan mesin
Tentu saja, ada sejumlah besar mesin siap pakai untuk cerita pendek visual, yang, tidak diragukan lagi, lebih baik daripada solusi yang dijelaskan di bawah ini. Namun, tidak peduli apa pun programmer saya, jika saya tidak menulis yang lain. Karena itu, mari kita berpura-pura bahwa perkembangannya dibenarkan.
Faktanya, pilihannya kecil: Java atau C ++. Tanpa berpikir dua kali, saya memutuskan untuk mengimplementasikan rencana saya di Jawa, karena untuk pengembangan cepat, ia menyediakan semua kemungkinan (yaitu, manajemen memori otomatis dan kesederhanaan yang lebih besar dibandingkan dengan C ++, yang menyembunyikan banyak detail tingkat rendah dan, sebagai hasilnya, memungkinkan penekanan yang kurang pada bahasa itu sendiri dan hanya memikirkan logika bisnis), dan juga menyediakan dukungan untuk windows, grafik, dan audio di luar kotak.
Swing dipilih untuk mengimplementasikan antarmuka grafis, karena saya menggunakan Java 13, di mana JavaFX tidak lagi menjadi bagian dari perpustakaan, dan menambahkan puluhan megabita OpenJFX tergantung pada itu terlalu malas. Mungkin ini bukan solusi terbaik, tapi tetap saja.
Mungkin muncul pertanyaan: mesin gim macam apa itu, tetapi tanpa akselerasi perangkat keras? Jawabannya terletak pada kurangnya waktu untuk berurusan dengan OpenGL, dan juga tanpa makna mutlaknya: FPS tidak penting untuk novel visual (dalam hal apa pun, dengan animasi dan grafik sebanyak dalam hal ini).
2. Arsitektur mesin dan implementasinya
2.1 pernyataan masalah
Untuk memutuskan bagaimana melakukan sesuatu, Anda perlu memutuskan mengapa. Ini saya tentang pernyataan masalah, arsitekturnya tidak universal, tetapi mesin "khusus domain", menurut definisi, secara langsung tergantung pada permainan yang dimaksud.
Dengan mesin universal, saya memahami mesin yang mendukung konsep tingkat rendah, seperti "Obyek Game", "Adegan", "Komponen". Diputuskan untuk membuatnya bukan mesin universal, karena ini akan secara signifikan mengurangi waktu pengembangan.
Seperti yang direncanakan, permainan harus terdiri dari bagian-bagian berikut:

Artinya, ada latar belakang untuk setiap adegan, teks utama, serta bidang teks untuk input pengguna (novel visual dikandung dengan input pengguna sewenang-wenang, dan tidak dengan pilihan dari opsi yang diusulkan, seperti yang sering terjadi. Nanti saya akan menjelaskan mengapa ini buruk keputusan). Diagram juga menunjukkan bahwa ada beberapa adegan dalam permainan dan, sebagai akibatnya, transisi dapat dibuat di antara mereka.
Catatan: Menurut adegan saya maksud bagian logis dari permainan. Kriteria adegan bisa menjadi latar belakang yang sama di seluruh bagian ini.
Juga di antara persyaratan untuk mesin adalah kemampuan untuk memutar audio dan menampilkan pesan (dengan fungsi input pengguna opsional).
Mungkin keinginan yang paling penting adalah keinginan untuk menulis logika permainan bukan di Jawa, tetapi dalam beberapa bahasa deklaratif sederhana.
Ada juga keinginan untuk menyadari kemungkinan animasi prosedural, yaitu, gerakan gambar dasar, sehingga pada tingkat Jawa akan dimungkinkan untuk menentukan fungsi dimana kecepatan gerakan saat ini dipertimbangkan (misalnya, sehingga grafik kecepatannya langsung, atau sinusoid, atau yang lainnya).
Seperti yang direncanakan, semua interaksi pengguna harus dilakukan melalui sistem dialog. Dalam hal ini, dialog dianggap tidak harus berupa dialog dengan NPC atau sesuatu yang serupa, tetapi umumnya merupakan reaksi terhadap input pengguna mana pun yang telah didaftarkan pawang yang bersangkutan. Tidak jelas Ini akan menjadi lebih jelas segera.
2.2. Arsitektur dan implementasi
Dengan semua hal di atas, Anda dapat membagi mesin menjadi tiga bagian yang relatif besar yang sesuai dengan paket java yang sama:
display
- berisi segala sesuatu yang menyangkut output kepada pengguna informasi apa pun (grafik, teks dan suara), serta menerima input darinya. Semacam (Lihat), jika kita berbicara tentang MVC / MVP / dll.initializer
- berisi kelas di mana mesin diinisialisasi dan diluncurkan.sl
- berisi alat untuk bekerja dengan bahasa skrip (selanjutnya - SL).
Dalam paragraf ini saya akan mempertimbangkan dua bagian pertama. Saya akan mulai dengan yang kedua.
Kelas initializer memiliki dua metode utama: initialize()
dan run()
. Awalnya, kontrol datang ke kelas peluncur, dari tempat initialize()
dipanggil. Setelah panggilan, penginisialisasi menganalisis parameter yang diteruskan ke program (jalur ke direktori dengan pencarian dan nama pencarian untuk dijalankan), memuat manifes pencarian yang dipilih (tentangnya beberapa saat kemudian), menginisialisasi tampilan, memeriksa apakah versi bahasa (SL) yang diperlukan oleh pencarian didukung oleh data penerjemah, dan akhirnya, meluncurkan utas terpisah untuk konsol pengembang.
Segera setelah itu, jika semuanya berjalan lancar, peluncur memanggil metode run()
, yang menggerakkan pemuatan pencarian yang sebenarnya. Pertama, ada semua skrip yang terkait dengan pencarian yang diunduh (tentang struktur file pencarian - di bawah), mereka diumpankan ke penganalisa, yang hasilnya diberikan kepada penerjemah. Kemudian, inisialisasi semua adegan dimulai dan inisialisasi menyelesaikan pelaksanaan alirannya, akhirnya menggantung tombol Enter handler pada tampilan. Jadi, ketika pengguna menekan Enter, adegan pertama dimuat, tetapi lebih banyak tentang itu nanti.
Struktur file pencarian adalah sebagai berikut:

Ada folder terpisah untuk pencarian, di mana root adalah manifes, serta tiga folder tambahan: audio
- untuk suara, graphics
- untuk bagian visual dan scenes
- untuk skrip yang menggambarkan adegan.
Saya ingin menjelaskan secara singkat manifestonya. Ini berisi bidang-bidang berikut:
sl_version_req
- Versi SL diperlukan untuk memulai pencarian,init_scene
- nama adegan dari mana pencarian dimulai,quest_name
- nama pencarian indah yang muncul di judul jendela,resolution
- resolusi layar yang tujuan pencariannya (beberapa kata tentang ini nanti),font_size
- ukuran font untuk semua teks,font_name
adalah nama font untuk semua teks.
Perlu dicatat bahwa selama inisialisasi tampilan, antara lain, perhitungan resolusi rendering dilakukan: yaitu, resolusi yang diperlukan diambil dari manifes dan diperas ke ruang yang tersedia untuk jendela sehingga:
- rasio aspek tetap sama seperti dalam resolusi dari manifes,
- semua ruang yang tersedia ditempati dengan lebar atau tinggi.
Berkat ini, pengembang pencarian dapat yakin bahwa gambarnya, misalnya 16: 9, akan ditampilkan pada layar apa pun dalam rasio ini.
Juga, ketika tampilan diinisialisasi, kursor disembunyikan, karena tidak terlibat dalam gameplay.
Singkatnya tentang konsol pengembang. Ini dikembangkan karena alasan berikut:
- Untuk debug
- Jika terjadi kesalahan selama pertandingan, itu dapat diperbaiki melalui konsol pengembang.
Ini hanya menerapkan beberapa perintah, yaitu: keluaran deskriptor dari jenis tertentu dan statusnya, keluaran utas bekerja, restart layar, dan perintah - exec
yang paling penting, yang memungkinkan mengeksekusi kode SL dalam adegan saat ini.
Ini mengakhiri deskripsi penginisialisasi dan hal-hal terkait, dan kita dapat melanjutkan ke deskripsi tampilan.
Struktur akhirnya adalah sebagai berikut:

Dari pernyataan masalah, kita dapat menyimpulkan bahwa semua yang harus dilakukan adalah menggambar, menggambar teks, memutar audio.
Bagaimana teks / gambar biasanya digambar dalam mesin universal dan seterusnya? Ada metode tipe update()
, yang disebut setiap centang / langkah / bingkai / render / bingkai / dll dan di mana ada panggilan ke metode tipe drawText()
/ drawImage()
- ini memastikan penampilan teks / gambar dalam bingkai ini. Namun, begitu panggilan ke metode tersebut berhenti, rendering dari hal-hal yang terkait berhenti.
Dalam kasus saya, diputuskan untuk melakukan sedikit berbeda. Karena untuk novel visual, teks dan gambar relatif permanen, dan juga hampir semua yang dilihat pengguna (yaitu, mereka cukup penting), mereka dibuat sebagai objek game - yaitu, hal-hal yang hanya perlu Anda spawn dan tidak akan hilang sampai Anda bertanya kepada mereka. Selain itu, solusi ini menyederhanakan implementasi.
Objek (dari sudut pandang OOP) yang menggambarkan teks / gambar disebut descriptor. Artinya, untuk pengguna mesin API hanya ada deskriptor yang dapat ditambahkan ke kondisi tampilan dan dihapus dari itu. Dengan demikian, dalam versi final tampilan ada deskriptor berikut (mereka sesuai dengan kelas dengan nama yang sama):
Layar juga berisi bidang untuk penerima input saat ini (deskriptor input) dan bidang yang menunjukkan deskriptor teks mana yang sekarang memiliki fokus dan yang teksnya akan bergulir di bawah tindakan yang sesuai pada bagian pengguna.
Siklus permainan terlihat seperti ini:
- Pemrosesan audio - memanggil metode
update()
pada deskriptor audio, yang memeriksa keadaan audio saat ini, membebaskan memori (jika perlu), dan melakukan pekerjaan teknis lainnya. - Memproses penekanan tombol - mentransfer karakter yang dimasukkan ke deskriptor untuk menerima input, memproses penekanan tombol untuk tombol gulir (panah atas dan bawah) dan Backspace.
- Pemrosesan Animasi.
- Menghapus latar belakang di buffer rendering (
BufferedImage
berfungsi sebagai buffer). - Menggambar gambar.
- Rendering teks.
- Bidang gambar untuk input.
- Output dari buffer ke layar.
- Menangani
PostWorkDescriptor
. - Beberapa bekerja untuk mengganti status tampilan, yang akan saya bahas nanti (di bagian interpreter SL).
- Hentikan aliran untuk waktu yang dihitung secara dinamis sehingga FPS sama dengan nilai yang ditentukan (30 secara default).
Catatan: Mungkin muncul pertanyaan, "Mengapa memberikan bidang input jika deskriptor teks yang sesuai telah dibuat untuk mereka yang akan diberikan langkah lebih awal?" Bahkan, rendering dalam paragraf 7 tidak terjadi - hanya parameter InputDescriptor
yang disinkronkan dengan parameter InputDescriptor
- seperti visibilitas layar, posisi, ukuran, dan lainnya. Ini dilakukan, seperti ditunjukkan di atas, untuk alasan bahwa pengguna tidak secara langsung mengontrol deskriptor input yang sesuai dengan deskriptor teks dan umumnya tidak tahu apa-apa tentang itu.
Perlu dicatat bahwa ukuran dan posisi elemen pada layar tidak diatur dalam piksel, tetapi dalam ukuran relatif - angka dari 0 hingga 1 (diagram di bawah). Yaitu, seluruh lebar untuk rendering adalah 1, dan keseluruhan tinggi adalah 1 (dan mereka tidak sama, yang saya lupa beberapa kali dan kemudian menyesal). Itu juga akan bermanfaat untuk menjadikan (0,0) menjadi pusat, dan lebar / tinggi harus sama dengan dua, tetapi untuk beberapa alasan saya lupa / tidak memikirkannya. Namun, bahkan opsi dengan lebar / tinggi sama dengan 1 menyederhanakan kehidupan pengembang pencarian.

Beberapa kata tentang sistem untuk membebaskan memori.
Setiap deskriptor memiliki metode setDoFree(boolean)
, yang harus dihubungi pengguna jika ia ingin menghancurkan deskriptor yang diberikan. Pengumpulan sampah untuk deskriptor dari beberapa tipe terjadi segera setelah memproses semua deskriptor dari tipe ini. Juga, audio yang diputar sekali dihapus secara otomatis setelah pemutaran berakhir. Sama persis dengan animasi non-looping.
Jadi, saat ini Anda dapat menggambar apa pun yang Anda inginkan, tetapi ini bukan gambar di atas, yang hanya memiliki latar belakang, teks utama dan bidang input. Dan inilah pembungkus di atas layar, yang sesuai dengan kelas DefaultDisplayToolkit
.
Ketika diinisialisasi, itu hanya menambahkan deskriptor untuk latar belakang, teks, dll ke layar.Ia juga tahu bagaimana menampilkan pesan dengan ikon opsional, bidang input dan panggilan balik.
Kemudian bug kecil muncul, sebuah koreksi penuh yang membutuhkan mengulangi setengah dari sistem rendering: jika Anda melihat urutan rendering dalam loop game, Anda dapat melihat bahwa gambar diambil terlebih dahulu dan hanya kemudian teks. Pada saat yang sama, ketika toolkit menunjukkan gambar, itu menempatkannya di tengah layar lebar dan tinggi . Dan jika ada banyak teks dalam pesan, maka sebagian teks harus tumpang tindih dengan adegan. Namun, karena latar belakang pesan adalah gambar (benar-benar hitam, tapi tetap saja), dan gambar digambar sebelum teks, satu teks dilapiskan pada teks lainnya (tangkapan layar di bawah). Masalahnya sebagian diselesaikan dengan pemusatan vertikal bukan pada layar, tetapi di area di atas teks utama. Solusi lengkap akan mencakup memperkenalkan parameter kedalaman dan mengulang renderers dari kata "sepenuhnya".
Mungkin ini tentang tampilan, akhirnya, semuanya. Anda dapat beralih ke bahasa, seluruh API untuk bekerja dengan yang terkandung dalam paket sl
.
3. Bahasa scripting
Catatan: Jika% USERNAME% yang dihormati membacanya di sini, dia melakukannya dengan baik, dan saya akan memintanya untuk tidak berhenti melakukannya: sekarang ini akan jauh lebih menarik daripada sebelumnya.
3.1. Bahasa
Awalnya, saya ingin membuat bahasa deklaratif di mana hanya perlu menunjukkan semua parameter yang diperlukan untuk adegan itu, dan itu saja. Mesin akan mengambil semua logika. Namun, pada akhirnya, saya sampai pada bahasa prosedural, bahkan dengan elemen OOP (hampir tidak dapat dibedakan), dan ini adalah solusi yang baik, karena, dibandingkan dengan versi deklaratif, itu memberi peluang lebih banyak fleksibilitas dalam logika game.
Sintaks bahasa dipikirkan agar sesederhana mungkin untuk parsing, yang logis, mengingat jumlah waktu yang tersedia.
Jadi, kode tersebut disimpan dalam file teks dengan ekstensi SSF; setiap file berisi deskripsi satu atau lebih adegan; setiap adegan berisi nol atau lebih aksi; setiap tindakan berisi nol atau lebih operator.
Sedikit penjelasan tentang persyaratan. Suatu tindakan hanyalah sebuah prosedur tanpa kemungkinan lewat argumen (sama sekali tidak mencegah perkembangan game). Operator tampaknya tidak cukup apa arti kata ini dalam bahasa biasa (+, -, /, *), tetapi bentuknya sama: operator adalah totalitas nama dan semua argumennya.
Mungkin Anda ingin akhirnya melihat kode sumber untuk SL, ini dia:
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 } }
Sekarang menjadi jelas apa operatornya. Terlihat juga bahwa setiap tindakan adalah blok pernyataan (pernyataan dapat berupa blok pernyataan), dan juga fakta bahwa komentar satu baris didukung (tidak masuk akal untuk memasukkan yang multi-baris, selain itu, saya tidak menggunakan yang satu-baris).
Demi penyederhanaan, konsep seperti "variabel" tidak diperkenalkan ke dalam bahasa; sebagai hasilnya, semua nilai yang digunakan dalam kode adalah literal. Tergantung pada jenisnya, literal berikut dibedakan:
Beberapa kata tentang penguraian bahasa. Ada beberapa level "memuat" kode (diagram di bawah):
- Tokenizer adalah kelas modular untuk memecah kode sumber menjadi token (unit semantik minimum bahasa). Setiap jenis token dikaitkan dengan nomor - jenisnya. Mengapa modular? Karena bagian-bagian dari tokenizer yang memeriksa apakah ada bagian dari kode sumber yang merupakan token dari jenis tertentu diisolasi dari tokenizer dan diunduh dari luar (dari paragraf kedua).
- Add-on tokenizer adalah kelas yang mendefinisikan penampilan setiap jenis token di SL; di tingkat bawah menggunakan tokenizer. Juga di sini adalah penyaringan token ruang angkasa dan penghilangan komentar single-line. Outputnya memberikan aliran token yang bersih, yang digunakan ...
- ... parser (itu juga modular), yang menghasilkan pohon sintaksis abstrak pada output. Modular - karena dengan sendirinya ia hanya dapat mengurai adegan dan tindakan, tetapi tidak tahu bagaimana menganalisis operator. Oleh karena itu, modul dimuat ke dalamnya (pada kenyataannya, dia sendiri memuatnya dalam konstruktor, yang tidak terlalu baik), yang dapat mengurai masing-masing operatornya.

Sekarang, secara singkat tentang operator, sehingga gagasan fungsionalitas bahasa muncul. Awalnya, ada 11 operator, kemudian dalam proses berpikir melalui permainan, beberapa dari mereka bergabung menjadi satu, beberapa berubah, dan 9 lainnya ditambahkan. Ini tabel ringkasannya:
Operator untuk bekerja dengan penghitung - variabel integer khusus adegan.
Ada juga pemikiran memperkenalkan return
(fungsionalitas yang sesuai bahkan ditambahkan pada tingkat inti penerjemah), tetapi saya lupa, dan itu tidak berguna.
, , : 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. Grafik
, :

, , - - " ". :
- (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" .