Menulis mesin gim di tahun pertama Anda: mudah! (Hampir)

Halo! Nama saya Gleb Maryin, tahun pertama saya sarjana "Matematika Terapan dan Ilmu Komputer" di St. Petersburg HSE. Pada semester kedua, semua mahasiswa baru di program kami membuat proyek tim dalam C ++. Rekan satu tim saya dan saya memutuskan untuk menulis mesin permainan. 

Baca apa yang kita dapat di bawah kucing.


Ada tiga dari kita di tim: saya, Alexei Luchinin dan Ilya Onofriychuk. Tidak ada dari kita yang ahli dalam pengembangan game, apalagi dalam menciptakan mesin game. Ini adalah proyek besar pertama bagi kami: sebelum itu, kami hanya mengerjakan pekerjaan rumah dan laboratorium, jadi tidak mungkin para profesional di bidang komputer grafis akan menemukan informasi baru untuk diri mereka sendiri di sini. Kami akan senang jika ide-ide kami membantu mereka yang juga ingin membuat mesin mereka sendiri. Tetapi topik ini kompleks dan beragam, dan artikel ini sama sekali tidak mengklaim sebagai literatur khusus yang lengkap.

Semua orang yang tertarik mempelajari implementasi kami - selamat membaca!

Seni grafis


Jendela pertama, mouse dan keyboard


Untuk membuat windows, menangani input mouse dan keyboard, kami memilih perpustakaan SDL2. Itu pilihan acak, tapi sejauh ini kami belum menyesalinya. 

Penting pada tahap pertama untuk menulis pembungkus yang nyaman di atas perpustakaan sehingga Anda dapat membuat jendela dengan beberapa baris, melakukan manipulasi dengannya seperti menggerakkan kursor dan memasuki mode layar penuh dan menangani acara: penekanan tombol, gerakan kursor. Tugasnya tidak sulit: kami dengan cepat membuat program yang tahu cara menutup dan membuka jendela, dan ketika Anda mengklik PCM, tampilkan "Halo, Dunia!". 

Kemudian siklus permainan utama muncul:

Event ev;
bool running = true;
while (running):
	ev = pullEvent();
	for handler in handlers[ev.type]:
		handler.handleEvent(ev);

Setiap pengendali acara terlampir - handlersmis handlers[QUIT] = {QuitHandler()}. Tugas mereka adalah menangani acara terkait. QuitHandlerDalam contoh, itu akan mengekspos running = false, sehingga menghentikan permainan.

Halo Dunia


Untuk menggambar di mesin kami gunakan OpenGL. Yang pertama Hello World, seperti yang saya pikirkan, dalam banyak proyek, adalah kotak putih dengan latar belakang hitam: 

glBegin(GL_QUADS);
glVertex2f(-1.0f, 1.0f);
glVertex2f(1.0f, 1.0f);
glVertex2f(1.0f, -1.0f);
glVertex2f(-1.0f, -1.0f);
glEnd();


Kemudian kami belajar cara menggambar poligon dua dimensi dan melakukan angka-angka dalam kelas yang terpisah GraphicalObject2d, yang dapat berputar dengan glRotate, bergerak dengan glTranslatedan meregangkan glScale. Kami mengatur warna dalam empat saluran menggunakan glColor4f(r, g, b, a).

Dengan fungsi ini, Anda sudah dapat membuat air mancur kotak yang indah. Buat kelas ParticleSystemyang memiliki array objek. Setiap iterasi dari loop utama, sistem partikel memperbarui kuadrat lama dan mengumpulkan beberapa yang baru yang dimulai dalam arah acak:



Kamera


Langkah selanjutnya adalah menulis kamera yang bisa bergerak dan melihat ke arah yang berbeda. Untuk memahami bagaimana mengatasi masalah ini, kami membutuhkan pengetahuan dari aljabar linier. Jika ini tidak terlalu menarik bagi Anda, Anda dapat melewati bagian ini, lihat gif, dan baca terus .

