Nhóm các thực thể của cùng một bộ thành phần vào bộ nhớ tuyến tính


11

Chúng tôi bắt đầu từ cách tiếp cận hệ thống-thành phần-thực thể cơ bản .

Hãy tạo tập hợp (hạn có nguồn gốc từ này bài viết) chỉ ra các thông tin về các loại linh kiện . Nó được thực hiện một cách linh hoạt trong thời gian chạy, giống như chúng ta sẽ thêm / xóa từng thành phần cho từng thực thể, nhưng hãy đặt tên chính xác hơn vì nó chỉ là về thông tin loại.

Sau đó, chúng tôi xây dựng các thực thể chỉ định tập hợp cho mỗi người trong số họ. Khi chúng ta tạo thực thể, tập hợp của nó là bất biến, điều đó có nghĩa là chúng ta không thể trực tiếp sửa đổi nó tại chỗ, nhưng chúng ta vẫn có thể lấy chữ ký của thực thể hiện tại thành một bản sao cục bộ (cùng với nội dung), thực hiện các thay đổi phù hợp với nó và tạo ra một thực thể mới của nó

Bây giờ đối với khái niệm khóa: bất cứ khi nào một thực thể được tạo, nó được gán cho một đối tượng được gọi là nhóm tập hợp , có nghĩa là tất cả các thực thể có cùng chữ ký sẽ nằm trong cùng một thùng chứa (ví dụ: trong std :: vector).

Bây giờ các hệ thống chỉ lặp đi lặp lại qua mỗi nhóm lợi ích của họ và thực hiện công việc của họ.

Cách tiếp cận này có một số ưu điểm:

  • các thành phần được lưu trữ trong một vài (chính xác: số lượng thùng) khối bộ nhớ liền kề - điều này giúp cải thiện sự thân thiện với bộ nhớ và dễ dàng hơn trong việc loại bỏ toàn bộ trạng thái trò chơi
  • hệ thống xử lý các thành phần theo cách tuyến tính, có nghĩa là sự kết hợp bộ nhớ cache được cải thiện - tạm biệt từ điển và nhảy bộ nhớ ngẫu nhiên
  • tạo một thực thể mới dễ dàng như ánh xạ tập hợp vào nhóm và đẩy lùi các thành phần cần thiết vào vector của nó
  • xóa một thực thể dễ dàng như một cuộc gọi đến std :: di chuyển để trao đổi phần tử cuối cùng với phần tử bị xóa, bởi vì thứ tự không quan trọng tại thời điểm này

nhập mô tả hình ảnh ở đây

Nếu chúng ta có nhiều thực thể có chữ ký hoàn toàn khác nhau, lợi ích của loại kết hợp bộ nhớ cache giảm dần, nhưng tôi không nghĩ rằng nó sẽ xảy ra trong hầu hết các ứng dụng.

Ngoài ra còn có một vấn đề với việc vô hiệu hóa con trỏ một khi các vectơ được phân bổ lại - điều này có thể được giải quyết bằng cách giới thiệu một cấu trúc như:

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

Vì vậy, bất cứ khi nào vì một lý do nào đó trong logic trò chơi của chúng tôi, chúng tôi muốn theo dõi một thực thể mới được tạo, bên trong nhóm chúng tôi đăng ký một entity_watcher và một khi thực thể đó phải là std :: move'd trong khi gỡ bỏ, chúng tôi sẽ tra cứu người theo dõi và cập nhật real_index_in_vectorgiá trị mới của họ . Hầu hết thời gian này chỉ áp dụng một tra cứu từ điển cho mỗi lần xóa thực thể.

Có bất kỳ nhược điểm hơn cho phương pháp này?

Tại sao giải pháp không được đề cập đến, mặc dù khá rõ ràng?

EDIT : Tôi đang chỉnh sửa câu hỏi để "trả lời câu trả lời", vì ý kiến ​​không đủ.

bạn mất đi tính chất động của các thành phần có thể cắm được, được tạo ra đặc biệt để thoát khỏi việc xây dựng lớp tĩnh.

Tôi không. Có lẽ tôi đã không giải thích nó đủ rõ ràng:

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

Nó đơn giản như chỉ cần lấy chữ ký của thực thể hiện có, sửa đổi nó và tải lên lại như một thực thể mới. Bản chất tự nhiên, năng động ? Tất nhiên. Ở đây tôi muốn nhấn mạnh rằng chỉ có một lớp "tập hợp" và một lớp "xô". Các thùng được điều khiển dữ liệu và được tạo trong thời gian chạy với số lượng tối ưu.

