Bagaimana menerapkan penanganan pesan dengan benar dalam sistem entitas berbasis komponen?

30

Saya menerapkan varian sistem entitas yang memiliki:

  • Sebuah kelas Entity yang sedikit lebih dari satu ID yang mengikat komponen bersama-sama

  • Sekelompok kelas komponen yang tidak memiliki "komponen logika", hanya data

  • Sekelompok kelas sistem (alias "subsistem", "manajer"). Ini melakukan semua pemrosesan logika entitas. Dalam kebanyakan kasus dasar, sistem hanya beralih melalui daftar entitas yang mereka minati dan melakukan tindakan pada masing-masing

  • Sebuah MessageChannel objek kelas yang dimiliki oleh semua sistem permainan. Setiap sistem dapat berlangganan jenis pesan tertentu untuk didengarkan dan juga dapat menggunakan saluran untuk menyiarkan pesan ke sistem lain

Varian awal penanganan pesan sistem adalah sesuatu seperti ini:

  1. Jalankan pembaruan pada setiap sistem game secara berurutan
  2. Jika suatu sistem melakukan sesuatu pada suatu komponen dan tindakan itu mungkin menarik bagi sistem lain, sistem akan mengirimkan pesan yang sesuai (misalnya, suatu sistem memanggil

    messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))

    setiap kali entitas dipindahkan)

  3. Setiap sistem yang berlangganan pesan tertentu akan dipanggil metode penanganan pesan

  4. Jika suatu sistem menangani suatu peristiwa, dan logika pemrosesan peristiwa membutuhkan pesan lain untuk disiarkan, pesan tersebut segera disiarkan dan rangkaian metode pemrosesan pesan lainnya dipanggil

Varian ini OK sampai saya mulai mengoptimalkan sistem deteksi tabrakan (itu menjadi sangat lambat karena jumlah entitas meningkat). Pada awalnya itu hanya akan mengulangi setiap pasangan entitas menggunakan algoritma brute force sederhana. Lalu saya menambahkan "indeks spasial" yang memiliki kisi sel yang menyimpan entitas yang berada di dalam area sel tertentu, sehingga memungkinkan untuk melakukan pemeriksaan hanya pada entitas di sel tetangga.

Setiap kali entitas bergerak, sistem tumbukan memeriksa apakah entitas bertabrakan dengan sesuatu di posisi baru. Jika ya, tabrakan terdeteksi. Dan jika kedua entitas yang bertabrakan adalah "objek fisik" (mereka berdua memiliki komponen RigidBody dan dimaksudkan untuk mendorong satu sama lain agar tidak menempati ruang yang sama), sistem pemisahan tubuh kaku khusus meminta sistem pergerakan untuk memindahkan entitas ke beberapa posisi tertentu yang akan memisahkan mereka. Ini pada gilirannya menyebabkan sistem pergerakan untuk mengirim pesan memberitahukan tentang posisi entitas yang berubah. Sistem deteksi tabrakan dimaksudkan untuk bereaksi karena perlu memperbarui indeks spasial itu.

