Mengembangkan game di LibGDX menggunakan templat Sistem Entitas Komponen

Hai Habr! Nama saya Andrey Shilo, saya adalah pengembang android di FINCH . Hari ini saya akan memberi tahu Anda tentang kesalahan apa yang tidak boleh dilakukan saat menulis bahkan game paling sederhana dan mengapa pendekatan arsitektur Entity Component System (ECS) keren.

Pertama kali selalu menyakitkan


Kami memiliki satu proyek menyenangkan untuk memegang media besar, yang merupakan sosial non-standar. jaringan dengan posting, komentar, suka dan video. Suatu kali, mereka memberi kami tugas - untuk memperkenalkan mekanik permainan sebagai promosi. Permainan tampak seperti balapan mini sederhana, di mana dengan ketukan di kiri / kanan, mobil bergerak ke jalur kiri / kanan. Jadi, menghindari rintangan dan mengumpulkan booster Anda harus sampai ke garis finish, semuanya, pemain memiliki tiga nyawa.

Permainan itu seharusnya dilaksanakan tepat di dalam aplikasi, tentu saja, pada layar yang terpisah. Kami memilih LibGDX tanpa syarat sebagai mesin, karena Anda dapat membuat kode game di kotiln, dan men-debug di desktop, meluncurkan game sebagai aplikasi java . Pada saat yang sama, kami tidak memiliki orang yang tahu mesin lain yang dapat diimplementasikan dalam aplikasi (jika Anda tahu, maka bagikan).

Permainan terlihat seperti ini:



Karena permainan menurut TK asli tampak sederhana, kami tidak mempelajari pendekatan arsitektur. Selain itu, promosi itu sendiri berlalu dengan cepat - rata-rata, satu bagian memakan waktu satu setengah bulan. Dengan demikian, nanti, kode permainan hanya akan dipotong dan tidak akan diperlukan sampai promo berikutnya, asalkan seseorang ingin mengulang sesuatu seperti itu.

Semua faktor yang dijelaskan di atas dan para manajer tercinta yang mendesak mendorong kami untuk menulis mekanik permainan tanpa arsitektur apa pun.

Deskripsi game yang dihasilkan


Sebagian besar kode dikompilasi di kelas: MyGdxGame: Game , GameScreen: Screen dan TrafficGame: Actor .

MyGdxGame - adalah titik masuk pada awal permainan, di sini parameter ditransfer ke konstruktor dalam bentuk string. GameScreen dan parameter game juga dibuat di sini, yang diteruskan ke kelas ini, tetapi dalam bentuk yang berbeda.

GameScreen - menciptakan aktor dari game TrafficGame, menambahkannya ke adegan, memberikannya parameter yang telah disebutkan, dan juga "mendengarkan" klik pengguna di layar dan memanggil metode yang sesuai dari aktor TrafficGame.

TrafficGame - aktor utama adegan di mana semua gerakan permainan terjadi: rendering dan logika kerja.

Meskipun menggunakan scene2d memungkinkan untuk membangun pohon aktor bersarang, ini bukan solusi arsitektur terbaik. Namun, untuk mengimplementasikan game UI / UX (di LibGDX), scene2d adalah pilihan yang bagus.

Dalam kasus kami, TrafficGame memiliki banyak koleksi instance campuran dan semua jenis panji perilaku yang diizinkan dalam metode dengan konstruksi besar saat membangun. Contoh:

var isGameActive: Boolean = true set(value) { backgroundActor?.isGameActive = value boostersMap.values.filterNotNull().forEach { it.isGameActive = value } obstaclesMap.values.filterNotNull().forEach { it.isGameActive = value } finishActor.isGameActive = value field = value } private var backgroundActoolbarActor private val pauseButtonActor: PauseButtonActor private val finishActor: FinishActor private var isQuizWon = falser: BackgroundActor? = null private var playerCarActor: PlayerCarActor private var toolbarActor: To private var pointsTime = 0f private var totalTimeElapsed = 0 private val timeToFinishTheGame = 50 private var lastQuizBoosterTime = 0.0f private var lastBoosterTime = 0.0f private val boostersMap = hashMapOf<Long?, BoosterActor?>() private var boosterSpawnedCount = 0 private var totalBoostersEatenCount = 0 private val boosterLimit = 20 private var lastBoosterYPos = 0.0f private var toGenerateBooster = false private var lastObstacleTime = 0.0f private var obstaclesMap = hashMapOf<Long?, ObstacleActor?>() 