bạn cần phải đi qua tất cả các nhóm có thể chứa mục tiêu hợp lệ. Không có cấu trúc dữ liệu ngoài, việc phát hiện va chạm có thể khó khăn như nhau.

Vâng, đây là lý do tại sao chúng ta có các cấu trúc dữ liệu bên ngoài đã nói ở trên . Cách giải quyết đơn giản như giới thiệu một trình vòng lặp trong lớp Hệ thống phát hiện khi nào nên nhảy sang nhóm tiếp theo. Việc nhảy sẽ hoàn toàn trong suốt theo logic.


Tôi cũng đọc bài viết của Randy Gaul về việc lưu trữ tất cả các thành phần trong vectơ và để hệ thống của họ xử lý chúng. Tôi thấy có hai vấn đề lớn ở đó: nếu tôi chỉ muốn cập nhật một tập hợp con của các thực thể (ví dụ như nghĩ về việc loại bỏ). Do đó các thành phần sẽ được ghép lại với các thực thể. Đối với mỗi bước lặp thành phần tôi cần kiểm tra xem thực thể thuộc về nó đã được chọn để cập nhật chưa. Vấn đề khác là một số hệ thống cần xử lý nhiều loại thành phần khác nhau để lấy lại lợi thế kết hợp bộ đệm. Bất kỳ ý tưởng làm thế nào để đối phó với những vấn đề này?
tiguchi

Câu trả lời:


7

Về cơ bản, bạn đã thiết kế một hệ thống đối tượng tĩnh với bộ cấp phát nhóm và với các lớp động.

Tôi đã viết một hệ thống đối tượng hoạt động gần như giống hệt với hệ thống "tập hợp" của bạn trong thời đi học, mặc dù tôi luôn có xu hướng gọi "tập hợp" là "bản thiết kế" hoặc "bản mẫu" trong các thiết kế của riêng tôi. Kiến trúc này là một nỗi đau ở mông hơn là các hệ thống đối tượng ngây thơ và không có lợi thế hiệu suất có thể đo lường được so với một số thiết kế linh hoạt hơn tôi so sánh với nó. Khả năng tự động sửa đổi một đối tượng mà không cần phải xác định lại hoặc phân bổ lại nó là cực kỳ quan trọng khi bạn đang làm việc với một trình soạn thảo trò chơi. Nhà thiết kế sẽ muốn kéo các thành phần thả vào các định nghĩa đối tượng của bạn. Mã thời gian chạy thậm chí có thể cần phải sửa đổi các thành phần hiệu quả trong một số thiết kế, mặc dù cá nhân tôi không thích điều đó. Tùy thuộc vào cách bạn liên kết các tham chiếu đối tượng trong trình soạn thảo của mình,

Bạn sẽ nhận được sự kết hợp bộ nhớ cache tồi tệ hơn bạn nghĩ trong hầu hết các trường hợp không tầm thường. Chẳng hạn, hệ thống AI của bạn không quan tâm đến Rendercác thành phần nhưng cuối cùng lại bị kẹt khi lặp lại chúng như một phần của mỗi thực thể. Các đối tượng được lặp đi lặp lại lớn hơn và các yêu cầu trong bộ nhớ cache kết thúc với dữ liệu không cần thiết và ít hơn toàn bộ các đối tượng được trả về với mỗi yêu cầu). Nó vẫn sẽ tốt hơn phương thức ngây thơ và thành phần đối tượng phương thức ngây thơ được sử dụng ngay cả trong các công cụ AAA lớn, vì vậy bạn có thể không cần tốt hơn, nhưng ít nhất đừng nghĩ rằng bạn không thể cải thiện nó hơn nữa.

Cách tiếp cận của bạn có ý nghĩa nhất đối với một sốthành phần, nhưng không phải tất cả. Tôi không thích ECS vì nó ủng hộ việc luôn đặt từng thành phần vào một thùng chứa riêng biệt, điều này có ý nghĩa đối với vật lý hoặc đồ họa hoặc không có ý nghĩa gì nếu bạn cho phép nhiều thành phần tập lệnh hoặc AI có thể ghép được. Nếu bạn để hệ thống thành phần được sử dụng không chỉ cho các đối tượng tích hợp mà còn là cách để các nhà thiết kế và lập trình viên trò chơi soạn thảo hành vi của đối tượng thì có thể hợp nhất tất cả các thành phần AI (thường sẽ tương tác) hoặc tất cả các tập lệnh các thành phần (vì bạn muốn cập nhật tất cả chúng trong một đợt). Nếu bạn muốn hệ thống hoạt động hiệu quả nhất thì bạn sẽ cần kết hợp các sơ đồ phân bổ và lưu trữ thành phần và dành thời gian để tìm ra kết luận phù hợp nhất cho từng loại thành phần cụ thể.


