Bagaimana cara kerja komunikasi entitas?


115

Saya punya dua kasus pengguna:

  1. Bagaimana cara entity_Amengirim take-damagepesan entity_B?
  2. Bagaimana entity_Apermintaan entity_BHP?

Inilah yang saya temui sejauh ini:

  • Antrian pesan
    1. entity_Amembuat take-damagepesan dan mempostingnya ke entity_Bantrian pesan.
    2. entity_Amembuat query-hppesan dan mengeposkannya ke entity_B. entity_Bsebagai gantinya menciptakan response-hppesan dan mengeposkannya ke entity_A.
  • Terbitkan / Berlangganan
    1. entity_Bberlangganan take-damagepesan (mungkin dengan beberapa penyaringan preemptive sehingga hanya pesan yang relevan dikirim). entity_Amenghasilkan take-damagepesan yang merujuk entity_B.
    2. entity_Aberlangganan update-hppesan (mungkin disaring). Setiap frame entity_Bmenyiarkan update-hppesan.
  • Sinyal / Slot
    1. ???
    2. entity_Amenghubungkan sebuah update-hpslot yang ke entity_B's update-hpsinyal.

Apakah ada yang lebih baik? Apakah saya memiliki pemahaman yang benar tentang bagaimana skema komunikasi ini akan mengikat ke dalam sistem entitas mesin game?

Jawaban:


67

Pertanyaan bagus! Sebelum saya sampai pada pertanyaan spesifik yang Anda ajukan, saya akan mengatakan: jangan meremehkan kekuatan kesederhanaan. Tenpn benar. Ingatlah bahwa semua yang Anda coba lakukan dengan pendekatan ini adalah menemukan cara yang elegan untuk menunda panggilan fungsi atau memisahkan penelepon dari callee. Saya dapat merekomendasikan coroutine sebagai cara intuitif yang mengejutkan untuk mengatasi beberapa masalah tersebut, tapi itu sedikit di luar topik. Terkadang, Anda lebih baik hanya memanggil fungsi dan hidup dengan fakta bahwa entitas A digabungkan langsung ke entitas B. Lihat YAGNI.

Yang mengatakan, saya telah menggunakan dan senang dengan model sinyal / slot dikombinasikan dengan pesan sederhana yang lewat. Saya menggunakannya di C ++ dan Lua untuk judul iPhone yang cukup sukses yang memiliki jadwal yang sangat ketat.

Untuk kasus sinyal / slot, jika saya ingin entitas A melakukan sesuatu sebagai respons terhadap sesuatu yang dilakukan entitas B (mis. Membuka kunci pintu ketika sesuatu mati) Saya mungkin memiliki entitas A yang berlangganan langsung ke peristiwa kematian entitas B. Atau mungkin entitas A akan berlangganan masing-masing grup entitas, menambah penghitung pada setiap peristiwa yang dipecat, dan membuka kunci pintu setelah N dari mereka telah meninggal. Juga, "grup entitas" dan "N dari mereka" biasanya akan menjadi desainer yang didefinisikan dalam data level. (Sebagai tambahan, ini adalah satu area di mana coroutine benar-benar dapat bersinar, misalnya, WaitForMultiple ("Dying", entA, entB, entC); door.Unlock ();)

Tapi itu bisa menjadi rumit ketika datang ke reaksi yang tergabung erat dengan kode C ++, atau kejadian game yang secara inheren sesaat: menangani kerusakan, memuat ulang senjata, men-debug, umpan balik AI berbasis lokasi yang digerakkan oleh pemain. Di sinilah pesan lewat dapat mengisi celah. Ini pada dasarnya bermuara pada sesuatu seperti, "beri tahu semua entitas di area ini untuk menerima kerusakan dalam 3 detik," atau "setiap kali Anda menyelesaikan fisika untuk mencari tahu siapa yang saya tembak, minta mereka untuk menjalankan fungsi skrip ini." Sulit untuk mengetahui bagaimana melakukannya dengan baik menggunakan terbitkan / berlangganan atau sinyal / slot.

