Saya punya pertanyaan tentang arsitektur game: Apa cara terbaik untuk memiliki komponen yang berbeda berkomunikasi satu sama lain?
Saya benar-benar minta maaf jika pertanyaan ini telah ditanyakan sejuta kali, tetapi saya tidak dapat menemukan apa pun dengan informasi yang saya cari.
Saya telah mencoba untuk membangun game dari awal (C ++ jika itu penting) dan telah mengamati beberapa perangkat lunak game open source untuk inspirasi (Super Maryo Chronicles, OpenTTD dan lainnya). Saya perhatikan bahwa banyak dari desain game ini menggunakan instance global dan / atau lajang di semua tempat (untuk hal-hal seperti render antrian, manajer entitas, manajer video, dan sebagainya). Saya mencoba menghindari contoh global dan lajang dan membangun sebuah mesin yang longgar mungkin, tapi saya memukul beberapa kendala yang berutang kepada pengalaman saya dalam desain yang efektif. (Bagian dari motivasi untuk proyek ini adalah untuk mengatasi ini :))
Saya telah membangun desain di mana saya memiliki satu GameCore
objek utama yang memiliki anggota yang analog dengan contoh global yang saya lihat dalam proyek lain (yaitu, ia memiliki manajer input, manajer video, GameStage
objek yang mengontrol semua entitas dan permainan game untuk tahap apa pun yang sedang dimuat, dll). Masalahnya adalah karena semuanya terpusat pada GameCore
objek, saya tidak memiliki cara mudah untuk komponen yang berbeda untuk berkomunikasi satu sama lain.
Melihat Super Maryo Chronicles, misalnya, setiap kali komponen permainan perlu berkomunikasi dengan komponen lain (yaitu, objek musuh ingin menambahkan dirinya ke antrian render untuk ditarik dalam tahap render), itu hanya berbicara kepada contoh global.
Bagi saya, saya harus membuat objek permainan saya meneruskan informasi yang relevan kembali ke GameCore
objek tersebut, sehingga GameCore
objek tersebut dapat meneruskan informasi itu ke komponen lain dari sistem yang membutuhkannya (yaitu: untuk situasi di atas, setiap objek musuh akan meneruskan informasi render mereka kembali ke GameStage
objek, yang akan mengumpulkan semuanya dan meneruskannya kembali GameCore
, yang pada gilirannya akan meneruskannya ke pengelola video untuk rendering). Ini terasa seperti desain yang benar-benar mengerikan, dan saya mencoba memikirkan resolusi untuk ini. Pikiranku tentang kemungkinan desain:
- Mesin virtual global (desain Super Maryo Chronicles, OpenTTD, dll)
- Memiliki
GameCore
objek bertindak sebagai perantara di mana semua objek berkomunikasi (desain saat ini dijelaskan di atas) - Berikan pointer komponen ke semua komponen lain yang perlu mereka bicarakan (yaitu, dalam contoh Maryo di atas, kelas musuh akan memiliki pointer ke objek video yang perlu diajak bicara)
- Break game menjadi subsistem - Misalnya, memiliki objek manajer di
GameCore
objek yang menangani komunikasi antara objek di subsistem mereka - (Pilihan lain? ....)
Saya membayangkan opsi 4 di atas untuk menjadi solusi terbaik, tetapi saya mengalami masalah dalam mendesainnya ... mungkin karena saya telah berpikir dalam hal desain yang saya lihat menggunakan global. Rasanya seperti saya mengambil masalah yang sama yang ada dalam desain saya saat ini dan mereplikasi di setiap subsistem, hanya pada skala yang lebih kecil. Sebagai contoh, GameStage
objek yang dijelaskan di atas agak merupakan upaya ini, tetapi GameCore
objek masih terlibat dalam proses.
Adakah yang bisa menawarkan saran desain di sini?
Terima kasih!
sumber
Jawaban:
Sesuatu yang kami gunakan dalam game kami untuk mengatur data global kami adalah pola desain ServiceLocator . Keuntungan dari pola ini dibandingkan dengan pola Singleton adalah bahwa implementasi data global Anda dapat berubah selama runtime aplikasi. Juga, objek global Anda dapat diubah selama runtime juga. Keuntungan lain adalah lebih mudah untuk mengatur urutan inisialisasi objek global Anda, yang sangat penting terutama di C ++.
mis. (kode C # yang dapat dengan mudah diterjemahkan ke C ++ atau Java)
Katakanlah Anda memiliki antarmuka rendering backend yang memiliki beberapa operasi umum untuk rendering barang.
Dan Anda memiliki implementasi backend render default
Dalam beberapa desain tampaknya sah untuk dapat mengakses backend render secara global. Dalam pola Singleton itu berarti bahwa setiap implementasi IRenderBackend harus diimplementasikan sebagai instance global yang unik. Tetapi menggunakan pola ServiceLocator tidak memerlukan ini.
Begini caranya:
Agar dapat mengakses objek global Anda, Anda perlu mengintensifkannya terlebih dahulu.
Hanya untuk mendemonstrasikan bagaimana implementasi dapat bervariasi selama runtime, katakanlah game Anda memiliki minigame tempat rendering isometrik dan Anda menerapkan IsometrikRenderBackend .
Saat Anda bertransisi dari kondisi saat ini ke kondisi minigame, Anda hanya perlu mengubah backend render global yang disediakan oleh pencari layanan.
Keuntungan lain adalah Anda juga dapat menggunakan layanan null. Sebagai contoh, jika kita memiliki ISoundManager layanan dan pengguna ingin menonaktifkan suara, kita hanya bisa menerapkan NullSoundManager yang tidak apa-apa bila metode yang disebut, sehingga dengan menetapkan ServiceLocator 's objek layanan ke NullSoundManager objek kita bisa mencapai hasil ini dengan hampir tidak ada jumlah pekerjaan.
Untuk meringkas, kadang-kadang mungkin tidak mungkin untuk menghilangkan data global tetapi itu tidak berarti bahwa Anda tidak dapat mengaturnya dengan benar dan dengan cara yang berorientasi objek.
sumber
std::unique_ptr<ISomeService>
.Ada banyak cara untuk merancang mesin game dan itu semua bermuara pada preferensi.
Untuk mendapatkan dasar-dasar keluar dari jalan, beberapa pengembang lebih suka untuk mendesainnya seperti piramida di mana ada beberapa kelas inti yang sering disebut sebagai kelas kernel, inti atau kerangka kerja yang menciptakan, memiliki, dan menginisialisasi serangkaian subsistem seperti seperti audio, grafik, jaringan, fisika, AI, dan tugas, entitas, dan manajemen sumber daya. Secara umum, subsistem ini dihadapkan kepada Anda oleh kelas kerangka kerja ini dan biasanya Anda akan meneruskan kelas kerangka kerja ini ke kelas Anda sendiri sebagai argumen konstruktor yang sesuai.
Saya yakin Anda berada di jalur yang benar dengan memikirkan pilihan # 4.
Perlu diingat ketika berbicara tentang komunikasi itu sendiri, itu tidak selalu harus menyiratkan panggilan fungsi langsung itu sendiri. Ada banyak cara tidak langsung komunikasi dapat terjadi, apakah itu melalui beberapa metode tidak langsung menggunakan
Signal and Slots
atau menggunakanMessages
.Terkadang dalam gim, penting untuk memungkinkan tindakan terjadi secara serempak agar loop game kami bergerak secepat mungkin sehingga frame rate cairan ke mata telanjang. Pemain tidak suka adegan lambat dan berombak dan jadi kami harus menemukan cara untuk menjaga hal-hal mengalir untuk mereka tetapi tetap mengalir logika tetapi di cek dan dipesan juga. Sementara operasi asinkron mengambil tempatnya, mereka juga bukan jawaban untuk setiap operasi.
Ketahuilah bahwa Anda akan memiliki gabungan komunikasi sinkron & asinkron. Pilih yang sesuai, tetapi ketahuilah bahwa Anda harus mendukung kedua gaya di antara subsistem Anda. Merancang dukungan untuk keduanya akan membantu Anda di masa depan.
sumber
Anda hanya perlu memastikan bahwa tidak ada dependensi terbalik atau siklus. Misalnya, jika Anda memiliki kelas
Core
, dan iniCore
memilikiLevel
, danLevel
memiliki daftarEntity
, maka pohon dependensi akan terlihat seperti:Jadi, mengingat pohon dependensi awal ini, Anda seharusnya tidak pernah
Entity
bergantung padaLevel
atauCore
, danLevel
seharusnya tidak pernah bergantung padaCore
. Jika salah satuLevel
atauEntity
perlu memiliki akses ke data yang lebih tinggi di pohon dependensi, itu harus dilewatkan sebagai parameter dengan referensi.Pertimbangkan kode berikut (C ++):
Dengan menggunakan teknik ini, Anda dapat melihat bahwa masing-masing
Entity
memiliki akses keLevel
, danLevel
memiliki akses keCore
. Perhatikan bahwa masing-masingEntity
menyimpan referensi keLevel
memori yang sama , membuang-buang. Setelah memperhatikan ini, Anda harus mempertanyakan apakah masingEntity
- masing benar - benar membutuhkan akses ke InternetLevel
.Dalam pengalaman saya, ada baik A) Solusi yang sangat jelas untuk menghindari ketergantungan terbalik, atau B) Tidak ada cara yang mungkin untuk menghindari contoh global dan lajang.
sumber
Jadi, pada dasarnya, Anda ingin menghindari negara yang bisa berubah global ? Anda bisa menjadikannya lokal, tidak berubah, atau tidak sama sekali. Latter paling efisien dan fleksibel, imo. Ini dikenal sebagai persembunyian iplementation.
sumber
Pertanyaannya sebenarnya adalah bagaimana mengurangi kopling tanpa mengorbankan kinerja. Semua objek global (layanan) biasanya membentuk semacam konteks yang bisa berubah selama waktu permainan. Dalam pengertian ini, pola pencari lokasi menyebarkan bagian-bagian yang berbeda dari konteks ke bagian aplikasi yang berbeda, yang mungkin atau mungkin tidak seperti yang Anda inginkan. Pendekatan dunia nyata lainnya adalah dengan mendeklarasikan struktur seperti ini:
Dan menyebarkannya sebagai penunjuk mentah yang tidak memiliki
sEnvironment*
. Di sini pointer menunjuk ke antarmuka sehingga kopling berkurang dengan cara yang serupa dibandingkan dengan pencari lokasi. Namun, semua layanan berada di satu tempat (yang mungkin atau mungkin tidak baik). Ini hanyalah pendekatan lain.sumber