Saya sedang mengerjakan game 2D di mana Anda dapat bergerak ke atas, bawah, kiri dan kanan. Saya memiliki dua objek logika permainan:
- Pemain: Memiliki posisi relatif terhadap dunia
- Dunia: Menggambar peta dan pemain
Sejauh ini, Dunia tergantung pada Player (yaitu memiliki referensi untuk itu), membutuhkan posisinya untuk mencari tahu di mana menggambar karakter pemain, dan bagian mana dari peta untuk menggambar.
Sekarang saya ingin menambahkan deteksi tabrakan untuk membuatnya mustahil bagi pemain untuk bergerak menembus dinding.
Cara paling sederhana yang bisa saya pikirkan adalah meminta Pemain bertanya kepada Dunia apakah gerakan yang dimaksud itu mungkin. Tapi itu akan memperkenalkan ketergantungan melingkar antara Player dan Dunia (yaitu masing-masing memegang referensi ke yang lain), yang tampaknya layak dihindari. Satu-satunya cara saya datang adalah untuk memiliki Dunia memindahkan Pemain , tetapi saya menemukan itu agak tidak intuitif.
Apa pilihan terbaik saya? Atau apakah menghindari ketergantungan sirkular tidak sepadan?
Jawaban:
Dunia seharusnya tidak menggambar dirinya sendiri; Renderer harus menggambar Dunia. Pemain seharusnya tidak menggambar sendiri; Renderer harus menarik Player relatif ke Dunia.
Pemain harus bertanya kepada Dunia tentang deteksi tabrakan; atau mungkin tabrakan harus ditangani oleh kelas terpisah yang akan memeriksa deteksi tabrakan tidak hanya terhadap dunia statis tetapi juga terhadap aktor lain.
Saya pikir Dunia mungkin seharusnya tidak menyadari Pemain sama sekali; itu harus menjadi primitif tingkat rendah bukan objek dewa. Pemain mungkin perlu memanggil beberapa metode Dunia, mungkin secara tidak langsung (deteksi tabrakan, atau memeriksa objek interaktif, dll).
sumber
Renderer
semacam itu diperlukan, tetapi itu tidak berarti logika untuk bagaimana setiap hal diberikan ditangani olehRenderer
, setiap hal yang perlu ditarik mungkin harus mewarisi dari antarmuka umum sepertiIDrawable
atauIRenderable
(atau antarmuka setara dalam bahasa apa pun yang Anda gunakan). Dunia bisa menjadiRenderer
, saya kira, tapi sepertinya itu akan melampaui tanggung jawabnya, terutama jika sudah menjadiIRenderable
dirinya sendiri.Inilah cara mesin rendering tipikal menangani hal-hal ini:
Ada perbedaan mendasar antara di mana suatu objek berada di ruang dan bagaimana objek itu ditarik.
Menggambar suatu objek
Anda biasanya memiliki kelas Renderer yang melakukan ini. Ini hanya membutuhkan objek (Model) dan menarik di layar. Itu dapat memiliki metode seperti drawSprite (Sprite), drawLine (..), drawModel (Model), apa pun yang Anda butuhkan. Ini Renderer jadi seharusnya melakukan semua hal ini. Ia juga menggunakan API apa pun yang Anda miliki di bawahnya sehingga Anda dapat memiliki misalnya penyaji yang menggunakan OpenGL dan yang menggunakan DirectX. Jika Anda ingin port game Anda ke platform lain, Anda cukup menulis renderer baru dan menggunakannya. Itu "itu" mudah.
Memindahkan objek
Setiap objek dilampirkan ke sesuatu yang kita suka menyebutnya sebagai SceneNode . Anda mencapai ini melalui komposisi. SceneNode berisi objek. Itu dia. Apa itu SceneNode? Ini adalah kelas sederhana yang terdiri dari semua transformasi (posisi, rotasi, skala) dari suatu objek (biasanya relatif terhadap SceneNode lain) bersama dengan objek yang sebenarnya.
Mengelola objek
Bagaimana SceneNodes dikelola? Melalui SceneManager . Kelas ini membuat dan melacak setiap SceneNode di adegan Anda. Anda dapat menanyakannya untuk SceneNode tertentu (biasanya diidentifikasi dengan nama string seperti "Player" atau "Table") atau daftar semua node.
Menggambar dunia
Ini seharusnya sudah cukup jelas sekarang. Cukup berjalan melewati setiap SceneNode dalam adegan dan minta Renderer menggambarnya di tempat yang tepat. Anda bisa menggambarnya di tempat yang tepat dengan meminta renderer menyimpan transformasi suatu objek sebelum membuatnya.
Deteksi Tabrakan
Ini tidak selalu sepele. Biasanya Anda dapat menanyakan adegan tentang objek apa yang ada pada titik tertentu dalam ruang, atau objek apa yang akan disilangkan oleh sinar. Dengan cara ini Anda dapat membuat sinar dari pemain Anda ke arah gerakan dan bertanya kepada manajer adegan apa objek pertama yang berpotongan sinar. Anda kemudian dapat memilih untuk memindahkan pemain ke posisi baru, memindahkannya dengan jumlah yang lebih kecil (untuk membuatnya di sebelah objek bertabrakan) atau tidak memindahkannya sama sekali. Pastikan kueri ini ditangani oleh kelas yang terpisah. Mereka harus meminta SceneManager daftar SceneNodes, tetapi itu tugas lain untuk menentukan apakah SceneNode mencakup titik di ruang angkasa atau berpotongan dengan sinar. Ingat bahwa SceneManager hanya membuat dan menyimpan node.
Jadi, apa pemainnya, dan apa dunia ini?
Player bisa berupa kelas yang berisi SceneNode, yang pada gilirannya berisi model yang akan dirender. Anda memindahkan pemain dengan mengubah posisi simpul adegan. Dunia hanyalah sebuah instance dari SceneManager. Ini berisi semua objek (melalui SceneNodes). Anda menangani deteksi tabrakan dengan membuat pertanyaan tentang keadaan saat ini dari pemandangan.
Ini jauh dari deskripsi lengkap atau akurat tentang apa yang terjadi di sebagian besar mesin, tetapi ini akan membantu Anda memahami dasar-dasarnya dan mengapa penting untuk menghormati prinsip-prinsip OOP yang digarisbawahi oleh SOLID . Jangan menyerah pada gagasan bahwa terlalu sulit untuk merestrukturisasi kode Anda atau itu tidak akan membantu Anda. Anda akan menang lebih banyak di masa depan dengan merancang kode Anda dengan cermat.
sumber
Mengapa Anda ingin menghindarinya? Ketergantungan melingkar harus dihindari jika Anda ingin membuat kelas yang dapat digunakan kembali. Tetapi Player bukanlah kelas yang perlu digunakan kembali sama sekali. Apakah Anda ingin menggunakan Player tanpa dunia? Mungkin tidak.
Ingat bahwa kelas tidak lebih dari kumpulan fungsi. Pertanyaannya adalah bagaimana seseorang membagi fungsionalitasnya. Lakukan apa yang perlu Anda lakukan. Jika Anda membutuhkan dekadensi melingkar, maka jadilah itu. (Omong-omong berlaku untuk semua fitur OOP. Kode hal-hal dengan cara yang melayani tujuan, jangan hanya mengikuti paradigma secara membabi buta.)
Sunting
Oke, untuk menjawab pertanyaan: Anda dapat menghindari bahwa Pemain perlu mengetahui Dunia untuk pemeriksaan tabrakan dengan menggunakan panggilan balik:
Jenis fisika yang telah Anda jelaskan dalam pertanyaan dapat ditangani oleh dunia jika Anda mengekspos kecepatan entitas:
Namun perhatikan bahwa Anda mungkin akan memerlukan ketergantungan pada dunia cepat atau lambat, kapan pun Anda membutuhkan fungsionalitas Dunia: Anda ingin tahu di mana musuh terdekat? Anda ingin tahu seberapa jauh langkan berikutnya? Ketergantungan itu.
sumber
render(World)
. Perdebatan seputar apakah semua kode harus dijejalkan dalam satu kelas, atau apakah kode harus dibagi menjadi unit logis dan fungsional, yang kemudian lebih mudah untuk mempertahankan, memperluas, dan mengelola. BTW, semoga sukses menggunakan kembali manajer komponen tersebut, mesin fisika, dan manajer input, semuanya dengan cerdik dibedakan dan sepenuhnya digabungkan.Desain Anda saat ini tampaknya bertentangan dengan prinsip pertama desain SOLID .
Prinsip pertama ini, yang disebut "prinsip tanggung jawab tunggal", umumnya merupakan pedoman yang baik untuk diikuti agar tidak membuat objek monolitik, melakukan segala sesuatu yang akan selalu merusak desain Anda.
Untuk melakukan konkret,
World
objek Anda bertanggung jawab untuk memperbarui dan menahan status game, dan untuk menggambar semuanya.Bagaimana jika kode rendering Anda berubah / harus diubah? Mengapa Anda harus memperbarui kedua kelas yang sebenarnya tidak ada hubungannya dengan rendering? Seperti yang telah dikatakan Liosan, Anda harus memiliki
Renderer
.Sekarang, untuk menjawab pertanyaan Anda yang sebenarnya ...
Ada banyak cara untuk melakukan ini, dan ini hanya satu cara untuk memisahkan:
Object
di mana pemain berada, namun, tetapi itu tidak bergantung pada kelas pemain (gunakan warisan untuk mencapai ini).InputManager
.Renderer
menarik semua benda.sumber
health
yang hanya dimiliki instancePlayer
ini).Pemain harus bertanya kepada Dunia tentang hal-hal seperti deteksi tabrakan. Cara untuk menghindari ketergantungan melingkar adalah tidak memiliki Dunia memiliki ketergantungan pada Player. Dunia perlu tahu di mana gambar itu sendiri: Anda mungkin ingin yang diabstraksi lebih jauh, mungkin dengan referensi ke objek Kamera yang pada gilirannya dapat memegang referensi ke Entitas untuk dilacak.
Apa yang ingin Anda hindari dalam hal referensi melingkar tidak begitu banyak memegang referensi satu sama lain, tetapi lebih merujuk satu sama lain secara eksplisit dalam kode.
sumber
Setiap kali dua jenis objek yang berbeda dapat saling bertanya. Mereka akan saling bergantung karena mereka perlu memiliki referensi ke yang lain untuk memanggil metode-metodenya.
Anda dapat menghindari ketergantungan sirkuler dengan meminta Dunia meminta Pemain, tetapi Pemain tidak dapat menanyakan Dunia, atau sebaliknya. Dengan cara ini Dunia memiliki referensi ke Pemain tetapi pemain tidak perlu referensi ke Dunia. Atau sebaliknya. Tetapi ini tidak akan menyelesaikan masalah, karena Dunia perlu bertanya kepada para pemain apakah mereka memiliki sesuatu untuk ditanyakan, dan memberi tahu mereka dalam panggilan berikutnya ...
Jadi Anda tidak dapat benar-benar mengatasi "masalah" ini dan saya pikir tidak perlu khawatir tentang itu. Jaga agar desainnya bodoh tetap sederhana selama Anda bisa.
sumber
Melucuti detail tentang pemain dan dunia, Anda memiliki kasus sederhana yaitu tidak ingin memperkenalkan ketergantungan sirkuler antara dua objek (yang tergantung pada bahasa Anda, mungkin tidak masalah, lihat tautan dalam komentar Fuhrmanator). Setidaknya ada dua solusi struktural yang sangat sederhana yang akan berlaku untuk ini dan masalah serupa:
1) Perkenalkan tunggal pola ke dalam kelas dunia Anda . Ini akan memungkinkan pemain (dan setiap objek lainnya) untuk dengan mudah menemukan objek dunia tanpa pencarian mahal atau tautan yang ditahan secara permanen. Inti dari pola ini adalah hanya bahwa kelas memiliki referensi statis untuk satu-satunya contoh dari kelas itu, yang ditetapkan pada instantiasi objek dan dihapus pada penghapusan itu.
Bergantung pada bahasa pengembangan Anda dan kompleksitas yang Anda inginkan, Anda dapat dengan mudah mengimplementasikan ini sebagai superclass atau antarmuka dan menggunakannya kembali untuk banyak kelas utama yang tidak Anda harapkan memiliki lebih dari satu di proyek Anda.
2) Jika bahasa yang Anda kembangkan mendukungnya (banyak bahasa), gunakan Referensi yang Lemah . Ini adalah referensi yang tidak mempengaruhi hal-hal seperti pengumpulan sampah. Sangat berguna dalam kasus-kasus ini, pastikan untuk tidak membuat asumsi tentang apakah objek yang Anda referensi lemah masih ada.
Dalam kasus khusus Anda, Pemain Anda dapat memegang referensi yang lemah ke dunia. Manfaat dari ini (seperti dengan singleton) adalah bahwa Anda tidak perlu pergi mencari objek dunia entah bagaimana setiap frame, atau memiliki referensi permanen yang akan menghambat proses yang dipengaruhi oleh referensi melingkar seperti pengumpulan sampah.
sumber
Seperti yang dikatakan orang lain, saya pikir Anda
World
melakukan satu hal terlalu banyak: ia mencoba untuk keduanya mengandung permainanMap
(yang harus menjadi entitas yang berbeda) dan menjadiRenderer
sekaligus.Jadi buat objek baru (disebut
GameMap
, mungkin), dan simpan data level peta di dalamnya. Tulis fungsi di dalamnya yang berinteraksi dengan peta saat ini.Maka Anda juga membutuhkan
Renderer
objek. Anda bisa menjadikanRenderer
objek ini benda yang berisiGameMap
danPlayer
(jugaEnemies
), dan juga menggambarnya.sumber
Anda dapat menghindari dependensi melingkar dengan tidak menambahkan variabel sebagai anggota. Gunakan fungsi CurrentWorld () statis untuk pemain atau sesuatu seperti itu. Namun, jangan menciptakan antarmuka yang berbeda dari yang diterapkan di Dunia, ini sama sekali tidak perlu.
Dimungkinkan juga untuk menghancurkan referensi sebelum / saat menghancurkan objek pemain untuk secara efektif menghentikan masalah yang disebabkan oleh referensi melingkar.
sumber