Mengalokasikan Entitas dalam Sistem Entitas


9

Saya tidak yakin bagaimana saya harus mengalokasikan / menyerupai entitas saya dalam sistem entitas saya. Saya memiliki berbagai pilihan, tetapi kebanyakan dari mereka tampaknya memiliki kontra terkait dengan mereka. Dalam semua kasus, entitas mirip dengan ID (integer), dan mungkin memiliki kelas wrapper yang terkait dengannya. Kelas wrapper ini memiliki metode untuk menambah / menghapus komponen ke / dari entitas.

Sebelum saya menyebutkan opsi, berikut adalah struktur dasar dari sistem entitas saya:

  • Kesatuan
    • Objek yang menggambarkan objek dalam game
  • Komponen
    • Digunakan untuk menyimpan data untuk entitas
  • Sistem
    • Berisi entitas dengan komponen tertentu
    • Digunakan untuk memperbarui entitas dengan komponen tertentu
  • Dunia
    • Berisi entitas dan sistem untuk sistem entitas
    • Dapat membuat / menghancurkan entites dan memiliki sistem yang ditambahkan / dihapus dari / ke sana

Inilah beberapa pilihan saya, yang telah saya pikirkan:

Pilihan 1:

Jangan menyimpan kelas pembungkus Entitas, dan simpan saja ID / ID yang dihapus berikutnya. Dengan kata lain, entitas akan dikembalikan berdasarkan nilainya, seperti:

Entity entity = world.createEntity();

Ini sangat mirip dengan entitasx, kecuali saya melihat beberapa kekurangan dalam desain ini.

Cons

  • Mungkin ada kelas pembungkus entitas duplikat (karena copy-ctor harus diimplementasikan, dan sistem harus mengandung entitas)
  • Jika Entitas dihancurkan, kelas pembungkus entitas duplikat tidak akan memiliki nilai yang diperbarui

Pilihan 2:

Menyimpan kelas pembungkus entitas dalam kumpulan objek. yaitu Entitas akan kembali dengan pointer / referensi, seperti:

Entity& e = world.createEntity();

Cons

  • Jika ada entitas duplikat, maka ketika suatu entitas dihancurkan, objek entitas yang sama dapat digunakan kembali untuk mengalokasikan entitas lain.

Opsi 3:

Gunakan ID mentah, dan lupakan kelas entitas pembungkus. Kejatuhan ini, saya pikir, adalah sintaks yang diperlukan untuk itu. Saya sedang berpikir untuk melakukan ini karena sepertinya yang paling sederhana & mudah untuk mengimplementasikannya. Saya tidak yakin tentang itu, karena sintaksisnya.

yaitu Untuk menambahkan komponen dengan desain ini, akan terlihat seperti:

Entity e = world.createEntity();
world.addComponent<Position>(e, 0, 3);

Seperti terlampir dalam hal ini:

Entity e = world.createEntity();
e.addComponent<Position>(0, 3);

Cons

  • Sintaksis
  • ID duplikat

Jawaban:


12

ID Anda harus merupakan campuran antara indeks dan versi . Ini akan memungkinkan Anda untuk menggunakan kembali ID secara efisien, menggunakan ID untuk mencari komponen dengan cepat, dan membuat "opsi 2" Anda lebih mudah untuk diterapkan (meskipun opsi 3 dapat dibuat lebih enak dengan beberapa pekerjaan).

struct entity {
  uint16 version;
  /* and other crap that doesn't belong in components */
};

std::vector<entity> pool;
std::vector<uint16> freelist;
typedef uint32 entity_id; /* this shoudl be a wrapper class */

entity_id createEntity()
{
  uint16 index;
  if (!freelist.empty())
  {
    pool.push_back(entity());
    freelist.push_back(pool.size() - 1);
  }
  index = freelist.pop_back();

  return (pool[id].version << 16) | index;
}

void deleteEntity(entity_id id)
{
   uint16 index = id & 0xFFFF;
   ++pool[index].version;
   freelist.push_back(index);
}

entity* getEntity(entity_id id)
{
  uint16 index = id & 0xFFFF;
  uint16 version = id >> 16;
  if (index < pool.size() && pool[index].version == version)
    return &pool[index];
  else
    return NULL;
 }

Itu akan mengalokasikan integer 32-bit baru yang merupakan kombinasi dari indeks unik (yang unik di antara semua objek hidup) dan tag versi (yang akan unik untuk semua objek yang pernah menempati indeks itu).

