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:
- Jalankan pembaruan pada setiap sistem game secara berurutan
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)
Setiap sistem yang berlangganan pesan tertentu akan dipanggil metode penanganan pesan
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):
- Sistem mulai memproses TimePassedMessage
- InputHandingSystem mengonversi penekanan tombol ke tindakan entitas (dalam hal ini, panah kiri berubah menjadi tindakan MoveWest). Tindakan entitas disimpan dalam komponen ActionExecutor
- ActionExecutionSystem , sebagai reaksi terhadap tindakan entitas, menambahkan pesan MovementDirectionChangeRequestedMessage ke akhir antrian pesan
- 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)
- Sistem berhenti memproses TimePassedMessage
- Sistem mulai memproses MovementDirectionChangeRequestedMessage
- MovementSystem mengubah kecepatan entitas / arah gerakan seperti yang diminta
- Sistem berhenti memproses MovementDirectionChangeRequestedMessage
- Sistem mulai memproses PositionChangedMessage
- CollisionDetectionSystem mendeteksi bahwa karena suatu entitas bergerak, ia berlari ke entitas lain (tangki masuk ke dalam dinding). Itu menambahkan CollisionOccuredMessage ke antrian
- Sistem berhenti memproses PositionChangedMessage
- Sistem mulai memproses CollisionOccuredMessage
- 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).
sumber
Jawaban:
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.
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 .
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.
sumber
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;))
sumber
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).
sumber