Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Programming

Mengelakkan Bloomberg Antipattern: Pendekatan Pragmatik kepada Komposisi Entiti

by
Difficulty:IntermediateLength:LongLanguages:

Malay (Melayu) translation by Aisyah Arrafah (you can also view the original English article)

Mengatur kod permainan anda menjadi entiti berasaskan komponen, dan bukan hanya bergantung pada warisan kelas, adalah pendekatan popular dalam pembangunan permainan. Dalam tutorial ini, kami akan melihat mengapa anda boleh melakukan ini, dan menyediakan enjin permainan mudah menggunakan teknik ini.


Introductionnounpengenalanintroduction, identitymukadimahintroduction

Dalam tutorial ini, saya akan meneroka entiti permainan berasaskan komponen, melihat mengapa anda mungkin mahu menggunakannya, dan mencadangkan pendekatan pragmatik untuk mencelupkan kaki anda ke dalam air.

Kerana itu satu cerita mengenai organisasi kod dan seni bina, saya akan mula dengan menjatuhkan penafian "keluar dari penjara" yang biasa: ini hanya satu cara untuk melakukan sesuatu, itu bukan satu-satunya cara atau mungkin cara terbaik, tetapi ia mungkin berfungsi untuk anda. Secara peribadi, saya ingin mengetahui tentang seberapa banyak pendekatan yang mungkin dan kemudian lakukan apa yang sesuai dengan saya.


Preview Keputusan Akhir


Sepanjang tutorial dua bahagian ini, kami akan membuat permainan Asteroid ini. (Kod sumber penuh boleh didapati di GitHub.) Di bahagian pertama ini, kami akan memberi tumpuan kepada konsep teras dan enjin permainan umum.


Apakah Masalah Adakah Kita Menyelesaikan?

Dalam permainan seperti Asteroid, kami mungkin mempunyai beberapa jenis asas "perkara" pada skrin: peluru, asteroid, kapal pemain dan kapal musuh. Kita mungkin mahu mewakili jenis asas ini sebagai empat kelas yang berasingan, masing-masing mengandungi semua kod yang kita perlu lukis, bernyawa, bergerak dan mengawal objek itu.

Walaupun ini akan berfungsi, lebih baik mengikuti prinsip Don't Repeat Yourself (DRY) dan cuba untuk menggunakan beberapa kod antara setiap kelas -- setelah semua, kod untuk memindahkan dan melukis peluru akan menjadi sangat sama seperti, jika tidak betul-betul sama dengan, kod untuk bergerak dan menggambar asteroid atau kapal.

Jadi kita boleh refactor fungsi rendering dan pergerakan kita ke dalam kelas asas yang semuanya bermula dari. Tetapi Kapal dan EnemyShip juga perlu dapat menembak. Pada ketika ini kami dapat menambah fungsi menembak ke kelas asas, mewujudkan kelas "Raksasa Gergasi" yang boleh dilakukan pada dasarnya segala-galanya, dan pastikan asteroid dan peluru tidak pernah memanggil fungsi menembak mereka. Kelas asas ini tidak lama lagi akan menjadi sangat besar, bengkak dalam saiz setiap entiti perlu dapat melakukan perkara-perkara baru. Ini tidak semestinya salah, tetapi saya dapati kelas yang lebih kecil, lebih khusus untuk lebih mudah dijaga.

Sebagai alternatif, kita boleh menurunkan warisan mendalam dan mempunyai sesuatu seperti EnemyShip memanjangkan Kapal meluaskan ShootingEntity memanjangkan Entiti. Sekali lagi pendekatan ini tidak salah, dan juga akan berfungsi dengan baik, tetapi apabila anda menambahkan lebih banyak jenis Entiti, anda akan mendapati diri anda sentiasa menyesuaikan hierarki warisan untuk mengendalikan semua senario yang mungkin, dan anda boleh memasukkan diri ke sudut di mana Entity baru perlu mempunyai fungsi dua kelas asas berbeza, yang memerlukan banyak warisan (yang kebanyakan bahasa pengaturcaraan tidak ditawarkan).

Saya telah menggunakan pendekatan hierarki mendalam sendiri, tetapi saya lebih suka pendekatan Giant Blob, sekurang-kurangnya semua entiti mempunyai antara muka yang sama dan entiti baru boleh ditambah dengan lebih mudah (jadi bagaimana jika semua pokok anda mempunyai A* pathfinding? !)

Terdapat, bagaimanapun, cara ketiga ...


Komposisi Lebih Pewarisan

