Phân bổ các thực thể trong một hệ thống thực thể


9

Tôi khá không chắc chắn về cách tôi nên phân bổ / giống với các thực thể của mình trong hệ thống thực thể của mình. Tôi có nhiều lựa chọn khác nhau, nhưng hầu hết trong số họ dường như có khuyết điểm liên quan đến chúng. Trong tất cả các trường hợp, các thực thể được tương tự bởi một ID (số nguyên) và có thể có một lớp bao bọc được liên kết với nó. Lớp trình bao bọc này có các phương thức để thêm / xóa các thành phần đến / từ thực thể.

Trước khi tôi đề cập đến các tùy chọn, đây là cấu trúc cơ bản của hệ thống thực thể của tôi:

  • Thực thể
    • Một đối tượng mô tả một đối tượng trong trò chơi
  • Thành phần
    • Được sử dụng để lưu trữ dữ liệu cho thực thể
  • Hệ thống
    • Chứa các thực thể với các thành phần cụ thể
    • Được sử dụng để cập nhật các thực thể với các thành phần cụ thể
  • Thế giới
    • Chứa các thực thể và hệ thống cho hệ thống thực thể
    • Có thể tạo / hủy các mục nhập và có các hệ thống được thêm / xóa khỏi / vào nó

Đây là những lựa chọn của tôi, mà tôi đã nghĩ đến:

Lựa chọn 1:

Không lưu trữ các lớp trình bao bọc Thực thể và chỉ lưu trữ ID / ID bị xóa tiếp theo. Nói cách khác, các thực thể sẽ được trả về theo giá trị, như vậy:

Entity entity = world.createEntity();

Điều này rất giống với entityx, ngoại trừ tôi thấy một số sai sót trong thiết kế này.

Nhược điểm

  • Có thể có các lớp trình bao bọc thực thể trùng lặp (vì trình sao chép phải được triển khai và các hệ thống cần phải chứa các thực thể)
  • Nếu một Thực thể bị hủy, các lớp trình bao bọc thực thể trùng lặp sẽ không có giá trị được cập nhật

Lựa chọn 2:

Lưu trữ các lớp bao bọc thực thể trong một nhóm đối tượng. tức là các thực thể sẽ được trả về bởi con trỏ / tham chiếu, như vậy:

Entity& e = world.createEntity();

Nhược điểm

  • Nếu có các thực thể trùng lặp, thì khi một thực thể bị hủy, cùng một đối tượng thực thể có thể được sử dụng lại để phân bổ một thực thể khác.

Tùy chọn 3:

Sử dụng ID thô và quên các lớp thực thể của trình bao bọc. Sự sụp đổ của điều này, tôi nghĩ, là cú pháp sẽ được yêu cầu cho nó. Tôi đang nghĩ về việc làm điều này có vẻ như đơn giản và dễ thực hiện nhất. Tôi khá không chắc chắn về nó, vì cú pháp.

tức là để thêm một thành phần với thiết kế này, nó sẽ trông như sau:

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

Như được thêm vào này:

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

Nhược điểm

  • Cú pháp
  • ID trùng lặp

Câu trả lời:


12

ID của bạn phải là hỗn hợp của chỉ mụcphiê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 isEntityValidhoặ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_idcho 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::dequehoặ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 worlddù 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 entitylớ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 _worldviê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_ptrthay 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_entityweak_entitychỉ 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 entityngay 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^16thì 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__id, chỉ cần cẩn thận với luồng.


Tại sao không chứa ID trong cấu trúc thực thể? Tôi khá bối rối. Ngoài ra, bạn có thể sử dụng std :: shared_ptr / yếu_ptr cho owning_entityweak_entity?
Miguel.martin