Kami ingin menggambar titik pada koordinat layar, mengetahui koordinatnya relatif terhadap pusat objek yang dimilikinya.

  1. Pertama, kita perlu menemukan koordinatnya relatif terhadap pusat dunia tempat objek itu berada.
  2. Kemudian, mengetahui koordinat dan lokasi kamera, cari posisi titik di dasar kamera.
  3. Kemudian proyeksikan verteks ke bidang layar. 

Seperti yang Anda lihat, ada tiga tahap. Perkalian dengan tiga matriks sesuai dengan mereka. Kami menyebut matriks ini Model, Viewdan Projection.

Mari kita mulai dengan mendapatkan koordinat objek di dasar dunia. Tiga transformasi dapat dilakukan dengan objek: skala, memutar, dan bergerak. Semua operasi ini ditentukan dengan mengalikan vektor asli (koordinat dalam dasar objek) dengan matriks yang sesuai. Maka matriks Modelakan terlihat seperti ini: 

Model = Translate * Scale * Rotate. 

Selanjutnya, mengetahui posisi kamera, kami ingin menentukan koordinat dalam dasarnya: kalikan koordinat yang sebelumnya diperoleh dengan matriks View. Di C ++, ini mudah dihitung menggunakan fungsi:


glm::mat4 View = glm::lookAt(cameraPosition, objectPosition, up);

Secara harfiah: lihat objectPositiondari suatu posisi cameraPosition, dan arah ke atas adalah "ke atas". Mengapa arahan ini perlu? Bayangkan memotret teko. Anda mengarahkan kamera kepadanya dan menempatkan ketel di bingkai. Pada titik ini, Anda dapat mengatakan dengan tepat di mana bingkai berada di bagian atas (kemungkinan besar di mana ketel memiliki penutup). Program tidak dapat menemukan bagi kita bagaimana mengatur frame, dan itulah sebabnya vektor "atas" harus ditentukan.

Kami mendapat koordinat di dasar kamera, masih memproyeksikan koordinat yang diperoleh di bidang kamera. Matriks terlibat dalam hal ini Projection, yang menciptakan efek mengurangi objek ketika dihapus dari kita.

Untuk mendapatkan koordinat titik pada layar, Anda perlu mengalikan vektor dengan matriks setidaknya lima kali. Semua matriks memiliki ukuran 4 oleh 4, jadi Anda harus melakukan beberapa operasi multiplikasi. Kami tidak ingin memuat inti prosesor dengan banyak tugas sederhana. Untuk ini, kartu video yang memiliki sumber daya yang diperlukan lebih baik. Jadi, Anda perlu menulis shader: instruksi kecil untuk kartu video. OpenGL memiliki bahasa shader GLSL khusus, mirip dengan C, yang akan membantu kami melakukan ini. Mari kita tidak membahas detail penulisan shader, lebih baik akhirnya melihat apa yang terjadi:


Penjelasan: Ada sepuluh kotak yang berjarak pendek di belakang satu sama lain. Di sisi kanan mereka adalah pemain yang memutar dan menggerakkan kamera. 

Fisika


Apa itu permainan tanpa fisika? Untuk menangani interaksi fisik, kami memutuskan untuk menggunakan pustaka Box2d dan membuat kelas WorldObject2dyang diwarisi darinya GraphicalObject2d. Sayangnya, Box2d tidak bekerja di luar kotak, sehingga Ilya yang pemberani menulis bungkus untuk b2Body dan semua koneksi fisik yang ada di perpustakaan ini.


Sampai saat itu, kami berpikir untuk membuat grafik di mesin benar-benar dua dimensi, dan untuk penerangan, jika kami memutuskan untuk menambahkannya, gunakan teknik raycasting. Tapi kami punya kamera indah yang bisa menampilkan objek dalam ketiga dimensi. Karena itu, kami menambahkan ketebalan pada semua objek dua dimensi - mengapa tidak? Selain itu, di masa depan, ini akan memungkinkan Anda untuk membuat pencahayaan yang cukup indah yang akan meninggalkan bayangan dari benda tebal.