Tentu, Anda tidak boleh menulis seperti itu. Tetapi dalam pembelaan saya akan mengatakan bahwa itu terjadi karena:

  • Anda harus mulai sekarang, baik, dan kami akan menunjukkan TK terakhir dengan desain nanti. Klasik
  • Arsitektur yang sudah dikenal (MVP / MVC / MVVM, dll.) Tidak cocok untuk implementasi proses game, karena mereka dirancang murni untuk antarmuka pengguna, dalam game semuanya terjadi secara real time.
  • Awalnya, permainan ini tampak sederhana, tetapi sebenarnya membutuhkan banyak kode, dengan mempertimbangkan sejumlah besar nuansa, yang sebagian besar muncul selama penulisan permainan.



Selain semua kesulitan ini, ada masalah umum lainnya dengan warisan. Jika Anda membuat game lebih sulit, misalnya platformer, maka muncul pertanyaan - "Bagaimana cara mendistribusikan kode yang dapat digunakan kembali antara objek-objek game?" Opsi yang paling umum dipilih adalah pewarisan, di mana kode yang dapat digunakan kembali ditempatkan di kelas induk. Tetapi solusi ini menciptakan banyak masalah jika kondisi yang muncul tidak sesuai dengan pohon warisan:



Dan mereka biasanya memecahkan masalah seperti itu dengan menulis ulang struktur pohon warisan dari awal (well, kali ini tentu akan lebih baik), atau kelas induknya dipatahkan dengan kruk.



ECS adalah segalanya bagi kami


Cerita yang sama sekali berbeda adalah game promo kami yang kedua. Dia seperti Flappy Bird , tetapi dengan perbedaan: karakternya dikendalikan oleh suara, dan langit-langit dan lantai bukanlah hambatan - Anda bisa meluncur di atasnya.
Contoh gameplay dan untuk perbandingan, proses bermain Flappy Bird:




Untuk kejelasan, dalam contoh, kamera dilepas untuk melihat bagian belakang permainan. Lantai dan langit-langit adalah blok persegi yang, mencapai tepi, disusun kembali ke awal, dan hambatan dihasilkan sesuai dengan pola yang diberikan yang datang dari belakang. Desain game dipilih oleh pelanggan, jadi jangan kaget.

Saya suka pengembangan game untuk perangkat seluler dan pada jam-jam libur, demi percobaan, saya akan mengeksplorasi pola permainan dan segala sesuatu yang terkait dengan pengembangan game. Saya membaca buku tentang pola desain game , tetapi tidak mengerti apa yang sebenarnya menjadi arsitektur logika gameplay yang sebenarnya, sampai saya menemukan ECS.

Sistem Komponen Entitas - pola desain yang paling sering digunakan dalam pengembangan game. Gagasan utama polanya adalah komposisi alih-alih warisan . Komposisi ini memungkinkan Anda untuk mencampur mekanisme yang berbeda pada objek game, ini, di masa depan, memungkinkan Anda untuk mendelegasikan pengaturan properti objek ke desainer game, misalnya, melalui konstruktor tertulis. Karena saya sudah terbiasa dengan pola ini, kami memutuskan untuk menerapkannya di game kedua.