Saat menghapus entitas, Anda menambah versi. Sekarang jika Anda memiliki referensi ke id yang beredar, itu tidak akan lagi memiliki tag versi yang sama dengan entitas yang menempati tempat itu di kolam. Upaya apa pun untuk menelepon getEntity( isEntityValidatau apa pun yang Anda inginkan) akan gagal. Jika Anda mengalokasikan objek baru di posisi itu, ID lama akan tetap gagal.

Anda dapat menggunakan sesuatu seperti ini untuk "opsi 2" Anda untuk memastikannya berfungsi tanpa khawatir tentang referensi entitas lama. Perhatikan bahwa Anda tidak boleh menyimpan entity*karena mereka mungkin bergerak ( pool.push_back()dapat merealokasi dan memindahkan seluruh kumpulan!) Dan hanya menggunakan entity_iduntuk referensi jangka panjang saja. Gunakan getEntityuntuk mengambil objek yang lebih cepat diakses hanya dalam kode lokal. Anda juga dapat menggunakan std::dequeatau serupa untuk menghindari pembatalan pointer jika Anda inginkan.

"Opsi 3" Anda adalah pilihan yang benar-benar valid. Tidak ada yang salah dengan menggunakan world.foo(e)bukan e.foo(), terutama karena Anda mungkin ingin referensi untuk worldtetap dan itu belum tentu lebih baik (meskipun tidak selalu lebih buruk) untuk menyimpan referensi itu di entitas itu sendiri.

Jika Anda benar-benar ingin e.foo()sintaks bertahan, pertimbangkan "penunjuk pintar" yang menangani ini untuk Anda. Membangun contoh kode yang saya berikan di atas, Anda dapat memiliki sesuatu seperti:

class entity_ptr {
  world* _world;
  entity_id _id;

public:
  entity_ptr() : _id(0) { }
  entity_ptr(world& world, entity_id id) : _world(&world), _id(id) { }

  bool empty() const { return _world != NULL && _world->getEntity(_id) != NULL; }
  void clear() { _world = NULL; _id = 0; }
  entity* get() { assert(!empty()); return _world->getEntity(_id); }
  entity* operator->() { return get(); }
  entity& operator*() { return *get(); }
  // add const method where appropriate
};

Sekarang Anda memiliki cara untuk menyimpan referensi ke entitas yang menggunakan ID unik dan yang dapat menggunakan ->operator untuk mengakses entitykelas (dan metode apa pun yang Anda buat di dalamnya) secara alami. The _worldanggota bisa menjadi tunggal atau global, juga, jika Anda lebih memilih.

Kode Anda hanya menggunakan entity_ptrdi tempat referensi entitas lain dan berjalan. Anda bahkan dapat menambahkan penghitungan referensi otomatis ke kelas jika Anda mau (agak lebih andal jika Anda memperbarui semua kode itu ke C ++ 11 dan menggunakan pindahkan semantik dan nilai referensi) sehingga Anda bisa menggunakan di entity_ptrmana saja dan tidak lagi berpikiran berat tentang referensi dan kepemilikan. Atau, dan inilah yang saya inginkan, buat yang terpisah owning_entitydan weak_entityketik dengan hanya jumlah referensi yang mengelola sebelumnya sehingga Anda dapat menggunakan sistem tipe untuk membedakan antara pegangan yang membuat suatu entitas tetap hidup dan yang hanya referensi itu sampai hancur.

Perhatikan bahwa biaya overhead sangat rendah. Manipulasi bitnya murah. Pencarian tambahan ke kolam bukan biaya nyata jika Anda mengakses bidang lain entitysegera setelah itu. Jika entitas Anda benar-benar hanya id dan tidak ada yang lain maka mungkin ada sedikit overhead tambahan. Secara pribadi, gagasan tentang ECS ​​di mana entitas hanya ID dan tidak ada yang lain tampak sedikit ... akademis bagi saya. Setidaknya ada beberapa flag yang ingin Anda simpan di entitas umum, dan game yang lebih besar mungkin akan menginginkan koleksi komponen entitas dari beberapa jenis (daftar tautan inline jika tidak ada yang lain) untuk alat dan dukungan serialisasi.

Sebagai catatan yang agak final, saya sengaja tidak menginisialisasi entity::version. Itu tidak masalah. Tidak peduli apa versi awalnya, selama kita menambahnya setiap kali kita baik-baik saja. Jika itu berakhir dekat 2^16maka itu hanya akan membungkus. Jika Anda akhirnya membungkus dengan cara yang membuat ID lama tetap valid, beralihlah ke versi yang lebih besar (dan ID 64-bit jika Anda perlu). Agar aman, Anda mungkin harus menghapus entity_ptr setiap kali Anda memeriksanya dan itu kosong. Anda bisa empty()melakukan ini untuk Anda dengan bisa berubah _world_dan _id, hati-hati dengan threading.