Dalam beberapa kasus itu menyebabkan masalah karena isi sel (daftar objek Entitas generik dalam C #) bisa dimodifikasi saat sedang diiterasi, sehingga menyebabkan pengecualian untuk dilempar oleh iterator.

Jadi ... bagaimana saya bisa mencegah sistem tabrakan terganggu sementara memeriksa tabrakan?

Tentu saja saya dapat menambahkan beberapa logika "pintar" / "rumit" yang memastikan konten sel diulangi dengan benar, tetapi saya pikir masalahnya bukan terletak pada sistem tabrakan itu sendiri (saya juga memiliki masalah serupa di sistem lain), tetapi caranya pesan ditangani saat mereka melakukan perjalanan dari sistem ke sistem. Yang saya butuhkan adalah beberapa cara untuk memastikan bahwa metode penanganan peristiwa tertentu dapat melakukan pekerjaannya tanpa gangguan.

Apa yang saya coba:

  • Antrian pesan masuk . Setiap kali beberapa sistem menyiarkan pesan, pesan tersebut akan ditambahkan ke antrian pesan sistem yang tertarik dengannya. Pesan-pesan ini diproses ketika pembaruan sistem disebut setiap frame. Masalahnya : jika sistem A menambahkan pesan ke antrian B sistem, ia berfungsi dengan baik jika sistem B dimaksudkan untuk diperbarui lebih baru daripada sistem A (dalam kerangka permainan yang sama); jika tidak maka pesan akan diproses ke frame permainan berikutnya (tidak diinginkan untuk beberapa sistem)
  • Antrian pesan keluar . Ketika suatu sistem menangani suatu peristiwa, pesan apa pun yang disiarkannya ditambahkan ke antrian pesan keluar. Pesan tidak perlu menunggu pembaruan sistem diproses: pesan akan ditangani "segera" setelah penangan pesan awal selesai berfungsi. Jika penanganan pesan menyebabkan pesan lain disiarkan, mereka juga ditambahkan ke antrian keluar, sehingga semua pesan ditangani dengan bingkai yang sama. Masalah: jika sistem entitas seumur hidup (Saya menerapkan manajemen seumur hidup entitas dengan suatu sistem) membuat entitas, ia memberi tahu beberapa sistem A dan B tentang hal itu. Sementara sistem A memproses pesan, itu menyebabkan rantai pesan yang akhirnya menyebabkan entitas yang dibuat dihancurkan (misalnya, entitas peluru dibuat tepat di mana ia bertabrakan dengan beberapa kendala, yang menyebabkan peluru hancur sendiri). Sementara rantai pesan sedang diselesaikan, sistem B tidak mendapatkan pesan pembuatan entitas. Jadi, jika sistem B juga tertarik pada pesan penghancuran entitas, ia mendapatkannya, dan hanya setelah "rantai" selesai diselesaikan, apakah ia mendapatkan pesan pembuatan entitas awal. Ini menyebabkan pesan penghancuran diabaikan, pesan penciptaan menjadi "diterima",

EDIT - JAWABAN UNTUK PERTANYAAN, KOMENTAR:

  • Siapa yang memodifikasi isi sel sementara sistem tumbukan beralih di atasnya?

Sementara sistem tumbukan sedang melakukan pemeriksaan tumbukan pada beberapa entitas dan tetangga itu, tumbukan mungkin terdeteksi dan sistem entitas akan mengirim pesan yang akan langsung bereaksi setelah oleh sistem lain. Reaksi terhadap pesan dapat menyebabkan pesan lain dibuat dan juga ditangani segera. Jadi beberapa sistem lain mungkin membuat pesan bahwa sistem tumbukan kemudian perlu diproses segera (misalnya, entitas bergerak sehingga sistem tumbukan perlu memperbarui indeks spasial itu), meskipun pemeriksaan tumbukan sebelumnya belum selesai.

  • Tidak bisakah Anda bekerja dengan antrian pesan keluar global?

Saya mencoba satu antrian global baru-baru ini. Itu menyebabkan masalah baru. Masalah: Saya memindahkan entitas tangki ke entitas dinding (tangki dikontrol dengan keyboard). Lalu saya memutuskan untuk mengubah arah tangki. Untuk memisahkan tangki dan dinding setiap bingkai, CollidingRigidBodySeparationSystem memindahkan tangki dari dinding dengan jumlah sekecil mungkin. Arah pemisahan harus berlawanan dengan arah pergerakan tangki (ketika gambar permainan dimulai, tangki harus terlihat seolah-olah tidak pernah bergerak ke dinding). Tapi arahnya menjadi kebalikan dari arah BARU, sehingga memindahkan tangki ke sisi dinding yang berbeda dari sebelumnya. Mengapa masalah terjadi: Inilah cara penanganan pesan sekarang (kode sederhana):

public void Update(int deltaTime)
{   
    m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
    while (m_messageQueue.Count > 0)
    {
        Message message = m_messageQueue.Dequeue();
        this.Broadcast(message);
    }
}

private void Broadcast(Message message)
{       
    if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
    {
        // NOTE: all IMessageListener objects here are systems.
        List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
        foreach (IMessageListener listener in messageListeners)
        {
            listener.ReceiveMessage(message);
        }
    }
}

Kode mengalir seperti ini (mari kita asumsikan itu bukan bingkai game pertama):

  1. Sistem mulai memproses TimePassedMessage
  2. InputHandingSystem mengonversi penekanan tombol ke tindakan entitas (dalam hal ini, panah kiri berubah menjadi tindakan MoveWest). Tindakan entitas disimpan dalam komponen ActionExecutor
  3. ActionExecutionSystem , sebagai reaksi terhadap tindakan entitas, menambahkan pesan MovementDirectionChangeRequestedMessage ke akhir antrian pesan
  4. MovementSystem memindahkan posisi entitas berdasarkan data komponen Velocity dan menambahkan pesan PositionChangedMessage ke akhir antrian. Gerakan ini dilakukan dengan menggunakan arah gerakan / kecepatan frame sebelumnya (katakanlah utara)
  5. Sistem berhenti memproses TimePassedMessage
  6. Sistem mulai memproses MovementDirectionChangeRequestedMessage
  7. MovementSystem mengubah kecepatan entitas / arah gerakan seperti yang diminta
  8. Sistem berhenti memproses MovementDirectionChangeRequestedMessage
  9. Sistem mulai memproses PositionChangedMessage
  10. CollisionDetectionSystem mendeteksi bahwa karena suatu entitas bergerak, ia berlari ke entitas lain (tangki masuk ke dalam dinding). Itu menambahkan CollisionOccuredMessage ke antrian
  11. Sistem berhenti memproses PositionChangedMessage
  12. Sistem mulai memproses CollisionOccuredMessage
  13. CollidingRigidBodySeparationSystem bereaksi terhadap tabrakan dengan memisahkan tangki dan dinding. Karena dindingnya statis, hanya tangki yang dipindahkan. Arah pergerakan tank digunakan sebagai indikator dari mana tangki berasal. Itu diimbangi dalam arah yang berlawanan

BUG: Ketika tangki memindahkan frame ini, ia bergerak menggunakan arah gerakan dari frame sebelumnya, tetapi ketika dipisahkan, arah gerakan dari frame INI digunakan, meskipun sudah berbeda. Itu tidak seharusnya bekerja!

Untuk mencegah bug ini, arah gerakan lama perlu disimpan di suatu tempat. Saya bisa menambahkannya ke beberapa komponen hanya untuk memperbaiki bug khusus ini, tetapi tidakkah kasus ini menunjukkan beberapa cara yang salah secara mendasar dalam menangani pesan? Mengapa sistem pemisahan harus memperhatikan arah gerakan mana yang digunakannya? Bagaimana saya bisa menyelesaikan masalah ini dengan elegan?

  • Anda mungkin ingin membaca gamadu.com/artemis untuk melihat apa yang mereka lakukan dengan Aspek, yang sisi langkah beberapa masalah yang Anda lihat.

Sebenarnya, saya sudah akrab dengan Artemis cukup lama sekarang. Menyelidiki kode sumbernya, membaca forum, dll. Tetapi saya telah melihat "Aspek" disebutkan hanya di beberapa tempat dan, sejauh yang saya mengerti, mereka pada dasarnya berarti "Sistem". Tapi aku tidak bisa melihat bagaimana sisi Artemis melangkah beberapa masalah saya. Bahkan tidak menggunakan pesan.

  • Lihat juga: "Komunikasi entitas: Antrian pesan vs Terbitkan / Berlangganan vs. Sinyal / Slot"

Saya sudah membaca semua pertanyaan gamedev.stackexchange mengenai sistem entitas. Yang ini sepertinya tidak membahas masalah yang saya hadapi. Apakah saya melewatkan sesuatu?

  • Tangani kedua kasus secara berbeda, memperbarui kisi tidak perlu bergantung pada pesan gerakan karena merupakan bagian dari sistem tumbukan

Saya tidak yakin apa yang Anda maksud. Implementasi CollisionDetectionSystem yang lebih lama hanya akan memeriksa tabrakan pada pembaruan (ketika TimePassedMessage ditangani), tetapi saya harus meminimalkan pemeriksaan sebanyak yang saya bisa karena kinerja. Jadi saya beralih ke pengecekan tabrakan ketika suatu entitas bergerak (sebagian besar entitas dalam game saya statis).

Onlainas
sumber
Ada sesuatu yang tidak jelas bagi saya. Siapa yang memodifikasi isi sel sementara sistem tumbukan beralih di atasnya?
Paul Manta
Tidak bisakah Anda bekerja dengan antrian pesan keluar global? Jadi semua pesan di sana dikirim setiap kali setelah sistem dilakukan, ini termasuk sistem penghancuran diri.
Roy T.
Jika Anda ingin mempertahankan desain yang berbelit-belit ini maka Anda harus mengikuti @RoyT. Saran, ini satu-satunya cara (tanpa pesan kompleks, berdasarkan waktu) untuk menangani masalah urutan Anda. Anda mungkin ingin membaca gamadu.com/artemis untuk melihat apa yang mereka lakukan dengan Aspek, yang sisi langkah beberapa masalah yang Anda lihat.
Patrick Hughes
2
Anda mungkin ingin mempelajari bagaimana Axum melakukannya dengan mengunduh CTP dan mengkompilasi beberapa kode - dan kemudian membalikkan hasil rekayasa ke C # menggunakan ILSpy. Pesan lewat adalah fitur penting dari bahasa model aktor dan saya yakin Microsoft tahu apa yang mereka lakukan - sehingga Anda mungkin menemukan mereka memiliki implementasi 'terbaik'.
Jonathan Dickinson

Jawaban:

12

Anda mungkin pernah mendengar tentang anti-pola objek Dewa / Gumpalan. Nah masalah Anda adalah loop God / Blob. Bermain-main dengan sistem penyampaian pesan Anda akan memberikan solusi Band-Aid terbaik dan paling tidak akan membuang waktu. Faktanya, masalah Anda tidak ada hubungannya dengan pengembangan game sama sekali. Saya telah menangkap diri saya mencoba memodifikasi koleksi sambil mengulanginya beberapa kali, dan solusinya selalu sama: subdivide, subdivide, subdivide.

Saat saya memahami kata-kata dari pertanyaan Anda, metode Anda untuk memperbarui sistem tabrakan Anda saat ini terlihat luas seperti berikut.

for each possible collision
    check for collision
    handle collision
    modify collision world to reflect change // exception happens here

Ditulis dengan jelas seperti ini, Anda dapat melihat bahwa lingkaran Anda memiliki tiga tanggung jawab, padahal seharusnya hanya satu. Untuk mengatasi masalah Anda, bagi loop Anda saat ini menjadi tiga loop terpisah yang mewakili tiga lintasan algoritmik berbeda .

for each possible collision
    check for collision, record it if a collision occurs

for each found collision
    handle collision, record the collision response (delete object, ignore, etc.)

for each collision response
    modify collision world according to response

Dengan membagi kembali loop asli Anda menjadi tiga subloop, Anda tidak lagi mencoba untuk memodifikasi koleksi yang saat ini Anda iterasi. Perhatikan juga bahwa Anda tidak melakukan lebih banyak pekerjaan daripada di loop asli Anda, dan pada kenyataannya Anda mungkin mendapatkan beberapa kemenangan cache dengan melakukan operasi yang sama berkali-kali secara berurutan.

Ada juga manfaat lebih lanjut, yaitu sekarang Anda dapat memperkenalkan paralelisme ke dalam kode Anda. Pendekatan loop-gabungan Anda pada dasarnya bersifat serial (yang pada dasarnya adalah apa yang dikatakan oleh pengecualian modifikasi bersamaan!), Karena setiap iterasi loop berpotensi membaca dan menulis ke dunia benturan Anda. Ketiga subloop yang saya sajikan di atas, bagaimanapun, semuanya membaca atau menulis, tetapi tidak keduanya. Paling tidak pass pertama, memeriksa semua kemungkinan tabrakan, telah menjadi paralel memalukan, dan tergantung pada bagaimana Anda menulis kode Anda pass kedua dan ketiga mungkin juga.

Itik jantan
sumber
Saya sangat setuju dengan ini. Saya menggunakan pendekatan yang sangat mirip ini dalam permainan saya dan saya percaya ini akan memberikan hasil dalam jangka panjang. Beginilah seharusnya sistem tabrakan (atau manajer) bekerja (saya benar-benar yakin itu mungkin untuk tidak memiliki sistem pengiriman pesan sama sekali).
Emiliano
11

Bagaimana menerapkan penanganan pesan dengan benar dalam sistem entitas berbasis komponen?

Saya akan mengatakan bahwa Anda menginginkan dua jenis pesan: Sinkron dan Asinkron. Pesan sinkron ditangani segera sementara asinkron ditangani tidak dalam bingkai tumpukan yang sama (tetapi dapat ditangani dalam bingkai permainan yang sama). Keputusan yang biasanya dibuat atas dasar "per kelas pesan", misalnya "semua pesan EnemyDied tidak sinkron".

Beberapa acara hanya ditangani jauh lebih mudah dengan salah satu cara ini. Misalnya, dalam pengalaman saya, sebuah event ObjectGetsDeletedNow - jauh lebih seksi dan callback jauh lebih sulit untuk diterapkan daripada ObjectWillBeDeletedAtEndOfFame. Kemudian lagi, penangan pesan seperti "veto" (kode yang dapat membatalkan atau mengubah tindakan tertentu saat dijalankan, seperti efek Shield memodifikasi DamageEvent ) tidak akan mudah dalam lingkungan asinkron tetapi sepotong kue di panggilan sinkron.

Asyncronous mungkin lebih efisien dalam beberapa kasus (misalnya Anda dapat melewatkan beberapa penangan acara saat objek akan dihapus nanti). Kadang-kadang sinkron lebih efisien, terutama ketika menghitung parameter untuk suatu peristiwa itu mahal dan Anda lebih suka melewatkan fungsi panggilan balik untuk mengambil parameter tertentu daripada nilai yang sudah dihitung (kalau-kalau tidak ada yang tertarik pada parameter khusus ini).

Anda telah menyebutkan masalah umum lainnya dengan sistem pesan hanya-sinkron: Menurut pengalaman saya dengan sistem pesan sinkron, salah satu kasus kesalahan dan kesedihan yang paling umum adalah perubahan daftar sambil mengulangi daftar-daftar ini.

Pikirkan tentang hal ini: Sifatnya sinkron (segera menangani semua efek setelah beberapa tindakan) dan sistem pesan (memisahkan penerima dari pengirim sehingga pengirim tidak tahu siapa yang bereaksi terhadap tindakan) bahwa Anda tidak akan dapat dengan mudah lihat loop tersebut. Apa yang saya katakan adalah: Bersiaplah untuk menangani iterasi modifikasi diri semacam ini banyak. Ini semacam "dengan desain". ;-)