Pertimbangkan komponen-komponen dari pola:

  • Komponen - objek dengan struktur data sederhana yang tidak mengandung logika apa pun, atau bertindak sebagai pintasan. Komponen dibagi berdasarkan tujuan dan menentukan semua properti dari objek game. Kecepatan, posisi, tekstur, tubuh, dll. dll. semua ini dijelaskan dalam komponen dan kemudian ditambahkan ke objek game.

     class VelocityComponent: Component { val velocity = Vector2() } 
  • Entitas - objek game: penghalang / penguat / pahlawan yang dikontrol dan bahkan latar belakang. Mereka tidak memiliki kelas khusus berdasarkan tipe: UltraMegaSuperman: GameUnit, tetapi hanya merupakan wadah untuk set Komponen. Fakta bahwa entitas tertentu dengan demikian UltraMegaSuperman mendefinisikan set komponen dan parameternya.
    Misalnya, dalam kasus kami, karakter utama memiliki komponen berikut:

    • TextureComponent - menentukan apa yang harus digambar di layar
    • TransformComponent - posisi objek di ruang permainan
    • VelocityComponent - kecepatan objek di ruang permainan
    • HeroControllerComponent - berisi nilai-nilai yang mempengaruhi pergerakan pahlawan
    • ImmortalityTimeComponent - berisi sisa waktu keabadian
    • DynamicComponent - menunjukkan bahwa objek tidak statis dan tunduk pada gravitasi
    • BodyComponent - mendefinisikan tubuh fisik pahlawan 2d yang diperlukan untuk menghitung tabrakan
  • Sistem - berisi kode untuk memproses data dari komponen masing-masing entitas. Mereka tidak boleh menyimpan objek Entitas dan / atau Komponen , karena ini akan bertentangan dengan pola. Idealnya, mereka harus bersih sama sekali.

    Sistem melakukan semua pekerjaan kotor: menggambar semua objek dalam game, memindahkan objek dengan kecepatannya, memeriksa tabrakan, mengubah kecepatan dari kontrol yang masuk, dan sebagainya. Misalnya, efek gravitasi terlihat seperti ini:

     override fun processEntity(entity: Entity, deltaTime: Float) { entity.getComponent(VelocityComponent::class.java) .velocity .add(0f, -GRAVITY * deltaTime) } 

    Spesialisasi masing-masing sistem menentukan persyaratan untuk entitas yang harus diproses. Yaitu, dalam contoh di atas, entitas harus memiliki komponen kecepatan VelocityComponent dan DynamicComponent sehingga entitas ini dapat diproses, jika tidak entitas ini tidak menarik bagi sistem, dan begitu pula dengan yang lain. Untuk menggambar tekstur, misalnya, Anda perlu tahu apa tekstur TextureComponent dan di mana menggambar TransformComponent. Untuk menentukan persyaratan dalam setiap sistem, Keluarga ditulis dalam konstruktor di mana kelas komponen ditunjukkan.

     Family.all(TransformComponent::class.java, TextureComponent::class.java).get() 

    Juga, urutan entitas pemrosesan dalam sistem dapat disesuaikan oleh pembanding. Selain itu, urutan pelaksanaan sistem di mesin juga diatur oleh nilai prioritas.

Mesin menggabungkan tiga komponen. Ini berisi semua sistem dan semua entitas dalam game. Di awal permainan, semua sistem yang diperlukan dalam permainan

 engine.apply { addSystem(ControlSystem()) addSystem(GravitySystem()) addSystem(renderingSystem) addSystem(MovementSystem()) addSystem(EnemyGeneratorSystem()) } 
serta entitas awal ditambahkan ke mesin,
 val hero: Entity = engine.createEntity() engine.addEntity(hero) 

PooledEngine :: createEntity - mendapatkan objek entitas dari pool , karena entitas dapat dibuat selama permainan, agar tidak membuang sampah di memori. Jika perlu, mereka diambil dari kumpulan objek, dan ketika dihapus, mereka ditempatkan kembali. Demikian pula yang dilakukan untuk komponen. Saat menerima komponen dari kumpulan, perlu menginisialisasi semua bidang, karena mungkin berisi informasi tentang penggunaan sebelumnya dari komponen ini.

Hubungan antara bagian-bagian utama dari pola disajikan di bawah ini:



Mesin berisi kumpulan sistem dan kumpulan entitas. Setiap sistem menerima tautan dari mesin ke kumpulan entitas, yang merupakan seleksi dari koleksi umum sesuai dengan persyaratan sistem, itu akan diperbarui selama pertandingan ketika entitas dan komponen berubah. Setiap entitas berisi kumpulan komponennya yang mendefinisikannya dalam game.

