Lời khuyên về liên kết giữa hệ thống thành phần thực thể trong C ++


10

Sau khi đọc một vài tài liệu về hệ thống thành phần thực thể, tôi quyết định thực hiện của tôi. Cho đến nay, tôi có một lớp Thế giới chứa các thực thể và trình quản lý hệ thống (hệ thống), lớp Thực thể chứa các thành phần dưới dạng std :: map và một vài hệ thống. Tôi đang giữ các thực thể như một std :: vector trong Thế giới. Không có vấn đề cho đến nay. Điều làm tôi bối rối là sự lặp lại của các thực thể, tôi không thể có một tâm trí rõ ràng về điều đó, vì vậy tôi vẫn không thể thực hiện phần đó. Mỗi hệ thống có nên giữ một danh sách địa phương của các thực thể mà chúng quan tâm không? Hoặc tôi chỉ nên lặp qua các thực thể trong lớp Thế giới và tạo một vòng lặp lồng nhau để lặp qua các hệ thống và kiểm tra xem thực thể đó có các thành phần mà hệ thống quan tâm không? Ý tôi là :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

nhưng tôi nghĩ một hệ thống bitmask sẽ ngăn chặn sự linh hoạt trong trường hợp nhúng ngôn ngữ kịch bản. Hoặc có danh sách cục bộ cho mỗi hệ thống sẽ tăng mức sử dụng bộ nhớ cho các lớp. Tôi vô cùng bối rối.


Tại sao bạn mong đợi cách tiếp cận bitmask để cản trở các ràng buộc kịch bản? Bên cạnh đó, sử dụng các tham chiếu (const, nếu có thể) trong các vòng lặp cho mỗi vòng lặp để tránh sao chép các thực thể và hệ thống.
Benjamin Kloster

sử dụng bitmask ví dụ một int, sẽ chỉ chứa 32 thành phần khác nhau. Tôi không ngụ ý sẽ có hơn 32 thành phần nhưng nếu tôi có thì sao? tôi sẽ phải tạo một int hoặc 64 bit int khác, nó sẽ không động.
deniz

Bạn có thể sử dụng std :: bitset hoặc std :: vector <bool>, tùy thuộc vào việc bạn có muốn nó là động thời gian chạy hay không.
Benjamin Kloster

Câu trả lời:


7

Có danh sách cục bộ cho mỗi hệ thống sẽ tăng mức sử dụng bộ nhớ cho các lớp.

Đó là một sự đánh đổi không-thời gian truyền thống .

Trong khi lặp qua tất cả các thực thể và kiểm tra chữ ký của chúng là mã trực tiếp, nó có thể trở nên không hiệu quả khi số lượng hệ thống của bạn tăng lên - hãy tưởng tượng một hệ thống chuyên biệt (hãy để nó là đầu vào) tìm kiếm thực thể duy nhất của nó trong số hàng ngàn thực thể không liên quan .

Điều đó nói rằng, phương pháp này có thể vẫn đủ tốt tùy thuộc vào mục tiêu của bạn.

Mặc dù, nếu bạn lo lắng về tốc độ, tất nhiên có một giải pháp khác để xem xét.

Mỗi hệ thống có nên giữ một danh sách địa phương của các thực thể mà chúng quan tâm không?

Chính xác. Đây là một cách tiếp cận tiêu chuẩn sẽ cung cấp cho bạn hiệu suất tốt và khá dễ thực hiện. Theo tôi, chi phí bộ nhớ là không đáng kể - chúng tôi đang nói về việc lưu trữ con trỏ.

Bây giờ làm thế nào để duy trì các "danh sách quan tâm" này có thể không rõ ràng. Đối với bộ chứa dữ liệu, std::vector<entity*> targetsbên trong lớp của hệ thống là hoàn toàn đủ. Bây giờ những gì tôi làm là đây:

  • Thực thể trống rỗng khi tạo và không thuộc về bất kỳ hệ thống nào.
  • Bất cứ khi nào tôi thêm một thành phần vào một thực thể:

    • có được chữ ký bit hiện tại của nó ,
    • kích thước thành phần bản đồ đến nhóm thế giới có kích thước khối phù hợp (cá nhân tôi sử dụng boost :: pool) và phân bổ thành phần đó
    • có được chữ ký bit mới của thực thể (chỉ là "chữ ký bit hiện tại" cộng với thành phần mới)
    • Lặp đi lặp lại qua tất cả các hệ thống của thế giới và nếu có một hệ thống có chữ ký không khớp với chữ ký hiện tại của thực thể và không khớp với chữ ký mới, thì rõ ràng chúng ta nên đẩy con trỏ về thực thể của mình ở đó.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

Xóa một thực thể là hoàn toàn tương tự, với sự khác biệt duy nhất mà chúng tôi loại bỏ nếu một hệ thống khớp với chữ ký hiện tại của chúng tôi (có nghĩa là thực thể đó ở đó) và không khớp với chữ ký mới (có nghĩa là thực thể đó sẽ không còn ở đó nữa ).

Bây giờ bạn có thể xem xét việc sử dụng std :: list vì xóa khỏi vectơ là O (n), không đề cập đến việc bạn sẽ phải chuyển một khối dữ liệu lớn mỗi khi bạn xóa từ giữa. Trên thực tế, bạn không phải - vì chúng tôi không quan tâm đến việc xử lý đơn hàng ở cấp độ này, chúng tôi chỉ có thể gọi std :: remove và sống với thực tế là trên mỗi lần xóa, chúng tôi chỉ phải thực hiện tìm kiếm O (n) cho chúng tôi thực thể cần loại bỏ.