bagaimana saya bisa mencegah sistem tabrakan terganggu sementara memeriksa tabrakan?

Untuk masalah khusus Anda dengan deteksi tumbukan, mungkin cukup baik untuk membuat acara tumbukan asinkron, sehingga mereka akan antri sampai manajer tumbukan selesai dan dieksekusi sebagai satu kumpulan setelahnya (atau pada beberapa titik nanti dalam bingkai). Ini adalah solusi Anda "antrian masuk".

Masalahnya: jika sistem A menambahkan pesan ke antrian B sistem, ia berfungsi dengan baik jika sistem B dimaksudkan untuk diperbarui lebih baru daripada sistem A (dalam kerangka permainan yang sama); jika tidak maka pesan akan diproses ke frame permainan berikutnya (tidak diinginkan untuk beberapa sistem)

Mudah:

while (! queue.empty ()) {queue.pop (). handle (); }

Jalankan antrian berulang-ulang sampai tidak ada pesan yang tersisa. (Jika Anda berteriak "loop tak berujung" sekarang, ingatlah bahwa Anda kemungkinan besar akan memiliki masalah ini sebagai "spam pesan" jika itu akan ditunda ke frame berikutnya. Anda dapat menegaskan () untuk sejumlah iterasi yang waras untuk mendeteksi loop tanpa akhir, jika Anda suka;))

