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
( isEntityValid
atau 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_id
untuk referensi jangka panjang saja. Gunakan getEntity
untuk mengambil objek yang lebih cepat diakses hanya dalam kode lokal. Anda juga dapat menggunakan std::deque
atau 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 world
tetap 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 entity
kelas (dan metode apa pun yang Anda buat di dalamnya) secara alami. The _world
anggota bisa menjadi tunggal atau global, juga, jika Anda lebih memilih.
Kode Anda hanya menggunakan entity_ptr
di 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_ptr
mana saja dan tidak lagi berpikiran berat tentang referensi dan kepemilikan. Atau, dan inilah yang saya inginkan, buat yang terpisah owning_entity
dan weak_entity
ketik 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 entity
segera 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^16
maka 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.
owning_entity
danweak_entity
?shared_ptr
danweak_ptr
tetapi 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_ptr
khususnya mungkin tidak melakukan apa yang Anda inginkan; itu menghentikan entitas dari sepenuhnya dialokasikan / digunakan kembali sampai setiapweak_ptr
diatur ulang sementaraweak_entity
tidak.Saya sebenarnya sedang mengerjakan sesuatu yang serupa sekarang, dan telah menggunakan solusi yang paling dekat dengan nomor 1 Anda.
Saya memiliki
EntityHandle
instance yang dikembalikan dariWorld
. Masing-masingEntityHandle
memiliki pointer keWorld
(dalam kasus saya, saya sebut sajaEntityManager
), dan metode manipulasi / pengambilan dataEntityHandle
sebenarnya adalah panggilan keWorld
: misalnya untuk menambahkanComponent
suatu entitas, Anda dapat memanggilEntityHandle.addComponent(component)
, yang pada gilirannya akan memanggilWorld.addComponent(this, component)
.Dengan cara ini
Entity
kelas 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.sumber
World
bisa misalnya melempar pengecualian ketika mencoba untuk memanipulasi / mengambil data terkait dengan "mati" entitas.