Jika kita memikirkan masalah Asteroid dari segi perkara yang mungkin perlu dilakukan, kita mungkin akan mendapat senarai seperti ini:

  • bergerak()
  • menembak()
  • takeDamage()
  • die()
  • render()

Daripada membuat hierarki pusaka rumit yang objek dapat melakukan perkara-perkara yang mana, mari modelkan masalah dari segi komponen yang dapat melakukan tindakan ini.

Sebagai contoh, kita boleh membuat kelas Kesihatan, dengan kaedah takeDamage(), sembuh() dan mati(). Kemudian apa-apa objek yang perlu dapat merosakkan dan mati boleh "menulis" contoh kelas Kesihatan -- di mana "menulis" pada dasarnya bermaksud "menyimpan rujukan kepada contohnya sendiri".

Kita boleh membuat satu lagi kelas dipanggil Lihat untuk menjaga fungsi rendering, yang dipanggil Badan untuk mengendalikan pergerakan dan satu yang dipanggil Senjata untuk mengendalikan penangkapan.

Sistem Entiti Kebanyakan didasarkan pada prinsip yang diterangkan di atas, tetapi berbeza dengan cara anda mengakses kefungsian yang terkandung dalam komponen.

Mencerminkan API

Sebagai contoh, satu pendekatan adalah untuk mencerminkan API setiap komponen dalam Entiti, jadi entiti yang boleh merosakkan akan mempunyai fungsi takeDamage() yang sendiri memanggil fungsi takeDamage() dari komponen Kesihatannya.

Anda perlu membuat antara muka yang dipanggil sesuatu seperti IHealth untuk entiti anda untuk dilaksanakan, supaya objek lain boleh mengakses fungsi takeDamage(). Ini adalah bagaimana panduan OOP Java mungkin menasihati anda untuk melakukannya.

getComponent()

Satu lagi pendekatan adalah dengan hanya menyimpan setiap komponen dalam mencari nilai utama, supaya setiap Entiti mempunyai fungsi yang dipanggil sesuatu seperti getComponent ("componentName") yang mengembalikan rujukan kepada komponen tertentu. Anda kemudian perlu membuang rujukan anda kembali kepada jenis komponen yang anda mahu - sesuatu seperti:

Ini pada asasnya bagaimana sistem entiti/tingkah laku Unity berfungsi. Ia sangat fleksibel, kerana anda boleh terus menambah jenis komponen baru tanpa mengubah kelas asas anda, atau membuat subclass atau antaramuka baru. Ia juga mungkin berguna apabila anda mahu menggunakan fail konfigurasi untuk membuat entiti tanpa mengkompilasi kod anda, tetapi saya akan meninggalkannya kepada orang lain untuk difikirkan.

Komponen Awam

Pendekatan yang saya suka adalah untuk membiarkan semua entiti memiliki harta awam bagi setiap jenis utama komponen, dan meninggalkan bidang batal jika entiti tidak mempunyai fungsi tersebut. Apabila anda mahu memanggil kaedah tertentu, anda hanya "mencapai" ke entiti untuk mendapatkan komponen dengan fungsi itu -- sebagai contoh, hubungi musuh.health.takeDamage(5) untuk menyerang musuh.

Jika anda cuba memanggil health.takeDamage() pada entiti yang tidak mempunyai komponen Kesihatan, ia akan disusun, tetapi anda akan mendapat ralat runtime yang membiarkan anda tahu anda telah melakukan sesuatu yang bodoh. Dalam amalan ini jarang terjadi, kerana ia agak jelas jenis entiti mana yang akan mempunyai komponen (contohnya, tentu saja pokok tidak mempunyai senjata!).

Beberapa penyokong OOP yang ketat mungkin berpendapat bahawa pendekatan saya memecahkan beberapa prinsip OOP, tetapi saya dapati ia berfungsi dengan baik, dan ada duluan yang sangat baik dari sejarah Adobe Flash.

Dalam ActionScript 2, kelas MovieClip mempunyai kaedah untuk menggambar grafik vektor: sebagai contoh, anda boleh memanggil myMovieClip.lineTo() untuk melukis garis. Dalam ActionScript 3, kaedah lukisan ini dipindahkan ke kelas Grafik, dan setiap MovieClip mendapat komponen Grafik, yang anda akses dengan memanggil, contohnya, myMovieClip.graphics.lineTo() dengan cara yang sama yang saya nyatakan untuk musuh.health.takeDamage(). Jika ia cukup baik untuk pereka bahasa ActionScript, itu cukup baik untuk saya.