Pencahayaan muncul di antara kasus-kasus. Untuk membuatnya, perlu untuk menulis instruksi yang sesuai untuk menggambar setiap piksel - shader fragmen.



Tekstur


Kami menggunakan perpustakaan DevIL untuk mengunggah gambar. Masing - masing GraphicalObject2dmenjadi sesuai satu contoh kelas GraphicalPolygon- bagian depan objek - dan GraphicalEdge- sisi. Pada masing-masing Anda dapat meregangkan tekstur Anda. Hasil pertama:


Semua yang diperlukan dari grafik sudah siap: menggambar, satu sumber cahaya dan tekstur. Grafik - itu saja untuk saat ini.

Mesin negara, mengatur perilaku objek


Setiap objek, apa pun itu - keadaan dalam mesin negara, grafik atau fisik - harus "berdetak", yaitu, setiap iterasi dari loop game diperbarui.

Objek yang dapat memperbarui diwarisi dari kelas Perilaku yang kami buat. Ini memiliki fungsi onStart, onActive, onStopyang memungkinkan Anda untuk menimpa perilaku pewaris saat startup, selama hidup dan pada akhir aktivitasnya. Sekarang kita perlu membuat objek tertinggi Activityyang memanggil fungsi-fungsi ini dari semua objek. Fungsi loop yang melakukan ini adalah sebagai berikut:

void loop():
    onAwake();
    awake = true;
    while (awake):
        onStart();
        running = true
        while (running):
            onActive();
        onStop();
    onDestroy();

Untuk saat ini running == true, seseorang dapat memanggil fungsi pause()yang berfungsi running = false. Jika seseorang memanggil kill(), lalu awake, dan runningberalih ke false, dan aktivitas berhenti total.

Masalah: kami ingin menjeda sekelompok objek, misalnya, sistem partikel dan partikel di dalamnya. Dalam kondisi saat ini, Anda perlu memanggil secara manual onPauseuntuk setiap objek, yang sangat tidak nyaman.

Solusi: setiap orang Behaviorakan memiliki larik subBehaviorsyang akan diperbarui, yaitu:

void onStart():
	onStart() 		//     
	for sb in subBehaviors:
		sb.onStart()	//       Behavior
void onActive():
	onActive()
	for sb in subBehaviors:
		sb.onActive()

Dan seterusnya, untuk setiap fungsi.

Tetapi tidak setiap perilaku dapat diatur dengan cara ini. Misalnya, jika seorang musuh berjalan di peron, musuh, maka kemungkinan besar dia memiliki kondisi yang berbeda: dia berdiri idle_stay, dia berjalan di peron tanpa memperhatikan kita idle_walk, dan kapan saja dia bisa melihat kita dan masuk ke keadaan menyerang attack. Saya juga ingin mengatur kondisi untuk transisi antar negara, misalnya:

bool isTransitionActivated(): 		//  idle_walk->attack
	return canSee(enemy);

Pola yang diinginkan adalah mesin negara. Kami juga menjadikannya pewaris Behavior, karena pada setiap centang perlu untuk memeriksa apakah saatnya telah tiba untuk beralih negara. Ini berguna tidak hanya untuk objek dalam game. Sebagai contoh, Levelini adalah keadaan Level Switcher, dan transisi di dalam mesin pengontrol adalah kondisi untuk beralih level dalam permainan.

Negara memiliki tiga tahap: telah dimulai, terus berdetak, telah berhenti. Anda dapat menambahkan beberapa tindakan ke setiap tahap, misalnya, melampirkan tekstur ke objek, menerapkan impuls padanya, mengatur kecepatan, dan sebagainya.

Konservasi


Membuat level dalam editor, saya ingin dapat menyimpannya, dan game itu sendiri harus dapat memuat level dari data yang disimpan. Oleh karena itu, semua objek yang perlu diselamatkan diwariskan dari kelas NamedStoredObject. Ini menyimpan string dengan nama, nama kelas dan memiliki fungsi dump()yang membuang data tentang suatu objek menjadi string.  