I MI
sumber
Perhatikan bahwa saya tidak berbicara tentang "kapan" pesan asinkron ditangani. Menurut pendapat saya, itu baik-baik saja untuk memungkinkan modul deteksi tabrakan untuk menyiram pesannya setelah selesai. Anda juga dapat menganggap ini sebagai "pesan sinkron, tertunda hingga akhir loop" atau beberapa cara bagus "hanya mengimplementasikan iterasi dengan cara yang dapat dimodifikasi saat iterasi"
Imi
5

Jika Anda benar-benar mencoba untuk memanfaatkan sifat desain data-berorientasi ECS maka Anda mungkin ingin memikirkan cara paling DOD untuk melakukan ini.

Lihatlah blog BitSquid , khususnya bagian tentang acara. Sebuah sistem yang cocok dengan ECS disajikan. Buffer semua peristiwa ke dalam antrian jenis-pesan-bersih yang bagus, sistem yang sama dalam ECS adalah per-komponen. Sistem yang diperbarui setelahnya dapat secara efisien beralih ke antrian untuk jenis pesan tertentu untuk memprosesnya. Atau abaikan saja. Mana saja.

Misalnya, CollisionSystem akan menghasilkan buffer yang penuh dengan peristiwa collision. Sistem lain yang dijalankan setelah tabrakan kemudian dapat mengulangi daftar dan memprosesnya sesuai kebutuhan.