Ini bisa dengan mudah berlebihan (dibandingkan contoh tenpn). Ini juga bisa menjadi mengasapi yang tidak efisien jika Anda memiliki banyak tindakan. Namun terlepas dari kekurangannya, pendekatan "pesan dan acara" ini sangat cocok dengan kode permainan yang ditulis (misalnya dalam Lua). Kode skrip dapat mendefinisikan dan bereaksi terhadap pesan dan acara sendiri tanpa kode C ++ peduli sama sekali. Dan kode skrip dapat dengan mudah mengirim pesan yang memicu kode C ++, seperti mengubah level, memutar suara, atau bahkan hanya membiarkan senjata mengatur berapa banyak kerusakan yang diberikan pesan TakeDamage. Ini menghemat banyak waktu karena saya tidak harus terus-menerus bermain-main dengan luabind. Dan itu membiarkan saya menyimpan semua kode luabind saya di satu tempat, karena tidak banyak. Bila digabungkan dengan benar,

Juga, pengalaman saya dengan use case # 2 adalah bahwa Anda lebih baik menanganinya sebagai acara di arah lain. Alih-alih bertanya apa kesehatan entitas, nyalakan acara / kirim pesan setiap kali kesehatan membuat perubahan signifikan.

Dalam hal antarmuka, btw, saya berakhir dengan tiga kelas untuk mengimplementasikan semua ini: EventHost, EventClient, dan MessageClient. EventHosts membuat slot, EventClients berlangganan / sambungkan ke dalamnya, dan MessageClients mengaitkan delegasi dengan pesan. Perhatikan bahwa target delegasi MessageClient tidak harus berupa objek yang sama yang memiliki asosiasi. Dengan kata lain, MessageClients dapat ada hanya untuk meneruskan pesan ke objek lain. FWIW, metafora host / klien agak tidak pantas. Sumber / Sink mungkin konsep yang lebih baik.

Maaf, saya agak mengoceh di sana. Ini jawaban pertama saya :) Saya harap ini masuk akal.


Terima kasih atas jawabannya. Wawasan luar biasa. Alasan saya terlalu mendesain pesan yang lewat adalah karena Lua. Saya ingin dapat membuat senjata baru tanpa kode C ++ baru. Jadi, pikiran Anda menjawab beberapa pertanyaan saya yang tidak diminta.
deft_code

Adapun coroutine saya juga sangat percaya pada coroutine, tapi saya tidak pernah bisa bermain dengan mereka di C ++. Saya memiliki harapan yang samar-samar untuk menggunakan coroutine dalam kode lua untuk menangani pemblokiran panggilan (misalnya, menunggu kematian). Apakah itu sepadan dengan usaha? Saya takut bahwa saya mungkin dibutakan oleh keinginan kuat saya untuk coroutine di c ++.
deft_code

Terakhir, Apa permainan iphone itu? Bisakah saya mendapatkan informasi lebih lanjut tentang sistem entitas yang Anda gunakan?
deft_code

2
Sistem entitas sebagian besar dalam C ++. Jadi ada, misalnya, kelas Imp yang menangani perilaku Imp. Lua dapat mengubah parameter Imp saat menelurkan atau melalui pesan. Tujuan dengan Lua adalah agar sesuai dengan jadwal yang ketat, dan debugging kode Lua sangat memakan waktu. Kami menggunakan Lua untuk tingkat skrip (entitas mana yang pergi ke mana, peristiwa yang terjadi ketika Anda menekan pemicu). Jadi di Lua, kami akan mengatakan hal-hal seperti, SpawnEnt ("Imp") di mana Imp adalah asosiasi pabrik yang terdaftar secara manual. Itu akan selalu muncul dalam satu kumpulan entitas global. Bagus dan sederhana. Kami menggunakan banyak smart_ptr dan lemah_ptr.
BRaffle

