Hai lợi ích chính mà tôi liên tục nghe thấy được khen ngợi về các hệ thống thực thể là 1) việc xây dựng dễ dàng các loại thực thể mới do không phải vướng vào hệ thống phân cấp thừa kế phức tạp và 2) hiệu quả bộ đệm.
Lưu ý rằng (1) là một lợi ích của thiết kế dựa trên thành phần , không chỉ ES / ECS. Bạn có thể sử dụng các thành phần theo nhiều cách không có phần "hệ thống" và chúng hoạt động tốt (và nhiều trò chơi indie và AAA sử dụng các kiến trúc như vậy).
Mô hình đối tượng Unity tiêu chuẩn (sử dụng GameObject
và MonoBehaviour
đối tượng) không phải là ECS, mà là thiết kế dựa trên thành phần. Tất nhiên, tính năng Unity ECS mới là một ECS thực tế.
Các hệ thống cần có khả năng làm việc với nhiều hơn một thành phần, tức là cả hệ thống kết xuất và vật lý cần truy cập vào thành phần biến đổi.
Một số ECS sắp xếp các bộ chứa thành phần của chúng theo ID thực thể, nghĩa là các thành phần tương ứng trong mỗi nhóm sẽ theo cùng một thứ tự.
Điều này có nghĩa là nếu bạn lặp tuyến tính trên thành phần đồ họa, bạn cũng lặp lại tuyến tính trên các thành phần biến đổi tương ứng. Bạn có thể bỏ qua một số biến đổi (vì bạn có thể có các khối kích hoạt vật lý mà bạn không kết xuất hoặc tương tự) nhưng vì bạn luôn bỏ qua về phía trước trong bộ nhớ (và thông thường không phải là khoảng cách rất lớn), bạn vẫn sẽ đi để đạt được hiệu quả.
Điều này tương tự như cách bạn có Cấu trúc mảng (SOA) là cách tiếp cận được đề xuất cho HPC. CPU và bộ đệm có thể xử lý nhiều mảng tuyến tính gần như cũng có thể xử lý một mảng tuyến tính duy nhất và tốt hơn nhiều so với khả năng truy cập bộ nhớ ngẫu nhiên.
Một chiến lược khác được sử dụng trong một số triển khai ECS - bao gồm Unity ECS - là phân bổ các Thành phần dựa trên Archetype của Thực thể tương ứng của chúng. Đó là, tất cả các thực thể với một cách chính xác các bộ Components ( PhysicsBody
, Transform
) sẽ được phân bổ một cách riêng biệt từ Entities với các thành phần khác nhau (ví dụ PhysicsBody
, Transform
, và Renderable
).
Các hệ thống trong các thiết kế như vậy hoạt động bằng cách trước tiên tìm tất cả các Archetype phù hợp với yêu cầu của chúng (có bộ Thành phần bắt buộc), lặp lại danh sách Archetypes đó và lặp lại các Thành phần được lưu trữ trong mỗi Archetype phù hợp. Điều này cho phép truy cập thành phần O (1) hoàn toàn tuyến tính và thực trong Archetype và cho phép Hệ thống tìm các Thực thể tương thích với chi phí rất thấp (bằng cách tìm kiếm một danh sách nhỏ Archetypes thay vì tìm kiếm hàng trăm nghìn Thực thể).
Bạn có thể có các thành phần lưu trữ con trỏ tới các thành phần khác hoặc con trỏ tới các thực thể lưu trữ con trỏ tới các thành phần.
Các thành phần tham chiếu các thành phần khác trên cùng một thực thể không cần lưu trữ bất cứ thứ gì. Để tham chiếu các thành phần trên các thực thể khác, chỉ cần lưu trữ ID thực thể.
Nếu một thành phần được phép tồn tại nhiều lần cho một thực thể và bạn cần tham chiếu một thể hiện cụ thể, lưu trữ ID của thực thể khác và chỉ mục thành phần cho thực thể đó. Tuy nhiên, nhiều triển khai ECS không cho phép trường hợp này, đặc biệt vì nó làm cho các hoạt động này kém hiệu quả hơn.
Bạn có thể đảm bảo rằng mọi mảng thành phần đều lớn, trong đó 'n' là số lượng thực thể còn tồn tại trong hệ thống
Sử dụng các thẻ điều khiển (ví dụ: chỉ mục + đánh dấu thế hệ) và không phải con trỏ và sau đó bạn có thể thay đổi kích thước các mảng mà không sợ phá vỡ các tham chiếu đối tượng.
Bạn cũng có thể sử dụng cách tiếp cận "mảng chunked" (một mảng các mảng) tương tự như nhiều cách std::deque
triển khai phổ biến (mặc dù không có kích thước khối nhỏ đáng kể của các triển khai đã nói) nếu bạn muốn cho phép con trỏ vì một số lý do hoặc nếu bạn đã đo lường vấn đề với mảng thay đổi kích thước hiệu suất.
Thứ hai, đây là tất cả giả định rằng các thực thể được xử lý tuyến tính trong một danh sách mỗi khung hình / đánh dấu, nhưng trong thực tế, điều này không thường xảy ra
Nó phụ thuộc vào thực thể. Có, đối với nhiều trường hợp sử dụng, nó không đúng. Thật vậy, đây là lý do tại sao tôi nhấn mạnh mạnh mẽ sự khác biệt giữa thiết kế dựa trên thành phần (tốt) và hệ thống thực thể (một hình thức cụ thể của CBD).
Một số thành phần của bạn chắc chắn sẽ dễ dàng xử lý tuyến tính. Ngay cả trong các trường hợp sử dụng "cây nặng" thông thường, chúng tôi chắc chắn đã thấy hiệu suất tăng từ việc sử dụng các mảng được đóng gói chặt chẽ (chủ yếu trong các trường hợp liên quan đến N vài trăm, như các tác nhân AI trong một trò chơi thông thường).
Một số nhà phát triển cũng nhận thấy rằng những lợi thế về hiệu suất của việc sử dụng các cấu trúc dữ liệu được phân bổ tuyến tính theo định hướng dữ liệu vượt xa lợi thế về hiệu suất của việc sử dụng các cấu trúc dựa trên cây "thông minh hơn". Tất cả phụ thuộc vào trò chơi và các trường hợp sử dụng cụ thể, tất nhiên.
Giả sử bạn sử dụng trình kết xuất sector / cổng thông tin hoặc octree để thực hiện loại bỏ tắc. Bạn có thể lưu trữ các thực thể liên tục trong một khu vực / nút, nhưng bạn sẽ nhảy xung quanh dù bạn có thích hay không.
Bạn sẽ ngạc nhiên bao nhiêu mảng vẫn giúp. Bạn đang nhảy xung quanh trong một vùng bộ nhớ nhỏ hơn nhiều so với "bất cứ nơi nào" và thậm chí với tất cả các bước nhảy, bạn vẫn có nhiều khả năng kết thúc một thứ gì đó trong bộ nhớ cache. Với một cây có kích thước nhất định hoặc ít hơn, bạn thậm chí có thể tìm nạp trước toàn bộ vào bộ đệm và không bao giờ bỏ lỡ bộ đệm trên cây đó.
Ngoài ra còn có các cấu trúc cây được xây dựng để sống trong các mảng được đóng gói chặt chẽ. Chẳng hạn, với quãng tám của bạn, bạn có thể sử dụng cấu trúc giống như đống (cha mẹ trước con cái, anh chị em cạnh nhau) và đảm bảo rằng ngay cả khi bạn "đi sâu" vào cây bạn luôn lặp đi lặp lại trong mảng, điều này giúp CPU tối ưu hóa truy cập bộ nhớ / tra cứu bộ nhớ cache.
Đó là một điểm quan trọng để thực hiện. CPU x86 là một con thú phức tạp. CPU đang chạy một trình tối ưu hóa vi mã trên mã máy của bạn một cách hiệu quả, chia nó thành các vi lệnh nhỏ hơn và sắp xếp lại các hướng dẫn, dự đoán các mẫu truy cập bộ nhớ, v.v. Các mẫu truy cập dữ liệu quan trọng hơn có thể dễ hiểu nếu tất cả những gì bạn có là sự hiểu biết ở mức độ cao CPU hoặc bộ đệm hoạt động như thế nào.
Sau đó, bạn có các hệ thống khác, có thể thích các thực thể được lưu trữ theo một số thứ tự khác.
Bạn có thể lưu trữ chúng nhiều lần. Khi bạn loại bỏ các mảng của mình xuống các chi tiết tối thiểu, bạn có thể thấy bạn thực sự tiết kiệm bộ nhớ (vì bạn đã loại bỏ các con trỏ 64 bit của mình và có thể sử dụng các chỉ số nhỏ hơn) với phương pháp này.
Bạn có thể xen kẽ mảng thực thể của mình thay vì giữ các mảng riêng biệt, nhưng bạn vẫn lãng phí bộ nhớ
Điều này là phản đối với việc sử dụng bộ nhớ cache tốt. Nếu tất cả những gì bạn quan tâm là các biến đổi và dữ liệu đồ họa, tại sao làm cho máy dành thời gian để lấy tất cả các dữ liệu khác cho vật lý và AI và nhập và gỡ lỗi, v.v.
Đó là điểm thường được đưa ra có lợi cho ECS so với các đối tượng trò chơi nguyên khối (mặc dù không thực sự áp dụng khi so sánh với các kiến trúc dựa trên thành phần khác).
Đối với những gì nó có giá trị, hầu hết các triển khai ECS "cấp sản xuất" mà tôi biết sử dụng lưu trữ xen kẽ. Cách tiếp cận Archetype phổ biến mà tôi đã đề cập trước đó (ví dụ được sử dụng trong Unity ECS) được xây dựng rất rõ ràng để sử dụng lưu trữ xen kẽ cho các Thành phần được liên kết với Archetype.
AI là vô nghĩa nếu nó không thể ảnh hưởng đến trạng thái biến đổi hoặc hoạt hình được sử dụng để hiển thị của một thực thể.
Chỉ vì AI không thể truy cập một cách hiệu quả dữ liệu biến đổi tuyến tính không có nghĩa là không có hệ thống nào khác có thể sử dụng tối ưu hóa bố cục dữ liệu đó một cách hiệu quả. Bạn có thể sử dụng một mảng đóng gói để chuyển đổi dữ liệu mà không ngăn các hệ thống logic trò chơi thực hiện mọi thứ theo cách thông thường mà các hệ thống logic trò chơi thường làm.
Bạn cũng đang quên bộ đệm mã . Khi bạn sử dụng cách tiếp cận hệ thống của ECS (không giống như một số kiến trúc thành phần ngây thơ hơn), bạn sẽ đảm bảo rằng bạn đang chạy cùng một vòng mã nhỏ và không nhảy qua lại các bảng chức năng ảo để phân loại các Update
hàm ngẫu nhiên rải rác khắp nơi nhị phân của bạn. Vì vậy, trong trường hợp AI, bạn thực sự muốn giữ tất cả các thành phần AI khác nhau của mình (vì chắc chắn bạn có nhiều hơn một để bạn có thể soạn các hành vi!) Trong các thùng riêng biệt và xử lý từng danh sách riêng biệt để có được cách sử dụng bộ đệm mã tốt nhất.
Với hàng đợi sự kiện bị trì hoãn (trong đó một hệ thống tạo danh sách các sự kiện nhưng không gửi chúng cho đến khi hệ thống xử lý xong tất cả các thực thể), bạn có thể đảm bảo rằng bộ đệm mã của bạn được sử dụng tốt trong khi vẫn giữ các sự kiện.
Sử dụng một cách tiếp cận trong đó mỗi hệ thống biết hàng đợi sự kiện nào cần đọc ra cho khung, bạn thậm chí có thể thực hiện việc đọc các sự kiện nhanh chóng. Hoặc nhanh hơn không có, ít nhất.
Hãy nhớ rằng, hiệu suất không phải là tuyệt đối. Bạn không cần phải loại bỏ mọi lỗi bộ nhớ cache cuối cùng để bắt đầu thấy lợi ích hiệu suất của thiết kế hướng dữ liệu tốt.
Vẫn còn có nghiên cứu tích cực để làm cho nhiều hệ thống trò chơi hoạt động tốt hơn với kiến trúc ECS và các mẫu thiết kế hướng dữ liệu. Tương tự như một số điều tuyệt vời mà chúng ta đã thấy với SIMD trong những năm gần đây (ví dụ: trình phân tích cú pháp JSON), chúng ta đang thấy ngày càng nhiều thứ được thực hiện với kiến trúc ECS dường như không trực quan với kiến trúc trò chơi cổ điển nhưng cung cấp một số lợi ích (tốc độ, đa luồng, khả năng kiểm tra, vv).
Hoặc có lẽ có một cách tiếp cận hỗn hợp mà mọi người đang sử dụng nhưng không ai nói về
Đây là những gì tôi đã ủng hộ trong quá khứ, đặc biệt là đối với những người hoài nghi về kiến trúc ECS: sử dụng các phương pháp tiếp cận định hướng dữ liệu tốt cho các thành phần có hiệu suất rất quan trọng. Sử dụng kiến trúc đơn giản hơn, nơi đơn giản cải thiện thời gian phát triển. Không bấm còi mỗi thành phần đơn lẻ thành một định nghĩa quá mức nghiêm ngặt về thành phần hóa như ECS đề xuất. Phát triển kiến trúc thành phần của bạn theo cách mà bạn có thể dễ dàng sử dụng các cách tiếp cận giống như ECS trong đó chúng có ý nghĩa và sử dụng cấu trúc thành phần đơn giản hơn trong đó cách tiếp cận giống như ECS không có ý nghĩa (hoặc ít ý nghĩa hơn cấu trúc cây, v.v.) .
Cá nhân tôi là một người chuyển đổi tương đối gần đây thành sức mạnh thực sự của ECS. Mặc dù đối với tôi, yếu tố quyết định là điều hiếm khi được đề cập về ECS: nó làm cho các bài kiểm tra viết cho các hệ thống trò chơi và logic gần như không đáng kể so với các thiết kế dựa trên thành phần logic được kết hợp chặt chẽ mà tôi đã làm việc trước đây. Do các kiến trúc ECS đưa tất cả logic vào Hệ thống, vốn chỉ tiêu thụ Thành phần và tạo ra các bản cập nhật Thành phần, nên việc xây dựng một bộ Thành phần "giả" để kiểm tra hành vi của Hệ thống khá dễ dàng; bởi vì hầu hết logic trò chơi chỉ nên tồn tại bên trong Hệ thống, điều đó có nghĩa là việc kiểm tra tất cả các Hệ thống của bạn sẽ cung cấp độ bao phủ mã khá cao cho logic trò chơi của bạn. Các hệ thống có thể sử dụng các phụ thuộc giả (ví dụ: giao diện GPU) cho các thử nghiệm với mức độ phức tạp hoặc tác động hiệu suất ít hơn nhiều so với bạn '
Bên cạnh đó, bạn có thể lưu ý rằng rất nhiều người nói về ECS mà không thực sự hiểu nó là gì. Tôi thấy Unity cổ điển được gọi là ECS với tần suất giảm, minh họa rằng có quá nhiều nhà phát triển trò chơi đánh đồng "ECS" với "Thành phần" và hoàn toàn bỏ qua phần "Hệ thống thực thể". Bạn thấy rất nhiều tình yêu chất đống trên ECS trên Internet khi một phần lớn mọi người thực sự chỉ ủng hộ thiết kế dựa trên thành phần, chứ không phải ECS thực tế. Tại thời điểm này, gần như vô nghĩa để tranh luận về nó; ECS đã bị hỏng từ nghĩa gốc của nó thành một thuật ngữ chung và bạn cũng có thể chấp nhận rằng "ECS" không có nghĩa tương tự như "ECS hướng dữ liệu". : /