Sistem Saya (Ringkas)

Di bawah ini saya akan memaparkan versi sistem yang sangat sederhana yang saya gunakan di semua permainan saya. Dari segi bagaimana mudah, ia adalah seperti 300 baris kod untuk ini, berbanding 6,000 untuk enjin penuh saya. Tetapi kita sebenarnya boleh melakukan banyak perkara dengan hanya 300 baris ini!

Saya telah meninggalkan kefungsian yang cukup untuk membuat permainan kerja, sambil mengekalkan kod itu secepat yang mungkin jadi lebih mudah untuk diikuti. Kod ini akan berada di ActionScript 3, tetapi struktur yang sama mungkin dilakukan di kebanyakan bahasa. Terdapat beberapa pembolehubah awam yang boleh menjadi sifat (iaitu meletakkan di belakang mendapatkan dan menetapkan fungsi accessor), tetapi kerana ini adalah lebih jelas dalam ActionScript, saya telah meninggalkannya sebagai pembolehubah awam untuk kemudahan membaca.

Antara muka IEntity

Mari kita mulakan dengan menentukan antara muka yang semua entiti akan dilaksanakan:

Semua entiti boleh melakukan tiga tindakan: anda boleh mengemas kini mereka, menjadikan mereka dan memusnahkan mereka.

Mereka masing-masing mempunyai "slot" untuk lima komponen:

  • Badan, kedudukan dan saiz pengendalian.
  • fizik, pergerakan pengendalian.
  • kesihatan, pengendalian kecederaan.
  • Senjata, pengendalian menyerang.
  • Dan akhirnya pandangan, membolehkan anda menjadikan entiti itu.

Semua komponen ini adalah pilihan dan boleh dibiarkan batal, tetapi dalam amalan kebanyakan entiti akan mempunyai sekurang-kurangnya beberapa komponen.

Sekeping pemandangan statik yang pemain tidak dapat berinteraksi dengan (mungkin pokok, contohnya), hanya memerlukan tubuh dan pandangan. Ia tidak memerlukan fizik kerana ia tidak bergerak, ia tidak memerlukan kesihatan kerana anda tidak boleh menyerangnya, dan ia tentu tidak memerlukan senjata. Kapal pemain di Asteroids, sebaliknya, memerlukan semua lima komponen, kerana ia boleh bergerak, menembak dan terluka.

Dengan mengkonfigurasi lima komponen asas ini, anda boleh membuat objek paling mudah yang anda perlukan. Kadang-kadang mereka tidak akan mencukupi, bagaimanapun, dan pada ketika itu kita boleh melanjutkan komponen asas, atau membuat tambahan yang baru -- kedua-duanya akan dibincangkan kemudian.

Seterusnya kita mempunyai dua isyarat: entitiKreasi dan dimusnahkan.

Isyarat merupakan alternatif sumber terbuka kepada peristiwa asli ActionScript yang dibuat oleh Robert Penner. Mereka benar-benar baik untuk digunakan kerana mereka membolehkan anda untuk menghantar data antara penghantar dan pendengar tanpa perlu membuat banyak kelas Acara tersuai. Untuk maklumat lanjut tentang cara menggunakannya, lihat dokumentasi.

Isyarat entitiKreasi membolehkan entiti memberitahu permainan bahawa terdapat satu lagi entiti baru yang perlu ditambah - satu contoh klasik ialah ketika sebuah senjata menghasilkan peluru. Isyarat yang dimusnahkan membolehkan permainan (dan sebarang objek mendengar lain) mengetahui bahawa entiti ini telah dimusnahkan.

Akhirnya, entiti itu mempunyai dua dependencies pilihan lain: sasaran, yang merupakan senarai entiti yang mungkin mahu menyerang, dan kumpulan, yang merupakan senarai entiti yang dimilikinya. Sebagai contoh, kapal pemain mungkin mempunyai senarai sasaran, yang akan menjadi semua musuh dalam permainan, dan mungkin milik kumpulan yang juga mengandungi pemain lain dan unit yang mesra.

Kelas Entiti

Sekarang mari kita lihat kelas Entity yang melaksanakan antara muka ini.