1
Jadi BananaRaffle: Apakah Anda mengatakan ini adalah ringkasan akurat dari jawaban Anda: "Semua 3 solusi yang Anda posting memiliki kegunaannya, seperti yang dilakukan orang lain. Jangan mencari satu solusi sempurna, cukup gunakan apa yang Anda butuhkan di tempat yang masuk akal . "
Ipsquiggle

76
// in entity_a's code:
entity_b->takeDamage();

Anda bertanya bagaimana permainan komersial melakukannya. ;)


8
Suara turun? Serius, begitulah biasanya dilakukan! Sistem entitas bagus, tetapi mereka tidak membantu mencapai tonggak awal.
Tenpn

Saya membuat game Flash secara profesional, dan ini adalah cara saya melakukannya. Anda memanggil musuh. Kerusakan (10) dan kemudian Anda mencari info apa pun yang Anda butuhkan dari getter publik.
Iain

7
Ini serius bagaimana mesin permainan komersial melakukannya. Dia tidak bercanda. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer, dll.) Biasanya seperti itu.
AA Grapsas

3
Apakah game komersial juga salah mengeja "kerusakan"? :-P
Ricket

15
Ya, mereka melakukan kesalahan misspel, antara lain. :)
LearnCocos2D

17

Jawaban yang lebih serius:

Saya telah melihat papan tulis banyak digunakan. Versi sederhana tidak lebih dari struts yang diperbarui dengan hal-hal seperti HP entitas, yang kemudian dapat dicari entitas.

Papan tulis Anda bisa menjadi pandangan dunia dari entitas ini (tanyakan papan tulis B apa HP-nya), atau pandangan entitas tentang dunia (A menanyakan papan tulisnya untuk melihat apa target HP dari A).

Jika Anda hanya memperbarui papan tulis pada titik sinkronisasi dalam bingkai, Anda kemudian dapat membacanya dari titik lain di utas mana pun, membuat multithreading cukup mudah diterapkan.

Papan tulis yang lebih maju mungkin lebih seperti hashtable, memetakan string ke nilai. Ini lebih dapat dipelihara tetapi jelas memiliki biaya run-time.

Papan tulis secara tradisional hanya komunikasi satu arah - tidak akan menangani kerusakan akibat kerusakan.


Saya belum pernah mendengar model papan tulis sebelumnya.
deft_code

Mereka juga baik untuk mengurangi dependensi, seperti halnya antrian acara atau model publikasi / berlangganan.
Tenpn

2
Ini juga merupakan "definisi" kanonik tentang bagaimana sistem E / C / S "ideal" harus bekerja. "Komponen-komponen membentuk papan tulis; Sistem adalah kode yang bertindak atasnya. (Entitas, tentu saja, hanya long long ints atau serupa, dalam sistem ECS murni.)
BRPocock

6

Saya telah mempelajari masalah ini sedikit dan saya telah melihat solusi yang bagus.

Pada dasarnya ini semua tentang subsistem. Ini mirip dengan ide papan tulis yang disebutkan oleh tenpn.

Entitas terbuat dari komponen, tetapi mereka hanya tas properti. Tidak ada perilaku yang diterapkan dalam entitas itu sendiri.

Katakanlah, entitas memiliki komponen Kesehatan dan komponen Kerusakan.

Kemudian Anda memiliki beberapa MessageManager dan tiga subsistem: ActionSystem, DamageSystem, HealthSystem. Pada satu titik ActionSystem melakukan perhitungannya pada dunia game dan menghasilkan suatu peristiwa:

HIT, source=entity_A target=entity_B power=5

Acara ini dipublikasikan ke MessageManager. Sekarang pada satu titik waktu MessageManager melewati pesan yang tertunda dan menemukan bahwa DamageSystem telah berlangganan pesan HIT. Sekarang MessageManager mengirimkan pesan HIT ke DamageSystem. DamageSystem menelusuri daftar entitas yang memiliki komponen Kerusakan, menghitung titik kerusakan tergantung pada daya hantam atau keadaan lain dari kedua entitas, dll, dan menerbitkan acara