Siklus permainan disusun sebagai berikut:

  1. Menggunakan penerapan pola "Game cycle" dari LibGDX, dalam metode pembaruan kami mendapatkan kenaikannya di setiap langkah waktu - deltaTime.
  2. Selanjutnya, kita melewatkan waktu untuk mesin. Dan dia, pada gilirannya, beralih melalui sistem dalam satu siklus, mendistribusikannya deltaTime.
     for (i in 0 until systems.size()) { val system = systems[i] if (system.checkProcessing()) { system.update(deltaTime) } } 
  3. Sistem yang menerima deltaTime menyortir entitas mereka dan menerapkan perubahan pada mereka dengan mempertimbangkan deltaTime akun.
     for (i in 0 until entities.size()) { processEntity(entities[i], deltaTime) } 

Ini terjadi setiap ukuran permainan.

Manfaat ECS


  1. Data lebih dulu . Karena sistem hanya memproses entitas yang sesuai dengan mereka, jika tidak ada entitas seperti itu, sistem tidak akan melakukan apa pun, ini memungkinkan untuk menguji dan men-debug fitur baru, hanya membuat entitas yang diperlukan untuk ini.

    Misalnya, Anda membuat game "Tanks". Setelah beberapa waktu, Anda memutuskan untuk menambahkan jenis medan baru - "lava". Jika tangki mencoba melewatinya, maka itu akan berakhir dengan kegagalan. Tetapi teknologi futuristik datang untuk menyelamatkan, dengan menginstal yang Anda dapat menyeberangi lahar.

    Untuk men-debug kasus seperti itu, Anda tidak perlu membuat model penuh tank dan membangun peta lengkap dengan lokasi lava tambahan - cukup pikirkan komponen minimum yang diperlukan pada tangki dan tambahkan entitas ke mesin game untuk diuji. Semua ini terdengar jelas, tetapi pada kenyataannya Anda menemukan kelas TheTank yang di konstruktor meminta daftar parameter: kaliber, kecepatan, sprite, kecepatan tembakan, nama kru, dll. meskipun ini tidak perlu untuk menguji persimpangan lava.
  2. Juga, mengikuti contoh dari paragraf sebelumnya, kami mencatat fleksibilitas yang sangat besar , karena dengan pendekatan ini lebih mudah untuk menambah dan menghapus fitur.

    Contoh nyata. Skenario permainan dari promo kedua kami adalah bahwa pengguna, setelah mengeksekusi lagu selama ~ 2 menit, menabrak garis finish, dan permainan memulai hitungan mundur dengan memulai kembali level lagi, tetapi dengan menyimpan poin, sehingga memberikan istirahat bagi pemain.

    Beberapa hari sebelum rilis yang diharapkan datang tugas "untuk menghapus garis finish dan membuat laporan selama setengah jam bermain terus menerus dengan rintangan dan lagu looping." Perubahan global, tapi itu sangat mudah dilakukan - itu cukup untuk menghilangkan esensi dari garis finish dan menambahkan sistem referensi untuk akhir permainan.
  3. Semua perkembangan mudah untuk diuji . Mengetahui bahwa data ada di tempat pertama, Anda dapat mensimulasikan kasus uji, menjalankannya dan melihat hasilnya.
  4. Di masa depan, untuk memvalidasi keadaan game , Anda dapat menghubungkan server ke proses game. Dia akan menjalankan kode yang sama dengan input klien yang sama dan membandingkan hasilnya dengan hasil pada klien. Jika data tidak konvergen, maka klien adalah penipu atau kami memiliki kesalahan dalam permainan.


ECS di dunia besar gamedev


Perusahaan-perusahaan besar seperti Unity, Epic atau Crytek menggunakan templat ini dalam kerangka mereka untuk menyediakan alat dengan satu ton fitur kepada pengembang. Saya menyarankan Anda untuk melihat laporan tentang bagaimana logika gameplay diimplementasikan di Overwatch

Untuk pemahaman yang lebih baik, saya membuat contoh kecil di github .
Terima kasih atas perhatian anda!

Juga pada topik:


Source: https://habr.com/ru/post/id482374/


All Articles