Penafian:
Menurut pendapat saya, artikel tentang arsitektur perangkat lunak seharusnya tidak dan tidak bisa ideal. Setiap solusi yang dijelaskan mungkin tidak mencakup level yang diperlukan untuk satu programmer, dan untuk programmer lain itu akan memperumit arsitektur terlalu tidak perlu. Tetapi harus memberikan solusi untuk tugas-tugas yang telah ditetapkan untuk dirinya sendiri. Dan pengalaman ini, bersama dengan semua pengetahuan lain dari seorang programmer yang mempelajari, mengatur informasi, mengasah pendatang baru, dan mengkritik dirinya sendiri dan orang lain - pengalaman ini berubah menjadi produk perangkat lunak yang sangat baik. Artikel akan beralih di antara seni dan bagian teknis. Ini adalah eksperimen kecil dan saya harap ini akan menarik.- Dengar, aku datang dengan ide permainan yang hebat! - desainer game Vasya acak-acakan, dan matanya merah. Saya masih minum kopi dan holivar di Habré untuk menghabiskan waktu sebelum berdiri. Dia menatapku penuh harap sampai aku selesai menulis dalam komentar untuk pria itu apa yang salah tentang dirinya. Vasya tahu bahwa sampai keadilan berlaku dan kebenaran tidak dipertahankan, tidak ada gunanya melanjutkan pembicaraan dengan saya. Saya menambahkan kalimat terakhir dan menatapnya.
Singkatnya - penyihir dengan mana bisa merapal mantra, dan para prajurit bisa bertarung dalam pertarungan jarak dekat dan menghabiskan stamina. Baik penyihir dan prajurit bisa bergerak. Ya, masih ada kemungkinan untuk merampok sapi, tetapi kami akan melakukannya dalam versi berikutnya, singkatnya. Apakah Anda akan menunjukkan prototipe setelah stand-up, oke?
Dia lari pada bisnis desain gimnya, dan saya membuka IDE.
Bahkan, topik "komposisi versus warisan", "masalah pisang-monyet", "masalah belah ketupat (multiple inheritance)" adalah pertanyaan umum pada wawancara dalam format yang berbeda dan untuk alasan yang baik. Penggunaan warisan yang salah dapat menyulitkan arsitektur, dan programmer yang tidak berpengalaman tidak tahu bagaimana menghadapi hal ini dan, sebagai akibatnya, mulai mengkritik OOP secara keseluruhan dan mulai menulis kode prosedural. Oleh karena itu, programmer yang berpengalaman (atau mereka yang telah membaca hal-hal cerdas di Internet) menganggap tugas mereka untuk bertanya tentang hal-hal seperti itu dalam sebuah wawancara dalam berbagai bentuk. Jawaban universal adalah "komposisi lebih baik daripada warisan, dan tidak ada nuansa abu-abu yang harus diterapkan." Mereka yang baru saja membaca setiap jawaban seperti itu akan 100% puas.
Tapi, seperti yang saya katakan di awal artikel, setiap arsitektur akan sesuai dengan proyek Anda dan jika warisan cukup untuk proyek Anda dan semua yang diperlukan untuk menyelesaikan masalah adalah membuat Monyet dengan Pisang - buatlah. Programmer kami berada dalam situasi yang sama. Tidak masuk akal untuk menolak warisan hanya karena FPS akan menertawakan Anda.
class Character { x = 0; y = 0; moveTo (x, y) { this.x = x; this.y = y; } } class Mage extends Character { mana = 100; castSpell () { this.mana--; } } class Warrior extends Character { stamina = 100; meleeHit () { this.stamina--; } }
Stand-up, seperti biasa, berlarut-larut. Saya menggoyang-goyang kursi dan menggantung di telepon sementara June Petya mencoba meyakinkan penguji bahwa ketidakmungkinan kendali cepat melalui tombol kanan mouse bukanlah bug, karena tidak ada peluang seperti yang dijelaskan di mana pun, yang berarti Anda harus menyerahkan tugas ke departemen pra-produksi. Penguji berpendapat bahwa karena bagi pengguna kontrol melalui tombol kanan tampaknya wajib, maka ini adalah bug, bukan fitur. Faktanya, sebagai satu-satunya pemain di tim kami yang memainkan permainan kami di server pertempuran, ia ingin menambahkan fitur ini sesegera mungkin, tetapi ia tahu bahwa jika Anda memasukkannya ke dalam departemen pra-produksi, mesin birokrasi akan membiarkannya dirilis tidak lebih awal dari pada 4 berbulan-bulan, dan telah menerbitkannya sebagai bug - Anda bisa mendapatkannya di build berikutnya. Manajer proyek, seperti biasanya, datang terlambat, dan orang-orang itu sangat mengutuk sehingga mereka sudah beralih ke tikar dan, mungkin, hal-hal akan segera mencapai perkelahian jika direktur studio tidak mengalami kutukan dan tidak membawa keduanya ke kantornya. Mungkin lagi untuk 300 dolar yang didenda.
Ketika saya meninggalkan ruang reli, seorang desainer game berlari ke arah saya dan dengan gembira mengatakan bahwa semua orang menyukai prototipe, mereka membawanya untuk bekerja dan sekarang ini adalah proyek baru kami selama enam bulan ke depan. Saat kami berjalan menuju meja saya, dia dengan antusias memberi tahu fitur baru apa yang akan ada dalam permainan kami. Berapa banyak mantra berbeda yang ia temukan dan, tentu saja, bahwa akan ada seorang Paladin yang bisa bertarung dan melempar sihir. Dan seluruh departemen artis sudah mengerjakan animasi baru, dan China telah menandatangani perjanjian di mana game kami akan dirilis di pasar mereka. Saya diam-diam melihat kode prototipe saya, berpikir dalam-dalam, menyoroti semuanya dan menghapusnya.
Saya percaya bahwa dari waktu ke waktu, setiap programmer, berdasarkan pengalamannya, mulai melihat masalah-masalah nyata yang mungkin dia temui. Apalagi jika Anda bekerja dalam tim dengan satu desainer game untuk waktu yang lama. Kami memiliki banyak persyaratan dan fitur baru. Dan "arsitektur" lama kita jelas tidak bisa mengatasinya.
Ketika Anda diminta tugas serupa di sebuah wawancara, mereka pasti akan mencoba untuk menangkap Anda. Mereka bisa dalam berbagai bentuk - buaya, yang bisa berenang dan berlari. Tank yang bisa menembakkan meriam atau senapan mesin dan sebagainya. Properti paling penting dari tugas-tugas tersebut adalah bahwa Anda memiliki objek yang dapat melakukan beberapa hal berbeda. Dan warisan Anda tidak dapat mengatasi dengan cara apa pun, karena tidak mungkin untuk mewarisi dari FlyingObject dan SwimmingObject. Dan objek yang berbeda dapat melakukan tindakan yang berbeda. Pada titik ini, kami meninggalkan warisan dan beralih ke komposisi:
class Character { abilities = []; addAbility (...abilities) { for (const a of abilities) { this.abilities.push(a); } return this; } getAbility (AbilityClass) { for (const a of this.abilities) { if (a instanceof AbilityClass) { return a; } } return null; } }
Setiap tindakan yang mungkin sekarang adalah kelas yang terpisah dengan statusnya sendiri, dan jika perlu, kita dapat membuat karakter unik dengan melemparkan mereka sejumlah kemampuan yang diperlukan. Sebagai contoh, sangat mudah untuk membuat pohon ajaib abadi:
createMagicTree () { return new Character().addAbility( new SpellCastAbility() ); }
Kami kehilangan warisan dan komposisi muncul sebagai gantinya. Sekarang kita membuat karakter dan daftar kemungkinan kemampuannya. Tetapi ini tidak berarti bahwa warisan selalu buruk, hanya saja dalam kasus ini tidak cocok. Cara terbaik untuk memahami apakah warisan itu benar adalah dengan menjawab pertanyaan untuk diri sendiri apa hubungan yang diwakilinya. Jika koneksi ini "is-a", artinya, Anda mengindikasikan bahwa MeleeFightAbility adalah kemampuan, maka itu sempurna. Jika koneksi dibuat hanya karena Anda ingin menambahkan tindakan dan menampilkan "has-a", maka Anda harus memikirkan komposisi.
Saya senang melihat hasil yang bagus. Ia bekerja dengan cerdas dan tanpa bug, arsitektur impian! Saya yakin itu akan melewati lebih dari satu ujian waktu dan untuk waktu yang lama kita tidak perlu menulis ulang. Saya sangat antusias tentang kode saya sehingga saya bahkan tidak memperhatikan bagaimana June Petya mendekati saya.
Jalanan sudah gelap, yang membuatnya semakin mencolok bagaimana ia bersinar dengan kebahagiaan. Rupanya, dia berhasil mendorong keluar tugas dan menyingkirkan hukuman untuk tikar ke arah rekan-rekannya, yang diumumkan minggu lalu.
"Para seniman hanya melukis animasi ilahi," dia cepat-cepat mengobrol, "Aku tidak sabar untuk membuat mereka kacau." Plus terbang yang sangat indah ketika mantra penyembuhan diterapkan. Mereka begitu hijau dan plus seperti itu!
Aku mengutuk diriku sendiri, karena aku benar-benar lupa bahwa kita masih harus mengacaukan pandangan. Sial, sepertinya harus menulis ulang arsitektur.
Dalam artikel seperti itu biasanya hanya bekerja dengan model yang dijelaskan, karena abstrak dan dewasa, dan Anda dapat memberikan "gambar untuk ditampilkan" hingga Juni juga, tidak peduli apa arsitektur yang akan ada. Namun demikian, model kami harus memberikan informasi maksimum untuk tampilan sehingga dapat melakukan tugasnya. Di GameDev, pola "Tim" biasanya digunakan untuk ini. Singkatnya - kami memiliki keadaan tanpa logika, dan setiap perubahan harus terjadi di tim yang sesuai. Ini mungkin tampak seperti komplikasi, tetapi menawarkan banyak keuntungan:
- Mereka bergabung dengan sangat baik ketika satu tim memanggil tim lainnya
- Setiap perintah, ketika dieksekusi, adalah, pada kenyataannya, suatu peristiwa yang Anda dapat berlangganan
- Kita dapat dengan mudah membuat cerita bersambung.
Misalnya, perintah kerusakan mungkin terlihat seperti ini. Saat itulah prajurit akan menggunakannya ketika dipukul dengan pedang dan penyihir ketika dipukul oleh mantra api. Sekarang, untuk kesederhanaan, saya menerapkan validasi perintah melalui pengecualian, tetapi kemudian mereka dapat ditulis ulang sebagai kode kembali.
class DealDamageCommand extends Command { constructor (target, damage) { this.target = target; this.damage = damage; } execute () { const healthAbility = this.target.getAbility(HealthAbility); if (healthAbility == null) { throw new Error('NoHealthAbility'); } const resultHealth = healthAbility.health - this.damage; healthAbility.health = Math.max( 0, resultHealth ); } }
Saya suka membuat perintah hierarkis - ketika seseorang dieksekusi, itu
melahirkan banyak anak, yang kemudian dieksekusi oleh mesin. Jadi sekarang kita memiliki kemampuan untuk menangani kerusakan, kita dapat mencoba menerapkan serangan jarak dekat
class MeleeHitCommand extends Command { constructor (source, target, damage) { this.source = source; this.target = target; this.damage = damage; } execute () { const fightAbility = this.source.getAbility(MeleeFightAbility); if (fightAbility == null) { throw new Error('NoFightAbility'); } this.addChildren([ new DealDamageCommand(this.target, fightAbility.power); ]); } }
Kedua tim ini memiliki semua yang Anda butuhkan untuk animasi kami. Penyaji dapat dengan mudah berlangganan acara dan menampilkan semua yang diinginkan artis dengan kode berikut:
async onMeleeHit (meleeHitCommand) { await view.drawMeleeHit( meleeHitCommand.source, meleeHitCommand.target ); } async onDealDamage (dealDamageCommand) { await view.showDamageNumbers( dealDamageCommand.target, dealDamageCommand.damage ); }
Saya kehilangan hitungan, yang sekali berturut-turut saya tetap bekerja sampai gelap. Sejak kecil, perkembangan permainan telah menarik saya, bagi saya itu terasa ajaib, dan bahkan sekarang, ketika saya telah melakukan ini selama bertahun-tahun, saya cemas tentang hal itu. Terlepas dari kenyataan bahwa saya belajar sebuah rahasia tentang bagaimana mereka diciptakan - saya belum kehilangan kepercayaan pada sihir. Dan keajaiban ini membuat saya duduk dengan inspirasi seperti itu di malam hari dan menulis kode saya. Vasya mendatangi saya. Dia benar-benar tidak tahu cara memprogram, tetapi membagikan sikap saya terhadap permainan.
- Di sini - desainer game meletakkan di depan saya sebuah Talmud 200 halaman yang dicetak pada lembar A4. Meskipun dokumen desain dilakukan dalam pertemuan, kami suka mencetaknya pada tahap penting untuk merasakan pekerjaan ini dalam perwujudan fisik. Saya membukanya di halaman acak dan mendapatkan daftar besar berbagai mantra yang bisa dilakukan oleh penyihir dan paladin, deskripsi efeknya, persyaratan intelijen, harga mana, dan deskripsi perkiraan untuk seniman cara menampilkannya. Bekerja selama berbulan-bulan, karena hari ini saya akan kembali bekerja.
Arsitektur kami membuatnya mudah untuk membuat kombinasi mantra yang kompleks. Hanya saja setiap mantra dapat mengembalikan daftar perintah yang perlu dilakukan selama pemeran
class CastSpellCommand extends Command { constructor (source, target, spell) { this.source = source; this.target = target; this.spell = spell; } execute () { const spellAbility = this.source.getAbility(SpellCastAbility); if (spellAbility == null) { throw new Error('NoSpellCastAbility'); } this.addChildren(new PayManaCommand(this.source, this.spell.manaCost)); this.addChildren(this.spell.getCommands(this.source, this.target)); } } class Spell { manaCost = 0; getCommands (source, target) { return []; } } class DamageSpell extends Spell { manaCost = 3; constructor (damageValue) { this.damageValue = damageValue; } getCommands (source, target) { return [ new DealDamageCommand(target, this.damageValue) ]; } } class HealSpell extends Spell { manaCost = 2; constructor (healValue) { this.healValue = healValue; } getCommands (source, target) { return [ new HealDamageCommand(target, this.healValue) ]; } } class VampireSpell extends Spell { manaCost = 5; constructor (value) { this.value = value; } getCommands (source, target) { return [ new DealDamageCommand(target, this.value), new HealDamageCommand(source, this.value) ]; } }
Satu setengah tahun kemudian
Stand-up, seperti biasa, berlarut-larut. Aku bergoyang di kursi dan menggantung di laptop sementara Middle Petya berdebat dengan penguji tentang bug yang telah dimulai. Dengan segala ketulusan, ia mencoba meyakinkan penguji bahwa kurangnya kontrol melalui tombol kanan mouse di game baru kami tidak boleh ditandai sebagai bug, karena tugas seperti itu tidak pernah berdiri dan tidak berhasil oleh desainer game atau badut. Saya punya perasaan deja vu, tetapi pesan baru dalam perselisihan itu mengganggu saya:
- Dengar - desainer game menulis - Saya punya ide bagus ...