ID của bạn phải là hỗn hợp của chỉ mục và phiên bản . Điều này sẽ cho phép bạn sử dụng lại ID một cách hiệu quả, sử dụng ID để nhanh chóng tìm thấy các thành phần và làm cho "tùy chọn 2" của bạn dễ thực hiện hơn (mặc dù tùy chọn 3 có thể dễ thực hiện hơn với một số công việc).
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;
}
Điều đó sẽ phân bổ một số nguyên 32 bit mới, là sự kết hợp của một chỉ mục duy nhất (duy nhất trong số tất cả các đối tượng sống) và thẻ phiên bản (sẽ là duy nhất cho tất cả các đối tượng đã chiếm chỉ số đó).
Khi xóa một thực thể, bạn tăng phiên bản. Bây giờ nếu bạn có bất kỳ tham chiếu nào đến id đó trôi nổi xung quanh, nó sẽ không còn có thẻ phiên bản giống như thực thể chiếm vị trí đó trong nhóm. Mọi nỗ lực để gọi getEntity
(hoặc một isEntityValid
hoặc bất cứ điều gì bạn thích) sẽ thất bại. Nếu bạn phân bổ một đối tượng mới ở vị trí đó, ID cũ sẽ vẫn thất bại.
Bạn có thể sử dụng một cái gì đó như thế này cho "tùy chọn 2" của mình để đảm bảo nó chỉ hoạt động mà không phải lo lắng về các tham chiếu thực thể cũ. Lưu ý rằng bạn không bao giờ được lưu trữ entity*
vì chúng có thể di chuyển ( pool.push_back()
có thể phân bổ lại và di chuyển toàn bộ nhóm!) Và chỉ sử dụng entity_id
cho các tài liệu tham khảo dài hạn thay thế. Sử dụng getEntity
để truy xuất một đối tượng truy cập nhanh hơn chỉ trong mã cục bộ. Bạn cũng có thể sử dụng một std::deque
hoặc tương tự để tránh mất hiệu lực con trỏ nếu bạn muốn.
"Tùy chọn 3" của bạn là một lựa chọn hoàn toàn hợp lệ. Không có gì sai khi sử dụng world.foo(e)
thay vì e.foo()
, đặc biệt là vì bạn có thể muốn tham chiếu đến world
dù sao và nó không nhất thiết phải tốt hơn (mặc dù không nhất thiết tệ hơn) để lưu trữ tài liệu tham khảo đó trong chính thực thể.
Nếu bạn thực sự muốn e.foo()
cú pháp bám sát, hãy xem xét một "con trỏ thông minh" xử lý việc này cho bạn. Xây dựng mã ví dụ tôi đã từ bỏ ở trên, bạn có thể có một cái gì đó như:
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
};
Bây giờ bạn có một cách để lưu trữ một tham chiếu đến một thực thể sử dụng một ID duy nhất và có thể sử dụng ->
toán tử để truy cập entity
lớp (và bất kỳ phương thức nào bạn tạo trên đó) một cách khá tự nhiên. Thành _world
viên cũng có thể là người độc thân hoặc toàn cầu, nếu bạn thích.
Mã của bạn chỉ sử dụng entity_ptr
thay cho bất kỳ tham chiếu thực thể nào khác và đi. Bạn thậm chí có thể thêm tính năng tham chiếu tự động vào lớp nếu bạn thích (đáng tin cậy hơn một chút nếu bạn cập nhật tất cả mã đó lên C ++ 11 và sử dụng di chuyển ngữ nghĩa và tham chiếu giá trị) để bạn có thể sử dụng entity_ptr
ở mọi nơi và không còn phải suy nghĩ nhiều về tài liệu tham khảo và quyền sở hữu. Hoặc, và đây là những gì tôi thích, tạo một loại riêng biệt owning_entity
và weak_entity
chỉ có số tham chiếu quản lý trước đây để bạn có thể sử dụng hệ thống loại để phân biệt giữa các tay cầm giữ cho một thực thể tồn tại và những loại chỉ tham chiếu đến khi nó bị phá hủy.
Lưu ý rằng chi phí rất thấp. Các thao tác bit là giá rẻ. Việc tìm kiếm thêm vào nhóm không phải là một chi phí thực sự nếu bạn truy cập vào các lĩnh vực khác entity
ngay sau đó. Nếu các thực thể của bạn thực sự chỉ là id và không có gì khác thì có thể có thêm một chút chi phí. Cá nhân, ý tưởng về một ECS nơi các thực thể chỉ là ID và không có gì khác có vẻ hơi ... hàn lâm đối với tôi. Có ít nhất một vài cờ bạn sẽ muốn lưu trữ trên thực thể chung và các trò chơi lớn hơn có thể sẽ muốn có một bộ sưu tập các thành phần của thực thể nào đó (danh sách được liên kết nội tuyến nếu không có gì khác) cho các công cụ và hỗ trợ tuần tự hóa.
Như một lưu ý cuối cùng, tôi cố tình không khởi tạo entity::version
. Nó không thành vấn đề. Cho dù phiên bản ban đầu là gì, miễn là chúng tôi tăng nó mỗi lần chúng tôi ổn. Nếu nó kết thúc gần 2^16
thì nó sẽ chỉ quấn quanh. Nếu bạn kết thúc việc bọc xung quanh theo cách làm cho ID cũ vẫn hợp lệ, hãy chuyển sang phiên bản lớn hơn (và ID 64 bit nếu bạn cần). Để an toàn, có lẽ bạn nên xóa entity_ptr bất cứ khi nào bạn kiểm tra nó và nó trống. Bạn có thể làm empty()
điều này cho bạn với một đột biến _world_
và _id
, chỉ cần cẩn thận với luồng.
owning_entity
vàweak_entity
?