Itu menjaga sifat paralel yang berorientasi data dari desain ECS tanpa semua kompleksitas registrasi pesan atau sejenisnya. Hanya sistem yang benar-benar peduli tentang jenis peristiwa tertentu yang mengulangi antrian untuk jenis itu, dan melakukan iterasi satu-pass langsung atas antrian pesan yang seefisien yang Anda bisa dapatkan.

Jika Anda menyimpan komponen yang dipesan secara konsisten di setiap sistem (mis. Memesan semua komponen dengan id entitas atau sesuatu seperti itu) maka Anda bahkan mendapatkan manfaat yang bagus bahwa pesan akan dihasilkan dalam urutan yang paling efisien untuk mengulanginya dan mencari komponen yang sesuai dalam sistem pengolahan. Artinya, jika Anda memiliki entitas 1, 2, & 3 maka pesan dihasilkan dalam urutan itu dan pencarian komponen dilakukan saat memproses pesan akan secara ketat meningkatkan urutan alamat (yang merupakan yang tercepat).

Sean Middleditch
sumber
1
+1, tapi saya tidak percaya pendekatan ini tidak memiliki kerugian. Bukankah ini memaksa kita untuk saling ketergantungan hardcode antar sistem? Atau mungkin saling ketergantungan ini dimaksudkan untuk dikodekan, dengan satu atau lain cara?
Patryk Czachurski
2
@Daedalus: jika logika game membutuhkan pembaruan fisika untuk melakukan logika yang benar, bagaimana Anda tidak akan memiliki ketergantungan itu? Bahkan dengan model pubsub, Anda harus secara eksplisit berlangganan jenis pesan ini-dan-itu yang hanya dihasilkan oleh beberapa sistem lain. Menghindari ketergantungan itu sulit, dan kebanyakan hanya mencari tahu lapisan yang tepat. Grafis dan fisika bersifat independen, misalnya, tetapi akan ada lapisan lem tingkat yang lebih tinggi yang memastikan pembaruan simulasi fisika interpolasi tercermin dalam grafik, dll.
Sean Middleditch
Ini harus menjadi jawaban yang diterima. Cara sederhana untuk menyelesaikannya adalah dengan membuat jenis komponen baru, misalnya CollisionResolvable yang akan diproses oleh setiap sistem yang tertarik untuk melakukan sesuatu setelah tabrakan terjadi. Yang akan cocok dengan proposisi Drake, namun ada sistem untuk setiap loop subdivisi.
user8363