Untuk melakukan save, tetap hanya menimpa dump()untuk setiap objek. Memuat adalah konstruktor dari string yang berisi semua informasi tentang objek. Pengunduhan selesai saat konstruktor dibuat untuk setiap objek. 

Faktanya, gim dan editor hampir kelas yang sama, hanya di gamenya level dimuat dalam mode baca, dan di editor dalam mode rekam. Mesin menggunakan perpustakaan quickjson untuk menulis dan membaca objek dari json.

GUI


Pada titik tertentu, muncul pertanyaan di hadapan kita: biarkan gambar, mesin negara, dan sisanya ditulis. Bagaimana cara pengguna menulis game menggunakan ini? 

Dalam versi asli, ia harus mewarisi dari Game2ddan menimpa onActive, dan membuat objek di bidang kelas. Tetapi selama penciptaan, ia tidak dapat melihat apa yang ia ciptakan, dan ia juga perlu mengkompilasi programnya dan tautan ke perpustakaan kami. Kengerian! Akan ada nilai tambah - orang dapat menanyakan perilaku rumit yang bisa dibayangkan: misalnya, pindahkan blok tanah sebanyak nyawa pemain, dan lakukan asalkan Uranus berada di konstelasi Taurus dan euro tidak melebihi 40 rubel. Namun, kami masih memutuskan untuk membuat antarmuka grafis.

Dalam antarmuka grafis, jumlah tindakan yang dapat dilakukan dengan suatu objek akan terbatas: membolak-balik slide animasi, menerapkan kekuatan, mengatur kecepatan tertentu, dan sebagainya. Situasi yang sama dengan transisi di mesin negara. Dalam mesin besar, masalah sejumlah tindakan diselesaikan dengan menghubungkan program saat ini dengan yang lain - misalnya, Unity dan Godot menggunakan ikatan dengan C #. Sudah dari skrip ini Anda dapat melakukan apa saja: dan lihat di konstelasi Uranus, dan apa nilai tukar euro saat ini. Kami tidak memiliki fungsi seperti itu saat ini, tetapi rencana kami termasuk menghubungkan mesin dengan Python 3.

Untuk mengimplementasikan antarmuka grafis, kami memutuskan untuk menggunakan Dear ImGui, karena sangat kecil (dibandingkan dengan Qt yang terkenal) dan menulis di atasnya sangat sederhana. ImGui - paradigma menciptakan antarmuka grafis. Di dalamnya, setiap iterasi dari loop utama, semua widget dan jendela digambar ulang hanya jika perlu. Di satu sisi, ini mengurangi jumlah memori yang dikonsumsi, tetapi di sisi lain, kemungkinan besar membutuhkan waktu lebih lama dari satu pelaksanaan fungsi kompleks untuk membuat dan menyimpan informasi yang diperlukan untuk gambar berikutnya. Tetap hanya untuk mengimplementasikan antarmuka untuk membuat dan mengedit.

Inilah tampilan antarmuka grafis pada saat rilis artikel:


Editor level


Editor Mesin Negara

Kesimpulan


Kami hanya menciptakan dasar untuk menggantung sesuatu yang lebih menarik. Dengan kata lain, ada ruang untuk tumbuh: Anda dapat menerapkan rendering bayangan, kemampuan untuk membuat lebih dari satu sumber cahaya, Anda dapat menghubungkan mesin dengan interpreter Python 3 untuk menulis skrip untuk permainan. Saya ingin memperhalus antarmuka: membuatnya lebih cantik, menambahkan lebih banyak objek berbeda, mendukung hot key ...

Masih ada banyak pekerjaan, tetapi kami senang dengan apa yang kami miliki saat ini. 

Selama pembuatan proyek, kami mendapat banyak pengalaman beragam: bekerja dengan grafik, membuat antarmuka grafis, bekerja dengan file json, membungkus berbagai perpustakaan C. Dan juga pengalaman menulis proyek besar pertama dalam sebuah tim. Kami berharap bahwa kami dapat menceritakan tentang hal itu semenarik itu menarik untuk mengatasinya :)

Tautan ke proyek gihab : github.com/Glebanister/ample

All Articles