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
?