std :: list sẽ cung cấp cho bạn loại bỏ O (1) nhưng ở phía bên kia, bạn có thêm một chút chi phí bộ nhớ. Cũng nên nhớ rằng hầu hết thời gian bạn sẽ xử lý các thực thể và không xóa chúng - và điều này chắc chắn được thực hiện nhanh hơn bằng cách sử dụng std :: vector.

Nếu bạn rất quan trọng về hiệu suất, bạn có thể xem xét ngay cả một mẫu truy cập dữ liệu khác , nhưng bằng cách nào đó bạn vẫn duy trì một số "danh sách quan tâm". Mặc dù vậy, hãy nhớ rằng nếu bạn giữ cho API hệ thống thực thể của mình đủ trừu tượng thì sẽ không thành vấn đề để cải thiện các phương thức xử lý thực thể của hệ thống nếu tốc độ khung hình của bạn giảm vì chúng - vì vậy, bây giờ, hãy chọn phương pháp dễ nhất để bạn viết mã - chỉ sau đó hồ sơ và cải thiện nếu cần.


5

Có một cách tiếp cận đáng để xem xét khi mỗi hệ thống sở hữu các thành phần được liên kết với chính nó và các thực thể chỉ đề cập đến chúng. Về cơ bản, Entitylớp (đơn giản hóa) của bạn trông như thế này:

class Entity {
  std::map<ComponentType, Component*> components;
};

Khi bạn nói một RigidBodythành phần gắn liền với một Entity, bạn yêu cầu nó từ Physicshệ thống của bạn . Hệ thống tạo thành phần và cho phép thực thể giữ một con trỏ tới nó. Hệ thống của bạn sau đó trông như:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Bây giờ, điều này có thể trông hơi trực quan lúc đầu nhưng lợi thế nằm ở cách các hệ thống thực thể thành phần cập nhật trạng thái của chúng. Thông thường, bạn sẽ lặp qua các hệ thống của mình và yêu cầu họ cập nhật các thành phần liên quan

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

Điểm mạnh của việc có tất cả các thành phần thuộc sở hữu của hệ thống trong bộ nhớ liền kề là khi hệ thống của bạn lặp lại trên mọi thành phần và cập nhật nó, về cơ bản nó chỉ phải làm

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

Nó không phải lặp đi lặp lại trên tất cả các thực thể có khả năng không có thành phần mà chúng cần cập nhật và nó cũng có khả năng thực hiện bộ đệm rất tốt vì tất cả các thành phần sẽ được lưu trữ liên tục. Đây là một, nếu không phải là lợi thế lớn nhất của phương pháp này. Bạn sẽ thường có hàng trăm và hàng ngàn thành phần tại một thời điểm nhất định, cũng có thể thử và hoạt động tốt nhất có thể.

Tại thời điểm đó, các Worldvòng lặp duy nhất của bạn thông qua các hệ thống và gọi updatechúng mà không cần lặp lại các thực thể. Đó là (imho) thiết kế tốt hơn bởi vì sau đó trách nhiệm của các hệ thống rõ ràng hơn rất nhiều.

Tất nhiên, có vô số thiết kế như vậy, do đó bạn phải đánh giá cẩn thận nhu cầu của trò chơi của mình và chọn phương án phù hợp nhất nhưng đôi khi chúng ta có thể thấy ở đây đôi khi các chi tiết thiết kế nhỏ có thể tạo ra sự khác biệt.


câu trả lời tốt, cảm ơn. nhưng các thành phần không có chức năng (như update ()), chỉ có dữ liệu. và hệ thống xử lý dữ liệu đó. Vì vậy, theo ví dụ của bạn, tôi nên thêm một bản cập nhật ảo cho lớp thành phần và con trỏ thực thể cho mỗi thành phần, phải không?
deniz

@deniz Tất cả phụ thuộc vào thiết kế của bạn. Nếu các thành phần của bạn không có bất kỳ phương thức nào mà chỉ có dữ liệu, hệ thống vẫn có thể lặp lại chúng và thực hiện các hành động cần thiết. Đối với việc liên kết trở lại các thực thể, có, bạn có thể lưu trữ một con trỏ tới thực thể chủ sở hữu trong chính thành phần đó hoặc để hệ thống của bạn duy trì bản đồ giữa các điều khiển thành phần và thực thể. Thông thường, bạn muốn các thành phần của mình càng khép kín càng tốt. Một thành phần hoàn toàn không biết về thực thể mẹ của nó là lý tưởng. Nếu bạn cần giao tiếp theo hướng đó, thích các sự kiện và tương tự.
pwny

Nếu bạn nói nó sẽ tốt hơn cho hiệu quả, tôi sẽ sử dụng mô hình của bạn.
deniz

@deniz Hãy chắc chắn rằng bạn thực sự lập hồ sơ mã sớm và thường xuyên để xác định những gì hoạt động và không cho engin cụ thể của bạn :)
pwny

được rồi :) tôi sẽ làm bài kiểm tra căng thẳng
deniz

1

Theo tôi, một kiến ​​trúc tốt là tạo ra một lớp thành phần trong các thực thể và tách biệt việc quản lý từng hệ thống trong lớp thành phần này. Ví dụ, hệ thống logic có một số thành phần logic ảnh hưởng đến thực thể của chúng và lưu trữ các thuộc tính chung được chia sẻ cho tất cả các thành phần trong thực thể.

Sau đó, nếu bạn muốn quản lý các đối tượng của mỗi hệ thống theo các điểm khác nhau hoặc theo một thứ tự cụ thể, tốt hơn là tạo một danh sách các thành phần hoạt động trong mỗi hệ thống. Tất cả các danh sách các con trỏ mà bạn có thể tạo và quản lý trong các hệ thống đều ít hơn một tài nguyên được tải.

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.