Bạn có thể chứa ID thay thế nếu bạn muốn. Điểm duy nhất là giá trị của ID thay đổi khi một thực thể trong vị trí bị hủy trong khi ID cũng chứa chỉ mục của vị trí để tra cứu hiệu quả. Bạn có thể sử dụng shared_ptrweak_ptrlưu ý rằng chúng có nghĩa là cho các đối tượng được phân bổ riêng lẻ (mặc dù chúng có thể có các thông số tùy chỉnh để thay đổi điều đó) và do đó không phải là loại hiệu quả nhất để sử dụng. weak_ptrđặc biệt có thể không làm những gì bạn muốn; nó ngăn không cho một thực thể được giải phóng hoàn toàn / tái sử dụng cho đến khi mọi thực thể weak_ptrđược thiết lập lại trong khi weak_entitykhông.
Sean Middleditch

Sẽ dễ dàng hơn rất nhiều để giải thích cách tiếp cận này nếu tôi có bảng trắng hoặc không quá lười biếng để vẽ nó lên trong Paint hoặc một cái gì đó. :) Tôi nghĩ rằng hình dung cấu trúc làm cho nó cực kỳ rõ ràng.
Sean Middleditch

gamesfromwithin.com/managing-data-relationships Bài viết này dường như trình bày một số - điều tương tự bạn đã nói trong câu trả lời của bạn, đây có phải là ý bạn không?
Miguel.martin

1
Tôi là tác giả của EntityX và việc sử dụng lại các chỉ số đã làm phiền tôi trong một thời gian. Dựa trên nhận xét của bạn, tôi đã cập nhật EntityX để bao gồm một phiên bản. Cảm ơn @SeanMiddleditch!
Alec Thomas

0

Tôi thực sự đang làm việc trên một cái gì đó tương tự ngay bây giờ, và đã sử dụng một giải pháp gần nhất với số 1 của bạn.

Tôi có EntityHandletrường hợp trả lại từ World. Mỗi cái EntityHandlecó một con trỏ tới World(trong trường hợp của tôi, tôi chỉ gọi nó EntityManager) và các phương thức truy xuất / thao tác dữ liệu trong các EntityHandlecuộc gọi thực sự là World: ví dụ để thêm một Componentthực thể, bạn có thể gọi EntityHandle.addComponent(component), sẽ lần lượt gọi World.addComponent(this, component).

Bằng cách này, các Entitylớp trình bao bọc không được lưu trữ và bạn tránh được chi phí phụ theo cú pháp bạn nhận được với tùy chọn của mình 3. Nó cũng tránh được vấn đề "Nếu một Thực thể bị hủy, các lớp trình bao bọc thực thể trùng lặp sẽ không có giá trị cập nhật ", Bởi vì tất cả đều trỏ đến cùng một dữ liệu.


Điều gì xảy ra nếu bạn tạo một EntityHandle khác giống với thực thể đó, và sau đó bạn cố gắng xóa một trong các điều khiển? Tay cầm khác vẫn sẽ có cùng ID, điều đó có nghĩa là nó "xử lý" một thực thể đã chết.
Miguel.martin

Điều đó đúng, các tay cầm còn lại sau đó sẽ trỏ đến ID không còn "giữ" một thực thể. Tất nhiên, nên tránh các tình huống bạn xóa một thực thể và sau đó cố gắng truy cập nó từ nơi khác. Các Worldthể ví dụ như ném một ngoại lệ khi cố gắng thao tác / lấy dữ liệu liên quan đến một thực thể "chết".
vijoc

Trong khi tốt nhất nên tránh, trong thế giới thực, điều này sẽ xảy ra. Các tập lệnh sẽ giữ các tài liệu tham khảo, các đối tượng trò chơi "thông minh" (như tìm kiếm tên lửa) sẽ giữ các tài liệu tham khảo, v.v. người giới thiệu.
Sean Middleditch

Ví dụ, Thế giới có thể đưa ra một ngoại lệ khi cố gắng thao túng / truy xuất dữ liệu được liên kết với thực thể "đã chết" Không phải nếu ID cũ hiện được cấp phát với một thực thể mới.
Miguel.martin
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.