DAMAGE, source=entity_A target=entity_B amount=7

HealthSystem telah berlangganan pesan DAMAGE dan sekarang ketika MessageManager menerbitkan pesan DAMAGE ke HealthSystem, HealthSystem memiliki akses ke entitas entitas_A dan entitas_B dengan komponen Kesehatannya, jadi sekali lagi HealthSystem dapat melakukan perhitungannya (dan mungkin menerbitkan acara yang sesuai) ke MessageManager).

Dalam mesin permainan seperti itu, format pesan adalah satu-satunya penghubung antara semua komponen dan subsistem. Subsistem dan entitas sepenuhnya independen dan tidak mengetahui satu sama lain.

Saya tidak tahu apakah beberapa mesin game nyata telah mengimplementasikan ide ini atau tidak, tetapi sepertinya cukup solid dan bersih dan saya berharap suatu hari dapat menerapkannya sendiri untuk mesin game tingkat hobi saya.


Ini adalah jawaban yang jauh lebih baik daripada jawaban yang diterima IMO. Terpisah, dirawat dan diperpanjang (dan juga bukan bencana kopling seperti jawaban lelucon entity_b->takeDamage();)
Danny Yaroslavski

4

Mengapa tidak memiliki antrian pesan global, seperti:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

Dengan:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

Dan di akhir game loop / event handling:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

Saya pikir ini adalah pola Command. Dan Execute()merupakan virtual murni Event, yang turunannya mendefinisikan dan melakukan hal-hal. Jadi disini:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}

3

Jika game Anda adalah pemain tunggal, cukup gunakan metode objek sasaran (seperti yang disarankan tenpn).

Jika Anda (atau ingin mendukung) multipemain (lebih tepatnya multiclient), gunakan antrian perintah.

  • Ketika A tidak merusak B pada klien 1 cukup antri acara kerusakan.
  • Sinkronisasi antrian perintah melalui jaringan
  • Tangani perintah yang antri di kedua sisi.

2
Jika Anda serius menghindari kecurangan, A sama sekali tidak menyalahkan B pada klien. Klien yang memiliki A mengirimkan perintah "serangan B" ke server, yang melakukan persis apa yang dikatakan tenpn; server kemudian menyinkronkan keadaan itu dengan semua klien yang relevan.

@ Jo: Ya, jika ada server yang merupakan titik valid untuk dipertimbangkan, tetapi kadang-kadang tidak apa-apa untuk mempercayai klien (misalnya pada konsol) untuk menghindari beban server yang berat.
Andreas

2

Saya akan mengatakan: Jangan gunakan, selama Anda tidak secara eksplisit membutuhkan umpan balik instan dari kerusakan.

Entitas / komponen pengambil kerusakan / apa pun yang harus mendorong acara ke antrian acara lokal atau sistem pada tingkat yang sama yang menyimpan kerusakan-peristiwa.

Maka harus ada sistem overlay dengan akses ke kedua entitas yang meminta acara dari entitas a dan meneruskannya ke entitas b. Dengan tidak membuat sistem acara umum yang dapat digunakan oleh apa saja dari mana saja untuk meneruskan acara ke apa saja kapan saja, Anda membuat aliran data eksplisit yang selalu membuat kode lebih mudah untuk di-debug, lebih mudah untuk mengukur kinerja, lebih mudah untuk dipahami dan dibaca dan sering mengarah ke sistem yang dirancang lebih baik secara umum.


1

Telepon saja. Jangan lakukan permintaan-hp diikuti oleh permintaan-hp - jika Anda mengikuti model itu Anda akan berada di dunia yang terluka.

Anda mungkin ingin melihat Mono Continuations juga. Saya pikir itu akan ideal untuk NPC.