Ia kelihatan panjang, tetapi kebanyakannya hanyalah pengarah verbose dan fungsi setter (boo!). Bahagian penting untuk melihat adalah empat fungsi pertama: pembina, di mana kita mencipta Sinyal kita; memusnahkan(), di mana kami menghantar Isyarat yang hancur dan mengeluarkan entiti dari senarai kumpulannya; kemas kini(), di mana kami mengemaskini mana-mana komponen yang perlu bertindak setiap gelung permainan -- walaupun dalam contoh mudah ini, ini hanya komponen fizik -- dan akhirnya memberi(), di mana kita memberitahu pandangan untuk melakukan perkara itu.

Anda akan perhatikan bahawa kami tidak secara automatik menyuarakan komponen di sini dalam kelas entiti - ini kerana, seperti yang dijelaskan sebelumnya, setiap komponen adalah pilihan.

Komponen Individu

Sekarang mari kita lihat komponen-komponen satu demi satu. Pertama, komponen badan:

Semua komponen kami memerlukan rujukan kepada entiti pemiliknya, yang kami lulus kepada pembina. Tubuh itu kemudian mempunyai empat bidang mudah: kedudukan x dan y, sudut putaran, dan jejari untuk menyimpan saiznya. (Dalam contoh mudah ini, semua entiti adalah pekeliling!)

Komponen ini juga mempunyai satu kaedah: testCollision(), yang menggunakan Pythagoras untuk mengira jarak antara dua entiti, dan membandingkannya dengan radius gabungan mereka. (Maklumat lanjut di sini.)

Seterusnya mari kita lihat komponen Fizik:

Melihat fungsi kemas kini(), anda dapat melihat bahawa nilai velocityX dan velocityY ditambah ke kedudukan entiti, yang bergerak, dan halaju didarab dengan drag, yang mempunyai kesan secara perlahan memperlambat objek tersebut. Fungsi thrust() membolehkan cara cepat mempercepatkan entiti ke arah yang sedang dihadapi.

Seterusnya mari kita lihat komponen Kesihatan:

Komponen Kesihatan mempunyai fungsi yang disebut hit(), yang membolehkan entiti terluka. Apabila ini berlaku, nilai hit dikurangkan, dan mana-mana objek mendengar diberitahu dengan menghantar Isyarat yang Sakit. Jika hits kurang daripada sifar, entiti itu mati dan kami menghantar Isyarat yang mati.

Mari lihat apa yang ada di dalam komponen Senjata:

Tidak banyak di sini! Itu kerana ini benar-benar hanya kelas asas untuk senjata sebenar - seperti yang anda lihat dalam contoh Gun kemudian. Terdapat kaedah fire() yang subclass harus ditindih, tetapi di sini ia hanya mengurangkan nilai peluru.

Komponen terakhir untuk diperiksa ialah View:

Komponen ini sangat spesifik untuk Flash. Acara utama di sini adalah fungsi render(), yang mengemas kini satu kilat Flash dengan kedudukan kedudukan dan putaran badan, dan nilai alfa dan skala itu menyimpannya sendiri. Sekiranya anda ingin menggunakan sistem persembahan yang berbeza seperti copyPixels blitting atau Stage3D (atau sesungguhnya sistem yang berkaitan dengan pilihan platform yang berbeza), anda akan menyesuaikan kelas ini.

Kelas Permainan

Sekarang kita tahu apa entiti dan semua komponennya kelihatan seperti. Sebelum kita mula menggunakan enjin ini untuk membuat contoh permainan, mari kita melihat bahagian akhir enjin -- kelas Permainan yang mengawal keseluruhan sistem:

Terdapat banyak perincian pelaksanaan di sini, tapi mari kita pilih highlight.

Setiap bingkai, gelung Kelas permainan melalui semua entiti, dan memanggil kemas kini dan kaedah mereka. Dalam fungsi addEntity, kita menambah entiti baru kepada senarai entiti, mendengar Isyaratnya, dan jika ia mempunyai pandangan, tambahkan kedatangannya ke pentas.

Apabila onEntityDestroyed dicetuskan, kami mengeluarkan entiti dari senarai dan mengeluarkan sprite dari panggung. Dalam fungsi stopGame, yang hanya anda panggil jika anda ingin menamatkan permainan, kami mengeluarkan semua sprites entiti dari panggung dan menghapuskan senarai entiti dengan menetapkan panjangnya kepada sifar.


Lain kali...

Wow, kami berjaya! Itulah enjin permainan keseluruhan! Dari titik permulaan ini, kami boleh membuat banyak permainan arked 2D yang sederhana tanpa banyak kod tambahan. Dalam tutorial seterusnya, kami akan menggunakan enjin ini untuk membuat ruang gaya-Asteroids menembak-'em-up.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.