Mengapa tidak mengandung ID dalam struktur entitas? Saya cukup bingung. Bisakah Anda menggunakan std :: shared_ptr / kelemahan_ptr untuk owning_entitydan weak_entity?
miguel.martin

Anda dapat berisi ID sebagai gantinya jika Anda mau. Satu-satunya titik adalah bahwa nilai ID berubah ketika entitas dalam slot dihancurkan sementara ID juga berisi indeks slot untuk pencarian efisien. Anda dapat menggunakan shared_ptrdan weak_ptrtetapi menyadari bahwa itu dimaksudkan untuk objek yang dialokasikan secara individual (meskipun mereka dapat memiliki pengubah kustom untuk mengubahnya) dan karenanya bukan jenis yang paling efisien untuk digunakan. weak_ptrkhususnya mungkin tidak melakukan apa yang Anda inginkan; itu menghentikan entitas dari sepenuhnya dialokasikan / digunakan kembali sampai setiap weak_ptrdiatur ulang sementara weak_entitytidak.
Sean Middleditch

Akan jauh lebih mudah untuk menjelaskan pendekatan ini jika saya memiliki papan tulis atau tidak terlalu malas untuk menggambar ini di Paint atau sesuatu. :) Saya pikir memvisualisasikan struktur membuatnya sangat jelas.
Sean Middleditch

gamesfromwithin.com/managing-data-relationships Artikel ini sepertinya menyajikan beberapa hal yang sama dengan yang Anda katakan dalam jawaban Anda, apakah ini yang Anda maksud?
miguel.martin

1
Saya penulis EntityX , dan penggunaan kembali indeks telah mengganggu saya untuk sementara waktu. Berdasarkan komentar Anda, saya telah memperbarui EntityX untuk juga menyertakan versi. Terima kasih @SeanMiddleditch!
Alec Thomas

0

Saya sebenarnya sedang mengerjakan sesuatu yang serupa sekarang, dan telah menggunakan solusi yang paling dekat dengan nomor 1 Anda.

Saya memiliki EntityHandleinstance yang dikembalikan dari World. Masing-masing EntityHandlememiliki pointer ke World(dalam kasus saya, saya sebut saja EntityManager), dan metode manipulasi / pengambilan data EntityHandlesebenarnya adalah panggilan ke World: misalnya untuk menambahkan Componentsuatu entitas, Anda dapat memanggil EntityHandle.addComponent(component), yang pada gilirannya akan memanggil World.addComponent(this, component).

Dengan cara ini Entitykelas pembungkus tidak disimpan, dan Anda menghindari overhead tambahan dalam sintaks yang Anda dapatkan dengan opsi Anda 3. Ini juga menghindari masalah "Jika Entitas dihancurkan, kelas pembungkus entitas duplikat tidak akan memiliki nilai yang diperbarui ", karena semuanya menunjuk ke data yang sama.


Apa yang terjadi jika Anda membuat EntityHandle lain agar menyerupai entitas yang sama, dan kemudian Anda mencoba menghapus salah satu pegangan? Pegangan yang lain masih akan memiliki ID yang sama, yang berarti bahwa ia "menangani" entitas yang mati.
miguel.martin

Itu benar, pegangan yang tersisa lainnya kemudian akan menunjuk ke ID yang tidak lagi "memegang" suatu entitas. Tentu saja, situasi di mana Anda menghapus suatu entitas dan kemudian mencoba mengaksesnya dari tempat lain harus dihindari. The Worldbisa misalnya melempar pengecualian ketika mencoba untuk memanipulasi / mengambil data terkait dengan "mati" entitas.
vijoc

Meskipun sebaiknya dihindari, di dunia nyata ini akan terjadi. Skrip akan berpegang pada referensi, objek permainan "pintar" (seperti mencari rudal) akan berpegang pada referensi, dll. Anda benar-benar membutuhkan sistem yang dapat dengan baik dalam semua kasus mengatasi referensi basi atau yang melacak dan nol keluar dengan lemah referensi.
Sean Middleditch

Dunia misalnya bisa melempar pengecualian ketika mencoba memanipulasi / mengambil data yang terkait dengan entitas "mati". Tidak jika ID lama sekarang dialokasikan dengan entitas baru.
miguel.martin
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.