Bagaimana cara menghindari ketergantungan melingkar antara Player dan Dunia?


60

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?


4
Menurut Anda mengapa ketergantungan melingkar adalah hal yang buruk? stackoverflow.com/questions/1897537/…
Fuhrmanator

@ Fuhrmanator Saya tidak berpikir mereka umumnya hal yang buruk, tapi saya harus membuat hal-hal yang sedikit lebih rumit dalam kode saya untuk memperkenalkannya.
futlib

Saya marah posting tentang diskusi kecil kami, tidak ada yang baru sekalipun: yannbane.com/2012/11/… ...
jcora

Jawaban:


61

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).


25
@ snake5 - Ada perbedaan antara "bisa" dan "harus". Apa pun bisa menggambar apa pun - tetapi ketika Anda perlu mengubah kode yang berkaitan dengan menggambar, jauh lebih mudah untuk pergi ke kelas "Renderer" daripada mencari "Apa pun" yang menggambar. "terobsesi pada kompartementalisasi" adalah kata lain untuk "kohesi".
Nate

16
@ Mr.Beast, tidak, dia tidak. Dia menganjurkan desain yang bagus. Menjejalkan segala sesuatu dalam satu kesalahan kelas tidak masuk akal.
jcora

23
Wah, saya tidak berpikir itu akan memicu reaksi seperti itu :) Saya tidak punya apa-apa untuk ditambahkan ke jawaban, tetapi saya bisa menjelaskan mengapa saya memberikannya - karena saya pikir itu lebih sederhana. Tidak 'pantas' atau 'benar'. Saya tidak ingin itu terdengar seperti itu. Ini lebih mudah bagi saya karena jika saya menemukan diri saya menangani kelas dengan tanggung jawab terlalu banyak, pemisahan lebih cepat daripada memaksa kode yang ada dapat dibaca. Saya suka kode dalam potongan yang bisa saya mengerti, dan refactor sebagai reaksi terhadap masalah seperti yang dialami @futlib.
Liosan

12
@ snake5 Mengatakan menambahkan lebih banyak kelas menambahkan overhead untuk programmer sering benar-benar salah dalam pengalaman saya. Menurut pendapat saya 10x100 kelas garis dengan nama informatif dan tanggung jawab yang jelas lebih mudah dibaca dan lebih murah untuk programmer daripada kelas dewa 1000 garis tunggal.
Martin

7
Sebagai catatan tentang apa yang menarik apa, Renderersemacam itu diperlukan, tetapi itu tidak berarti logika untuk bagaimana setiap hal diberikan ditangani oleh Renderer, setiap hal yang perlu ditarik mungkin harus mewarisi dari antarmuka umum seperti IDrawableatau IRenderable(atau antarmuka setara dalam bahasa apa pun yang Anda gunakan). Dunia bisa menjadi Renderer, saya kira, tapi sepertinya itu akan melampaui tanggung jawabnya, terutama jika sudah menjadi IRenderabledirinya sendiri.
zzzzBov

35

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.


+1 - Saya menemukan diri saya membangun sistem gim saya sesuatu seperti ini, dan ternyata cukup fleksibel.
Cypher

+1, jawaban yang bagus. Lebih konkret dan to the point daripada saya sendiri.
jcora

+1, saya belajar banyak dari jawaban ini & bahkan memiliki akhir yang menginspirasi. Terima kasih @rootlocus
joslinm

16

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:

World::checkForCollisions()
{
  [...]
  foreach(entityA in entityList)
    foreach(entityB in entityList)
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
}

Player::onCollision(other)
{
  [... react on the collision ...]
}

Jenis fisika yang telah Anda jelaskan dalam pertanyaan dapat ditangani oleh dunia jika Anda mengekspos kecepatan entitas:

World::calculatePhysics()
{ 
  foreach(entityA in entityList)
    foreach(entityB in entityList)
    {
      [... move entityA according to its velocity as far as possible ...]
      if([... entityA has collided with the world ...])
         entityA.onWorldCollision();
      [... calculate the movement of entityB in order to know if A has collided with B ...]
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
    }
}

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.


4
+1 Ketergantungan melingkar sebenarnya tidak menjadi masalah di sini. Pada tahap ini tidak ada alasan untuk khawatir tentang itu. Jika gim tumbuh dan kodenya matang, mungkin akan menjadi ide yang bagus untuk refactor kelas Pemain dan Dunia di sub-kelas, memiliki sistem berbasis komponen yang tepat, kelas untuk penanganan input, mungkin Render, dll. Tapi untuk sebuah awal, tidak ada masalah.
Laurent Couvidou

4
-1, itu jelas bukan satu-satunya alasan untuk tidak memperkenalkan dependensi melingkar. Dengan tidak memperkenalkannya, Anda membuat sistem Anda lebih mudah diperluas dan diubah.
jcora