Tôi đã nói: chúng tôi không thể thay đổi chữ ký của thực thể và tôi có nghĩa là chúng tôi không thể trực tiếp sửa đổi tại chỗ, nhưng chúng tôi vẫn có thể lấy tập hợp hiện có thành một bản sao cục bộ, thay đổi và tải lên lại dưới dạng một thực thể mới - và những thứ này hoạt động khá rẻ, như tôi đã chỉ ra trong câu hỏi. Một lần nữa - chỉ có MỘT lớp "xô". "Tập hợp" / "Chữ ký" / "hãy đặt tên theo cách chúng tôi muốn" có thể được tạo động khi chạy theo cách tiếp cận tiêu chuẩn, tôi thậm chí sẽ đi xa hơn khi nghĩ về một thực thể là "chữ ký".
Patryk Czachurski

Và tôi đã nói rằng bạn không nhất thiết muốn đối phó với sự thống nhất. "Tạo một thực thể mới" có khả năng có nghĩa là phá vỡ tất cả các tay cầm hiện có cho thực thể, tùy thuộc vào cách hệ thống xử lý của bạn hoạt động. Cuộc gọi của bạn nếu chúng đủ rẻ hay không. Tôi thấy nó chỉ là một cơn đau ở mông phải đối phó.
Sean Middleditch

Được rồi, bây giờ tôi đã có quan điểm của bạn về điều này. Dù sao, tôi nghĩ rằng ngay cả khi việc thêm / xóa đắt hơn một chút, nó vẫn xảy ra rất đơn giản đến mức nó vẫn có giá trị đơn giản hóa rất nhiều quá trình truy cập các thành phần, xảy ra trong thời gian thực. Vì vậy, chi phí "thay đổi" là không đáng kể. Về ví dụ AI của bạn, không phải vẫn còn giá trị cho một vài hệ thống mà dù sao cũng cần dữ liệu từ nhiều thành phần?
Patryk Czachurski

Quan điểm của tôi là AI là nơi tiếp cận của bạn tốt hơn, nhưng đối với các thành phần khác thì không nhất thiết phải như vậy.
Sean Middleditch

4

Những gì bạn đã làm là tái thiết kế các đối tượng C ++. Lý do tại sao điều này cảm thấy rõ ràng là nếu bạn thay thế từ "thực thể" bằng "lớp" và "thành phần" bằng "thành viên" thì đây là một thiết kế OOP tiêu chuẩn sử dụng mixins.

1) bạn mất bản chất động của các thành phần có thể cắm được, được tạo riêng để thoát khỏi cấu trúc lớp tĩnh.

2) sự gắn kết bộ nhớ là quan trọng nhất trong một loại dữ liệu, không phải trong một đối tượng thống nhất nhiều loại dữ liệu ở một nơi. Đây là một trong những lý do mà các hệ thống thành phần + được tạo ra, để thoát khỏi phân đoạn bộ nhớ đối tượng lớp +.

3) thiết kế này cũng trở lại kiểu lớp C ++ vì bạn đang nghĩ thực thể là một đối tượng mạch lạc khi, trong thiết kế thành phần + hệ thống, thực thể chỉ là một thẻ / ID để làm cho hoạt động bên trong trở nên dễ hiểu đối với con người.

4) thật dễ dàng để một thành phần tự tuần tự hóa nó hơn là một đối tượng phức tạp để tuần tự hóa nhiều thành phần trong chính nó, nếu không thực sự dễ dàng theo dõi như một lập trình viên.

5) bước hợp lý tiếp theo trong đường dẫn này là loại bỏ Hệ thống và đưa mã đó trực tiếp vào thực thể, nơi nó có tất cả dữ liệu cần thiết để hoạt động. Tất cả chúng ta đều có thể thấy những gì ngụ ý =)