1

Jadi apa yang terjadi jika kita memiliki pemain A dan B yang mencoba menekan satu sama lain dalam siklus pembaruan yang sama ()? Misalkan Pembaruan () untuk pemain A terjadi sebelum Pembaruan () untuk pemain B dalam Siklus 1 (atau centang, atau apa pun sebutannya). Ada dua skenario yang dapat saya pikirkan:

  1. Pemrosesan langsung melalui pesan:

    • pemain A.Update () melihat pemain ingin menekan B, pemain B mendapat pesan yang memberitahukan kerusakan.
    • pemain B.HandleMessage () memperbarui titik hit untuk pemain B (dia mati)
    • pemain B.Update () melihat pemain B sudah mati .. dia tidak bisa menyerang pemain A

Ini tidak adil, pemain A dan B harus memukul satu sama lain, pemain B meninggal sebelum mengenai A hanya karena entitas / gim tersebut mendapat pembaruan () nanti.

  1. Mengantri pesan

    • Player A.Update () melihat pemain ingin menekan B, pemain B mendapat pesan yang memberitahukan kerusakan dan menyimpannya dalam antrian
    • Player A.Update () memeriksa antriannya, kosong
    • pemain B. Perbarui () pertama-tama memeriksa pergerakan sehingga pemain B mengirim pesan ke pemain A dengan kerusakan juga
    • player B.Update () juga menangani pesan dalam antrian, memproses kerusakan dari pemain A
    • Siklus baru (2): Pemain A ingin minum ramuan kesehatan sehingga Pemain A.Update () dipanggil dan langkah diproses
    • Player A.Update () memeriksa antrian pesan dan memproses kerusakan dari pemain B

Sekali lagi ini tidak adil .. pemain A seharusnya mengambil hitpoints dalam belokan / siklus / tik yang sama!


4
Anda tidak benar-benar menjawab pertanyaan tetapi saya pikir jawaban Anda akan membuat pertanyaan yang sangat bagus. Mengapa tidak melanjutkan dan bertanya bagaimana menyelesaikan prioritas "tidak adil" seperti itu?
bummzack

Saya ragu bahwa sebagian besar game peduli dengan ketidakadilan ini karena mereka memperbarui begitu sering sehingga jarang menjadi masalah. Satu solusi sederhana adalah dengan bergantian antara iterasi ke depan dan ke belakang melalui daftar entitas saat memperbarui.
Kylotan

Saya menggunakan 2 panggilan jadi saya memanggil Pembaruan () ke semua entitas, lalu setelah loop saya mengulangi lagi dan memanggil sesuatu seperti pEntity->Flush( pMessages );. Ketika entitas_A menghasilkan acara baru, itu tidak dibaca oleh entitas_B dalam bingkai itu (ia memiliki kesempatan untuk mengambil ramuan juga) kemudian keduanya menerima kerusakan dan setelah itu mereka memproses pesan penyembuhan ramuan yang akan menjadi yang terakhir dalam antrian . Pemain B tetap mati karena pesan ramuan adalah yang terakhir dalam antrian: P tetapi mungkin berguna untuk jenis pesan lain seperti membersihkan pointer ke entitas yang mati.
Pablo Ariel

Saya pikir pada level frame, sebagian besar implementasi game tidak adil. seperti kata Kylotan.
v.oddou

Masalah ini sangat mudah untuk dipecahkan. Terapkan kerusakan satu sama lain di penangan pesan atau apa pun. Anda pastinya tidak boleh menandai pemain sebagai mati di dalam penangan pesan. Dalam "Perbarui ()" Anda cukup melakukan "jika (hp <= 0) mati ();" (di awal "Perbarui ()" misalnya). Dengan begitu keduanya bisa saling membunuh pada saat bersamaan. Juga: Seringkali Anda tidak merusak pemain secara langsung, tetapi melalui beberapa objek perantara seperti peluru.
Tara
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.