4
@Bane Kamu tidak bisa kode apa pun tanpa lem itu. Perbedaannya adalah seberapa banyak tipuan yang Anda tambahkan. Jika Anda memiliki Game kelas -> Dunia -> Entitas atau jika Anda memiliki Game kelas -> Dunia, SoundManager, InputManager, PhysicsEngine, ComponentManager. Itu membuat hal-hal menjadi kurang mudah dibaca karena semua overhead (sintaksis) dan kompleksitas yang tersirat. Dan pada satu titik Anda akan memerlukan komponen untuk berinteraksi satu sama lain. Dan itulah titik di mana satu kelas lem membuat segalanya lebih mudah daripada segalanya dibagi antara banyak kelas.
API-Beast

3
Tidak, Anda sedang memindahkan tiang gawang. Tentu saja ada sesuatu yang harus dihubungi 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.
jcora

1
@Bane Ada cara lain untuk membagi hal-hal menjadi potongan logis daripada memperkenalkan kelas baru, btw. Anda juga dapat menambahkan fungsi baru atau membagi file Anda menjadi beberapa bagian yang dipisahkan oleh blok komentar. Menjaga agar tetap sederhana tidak berarti bahwa kode akan berantakan.
API-Beast

13

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, Worldobjek 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:

  1. Dunia tidak tahu apa itu pemain.
    • Itu memang memiliki daftar Objectdi mana pemain berada, namun, tetapi itu tidak bergantung pada kelas pemain (gunakan warisan untuk mencapai ini).
  2. Pemain diperbarui oleh beberapa orang InputManager.
  3. Dunia menangani gerakan dan deteksi tabrakan, menerapkan perubahan fisik yang tepat, dan mengirim pembaruan ke objek.
    • Misalnya, jika objek A dan objek B bertabrakan, dunia akan memberi tahu mereka dan kemudian mereka bisa menanganinya sendiri.
    • Dunia masih akan menangani fisika (jika desain Anda seperti itu).
    • Kemudian, kedua objek bisa melihat apakah benturan itu menarik minat mereka atau tidak. Misalnya, jika objek A adalah pemain, dan objek B adalah lonjakan, maka pemain dapat memberikan kerusakan pada dirinya sendiri.
    • Ini bisa diselesaikan dengan cara lain.
  4. The Renderermenarik semua benda.

Anda mengatakan bahwa dunia tidak tahu apa itu pemain, tetapi ia menangani deteksi tabrakan yang mungkin perlu mengetahui properti pemain, jika itu adalah salah satu objek yang bertabrakan.
Markus von Broady

Warisan, dunia harus mewaspadai beberapa jenis benda, yang bisa digambarkan secara umum. Masalahnya bukan di dunia hanya memiliki referensi ke pemain, tetapi bahwa itu mungkin bergantung padanya sebagai kelas (yaitu menggunakan bidang seperti healthyang hanya dimiliki instance Playerini).
jcora

Ah, maksud Anda dunia tidak memiliki referensi ke pemain, itu hanya memiliki berbagai objek yang mengimplementasikan antarmuka ICollidable, bersama dengan pemain jika diperlukan.
Markus von Broady

2
+1 Jawaban yang bagus. Tetapi: "tolong abaikan semua orang yang mengatakan desain perangkat lunak yang baik tidak penting". Umum. Tidak ada yang mengatakan itu.
Laurent Couvidou

2
Diedit! Sepertinya memang tidak perlu ...
jcora

1

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.


1

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.


0

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.


0

Seperti yang dikatakan orang lain, saya pikir Anda Worldmelakukan satu hal terlalu banyak: ia mencoba untuk keduanya mengandung permainan Map(yang harus menjadi entitas yang berbeda) dan menjadi Renderersekaligus.

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 Rendererobjek. Anda bisa menjadikan Rendererobjek ini benda yang berisi GameMap dan Player(juga Enemies), dan juga menggambarnya.


-6

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.


1
Aku bersamamu. OOP terlalu berlebihan. Tutorial dan pendidikan dengan cepat melompat ke OO setelah mempelajari hal-hal aliran kontrol dasar. Program OO umumnya lebih lambat dari kode prosedural, karena ada birokrasi antara objek Anda, Anda memiliki banyak akses pointer, yang menyebabkan shitload cache cache. Game Anda berfungsi tetapi sangat lambat. Game yang nyata, sangat cepat, dan kaya fitur yang menggunakan array global biasa dan fungsi yang dioptimalkan dengan tangan dan disetel dengan baik untuk semuanya untuk menghindari kesalahan cache. Yang dapat menghasilkan peningkatan kinerja sepuluh kali lipat.
Calmarius
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.