2) có thể tôi không hiểu bộ nhớ đệm đầy đủ, nhưng giả sử có một Hệ thống hoạt động với 10 thành phần. Theo cách tiếp cận tiêu chuẩn, xử lý mỗi thực thể có nghĩa là truy cập RAM 10 lần, bởi vì các thành phần nằm rải rác ở những nơi ngẫu nhiên trong bộ nhớ, ngay cả khi các nhóm được sử dụng - bởi vì các thành phần khác nhau thuộc về các nhóm khác nhau. Sẽ không phải là "quan trọng" để lưu trữ toàn bộ thực thể cùng một lúc và xử lý tất cả các thành phần mà không bỏ lỡ bộ đệm nào, mà thậm chí không phải thực hiện tra cứu từ điển? Ngoài ra, tôi đã thực hiện một chỉnh sửa để bao quát điểm 1)
Patryk Czachurski

@Sean Middleditch có một mô tả hay về sự cố bộ nhớ đệm này trong câu trả lời của anh ấy.
Patrick Hughes

3) Chúng không phải là đối tượng kết hợp theo bất kỳ cách nào. Về thành phần A nằm ngay cạnh thành phần B trong bộ nhớ, nó chỉ là "sự kết hợp bộ nhớ", không phải là "sự kết hợp logic", như John đã chỉ ra. Các thùng, trên sự sáng tạo của chúng, thậm chí có thể xáo trộn các thành phần chữ ký theo bất kỳ trật tự và nguyên tắc mong muốn nào vẫn sẽ được duy trì. 4) có thể dễ dàng "theo dõi" như nhau nếu chúng ta có đủ sự trừu tượng - điều chúng ta đang nói chỉ là một sơ đồ lưu trữ được cung cấp với các trình lặp và có lẽ một bản đồ bù byte có thể giúp xử lý dễ dàng như trong cách tiếp cận tiêu chuẩn.
Patryk Czachurski

5) Và tôi không nghĩ bất cứ điều gì trong ý tưởng này đều chỉ ra hướng này. Không phải là tôi không muốn đồng ý với bạn, tôi chỉ tò mò cuộc thảo luận này có thể dẫn đến đâu, mặc dù vậy nó có thể sẽ dẫn đến loại "đo lường" hoặc "tối ưu hóa sớm" nổi tiếng. :)
Patryk Czachurski

@PatrykCzachurski nhưng hệ thống của bạn không hoạt động với 10 thành phần.
dùng253751

3

Giữ các thực thể cùng nhau không quan trọng như bạn nghĩ, đó là lý do tại sao thật khó để nghĩ ra một lý do hợp lệ nào khác ngoài "vì đó là một đơn vị". Nhưng vì bạn thực sự đang làm điều này cho sự kết hợp bộ nhớ cache trái ngược với sự kết hợp logic, nó có thể có ý nghĩa.

Một khó khăn bạn có thể có là sự tương tác giữa các thành phần trong các thùng khác nhau. Chẳng hạn, việc tìm kiếm thứ gì đó mà AI của bạn có thể bắn vào chẳng hạn, bạn cần phải đi qua tất cả các thùng có thể chứa mục tiêu hợp lệ. Không có cấu trúc dữ liệu ngoài, việc phát hiện va chạm có thể khó khăn như nhau.

Để tiếp tục về việc tổ chức các thực thể với nhau để thống nhất logic, lý do duy nhất mà tôi có thể phải giữ các thực thể cùng nhau là vì mục đích nhận dạng trong các nhiệm vụ của tôi. Tôi cần biết nếu bạn vừa tạo thực thể loại A hoặc loại B, và tôi đã khắc phục điều này bằng cách ... bạn đoán nó: thêm một thành phần mới xác định tập hợp đặt thực thể này lại với nhau. Ngay cả sau đó, tôi không tập hợp tất cả các thành phần lại với nhau cho một nhiệm vụ lớn, tôi chỉ cần biết nó là gì. Vì vậy, tôi không nghĩ rằng phần này là rất hữu ích.


Tôi phải thừa nhận tôi không hiểu câu trả lời của bạn. Bạn có ý nghĩa gì bởi "sự gắn kết logic"? Về những khó khăn trong tương tác, tôi đã thực hiện một chỉnh sửa.
Patryk Czachurski

"Sự kết hợp logic", như trong: Nó có "ý nghĩa logic" để giữ tất cả các thành phần tạo nên một thực thể Cây gần nhau.
John McDonald
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.