OOP ECS so với ECS thuần túy


11

Đầu tiên, tôi biết rằng câu hỏi này liên quan đến chủ đề phát triển trò chơi nhưng tôi đã quyết định hỏi nó ở đây vì nó thực sự dẫn đến một vấn đề chung về phần mềm.

Trong tháng vừa qua, tôi đã đọc rất nhiều về Entity-Thành phần-Hệ thống và bây giờ khá thoải mái với khái niệm này. Tuy nhiên, có một khía cạnh dường như thiếu một "định nghĩa" rõ ràng và các bài viết khác nhau đã đề xuất các giải pháp hoàn toàn khác nhau:

Đây là câu hỏi liệu ECS có nên phá vỡ đóng gói hay không. Nói cách khác, ECS kiểu OOP của nó (các thành phần là các đối tượng có cả trạng thái và hành vi đóng gói dữ liệu cụ thể cho chúng) so với ECS thuần túy (các thành phần là cấu trúc kiểu c chỉ có dữ liệu công khai và hệ thống cung cấp chức năng).

Lưu ý rằng tôi đang phát triển Framework / API / Engine. Vì vậy, mục tiêu là nó có thể dễ dàng được mở rộng bởi bất cứ ai đang sử dụng nó. Điều này bao gồm những thứ như thêm một loại kết xuất hoặc thành phần va chạm mới.

Các vấn đề với phương pháp OOP

  • Các thành phần phải truy cập dữ liệu của các thành phần khác. Ví dụ: phương thức vẽ của thành phần kết xuất phải truy cập vào vị trí của thành phần biến đổi. Điều này tạo ra sự phụ thuộc trong mã.

  • Các thành phần có thể là đa hình mà tiếp tục giới thiệu một số phức tạp. Ví dụ: Có thể có một thành phần kết xuất sprite ghi đè phương thức vẽ ảo của thành phần kết xuất.

Các vấn đề với cách tiếp cận thuần túy

  • Vì hành vi đa hình (ví dụ để kết xuất) phải được thực hiện ở đâu đó, nên nó chỉ được gia công vào các hệ thống. (ví dụ: hệ thống kết xuất sprite tạo nút kết xuất sprite kế thừa nút kết xuất và thêm nó vào công cụ kết xuất)

  • Việc liên lạc giữa các hệ thống có thể khó tránh. Ví dụ, hệ thống va chạm có thể cần hộp giới hạn được tính từ bất kỳ thành phần kết xuất cụ thể nào. Điều này có thể được giải quyết bằng cách cho phép họ giao tiếp qua dữ liệu. Tuy nhiên, điều này loại bỏ các cập nhật tức thời vì hệ thống kết xuất sẽ cập nhật thành phần hộp giới hạn và hệ thống va chạm sau đó sẽ sử dụng nó. Điều này có thể dẫn đến các biểu tượng nếu thứ tự gọi các chức năng cập nhật của hệ thống không được xác định. Có một hệ thống sự kiện tại chỗ cho phép các hệ thống đưa ra các sự kiện mà các hệ thống khác có thể đăng ký xử lý của chúng. Tuy nhiên, điều này chỉ hoạt động để báo cho các hệ thống biết phải làm gì tức là các hàm void.

  • Có cờ bổ sung cần thiết. Lấy một thành phần bản đồ gạch chẳng hạn. Nó sẽ có một kích thước, kích thước gạch và trường danh sách chỉ mục. Hệ thống bản đồ ô vuông sẽ xử lý mảng đỉnh tương ứng và gán tọa độ kết cấu dựa trên dữ liệu của thành phần. Tuy nhiên, tính toán lại toàn bộ tilemap mỗi khung hình là đắt tiền. Do đó, cần có một danh sách để theo dõi tất cả các thay đổi được thực hiện để sau đó cập nhật chúng trong hệ thống. Theo cách OOP, điều này có thể được gói gọn bởi thành phần bản đồ ô vuông. Ví dụ, phương thức SetTile () sẽ cập nhật mảng đỉnh bất cứ khi nào nó được gọi.

Mặc dù tôi thấy vẻ đẹp của cách tiếp cận thuần túy, tôi thực sự không hiểu loại lợi ích cụ thể nào sẽ mang lại cho một OOP truyền thống hơn. Sự phụ thuộc giữa các thành phần vẫn tồn tại mặc dù bị ẩn đi trong các hệ thống. Ngoài ra tôi sẽ cần nhiều lớp hơn để hoàn thành cùng một mục tiêu. Điều này đối với tôi giống như một giải pháp được thiết kế quá mức mà không bao giờ là một điều tốt.

Hơn nữa, tôi không can thiệp vào hiệu suất nên toàn bộ ý tưởng về thiết kế hướng dữ liệu và các khoản tiền mặt không thực sự quan trọng với tôi. Tôi chỉ muốn một kiến ​​trúc đẹp ^^

Tuy nhiên, hầu hết các bài báo và thảo luận tôi đọc đề xuất cách tiếp cận thứ hai. TẠI SAO?

Hoạt hình

Cuối cùng, tôi muốn đặt câu hỏi về cách tôi sẽ xử lý hoạt hình trong một ECS thuần túy. Hiện tại tôi đã định nghĩa một hoạt hình là một functor thao túng một thực thể dựa trên một số tiến trình từ 0 đến 1. Thành phần hoạt hình có một danh sách các hoạt hình có một danh sách các hoạt hình. Trong chức năng cập nhật của nó, sau đó nó áp dụng bất kỳ hình ảnh động nào hiện đang hoạt động cho thực thể.

Ghi chú:

Tôi vừa đọc bài viết này Có phải đối tượng kiến ​​trúc Hệ thống Thành phần Thực thể được định hướng theo định nghĩa không? Điều này giải thích vấn đề tốt hơn tôi một chút. Mặc dù về cơ bản là cùng một chủ đề, nó vẫn không đưa ra bất kỳ câu trả lời nào về lý do tại sao phương pháp tiếp cận dữ liệu thuần túy lại tốt hơn.


1
Có lẽ một câu hỏi đơn giản nhưng nghiêm túc: bạn có biết những lợi thế / bất lợi của ECS không? Điều đó chủ yếu giải thích "tại sao".
Caramiriel

Vâng, tôi hiểu lợi thế của việc sử dụng các thành phần tức là thành phần chứ không phải thừa kế để tránh kim cương của cái chết thông qua nhiều thừa kế. Sử dụng các thành phần cũng cho phép thao tác Behaivour khi chạy. Và chúng là mô-đun. Điều tôi không hiểu là tại sao phải phân chia dữ liệu và chức năng. Triển khai hiện tại của tôi là trên github github.com/AdrianKoch3010/MarsBaseProject
Adrian Koch

Vâng, tôi chưa có đủ kinh nghiệm với ECS để thêm câu trả lời đầy đủ. Nhưng thành phần không chỉ được sử dụng để tránh DoD; bạn cũng có thể tạo các thực thể (duy nhất) trong thời gian chạy khó (er) để tạo bằng cách sử dụng phương pháp OO. Điều đó nói rằng, chia dữ liệu / thủ tục cho phép dữ liệu dễ dàng hơn để lý do. Bạn có thể thực hiện tuần tự hóa, lưu trạng thái, hoàn tác / làm lại và những thứ tương tự một cách dễ dàng. Vì dễ dàng lý luận về dữ liệu, nó cũng dễ dàng tối ưu hóa dữ liệu hơn. Bạn rất có thể có thể phân chia các thực thể theo lô (đa luồng) hoặc thậm chí giảm tải nó sang phần cứng khác để có được tiềm năng đầy đủ của nó.
Caramiriel

"Có thể có một thành phần kết xuất sprite ghi đè phương thức vẽ ảo của thành phần kết xuất." Tôi sẽ lập luận rằng đó không phải là ECS nữa nếu bạn làm / yêu cầu điều đó.
wonderra

Câu trả lời:


10

Đây là một trong những khó khăn. Tôi sẽ chỉ cố gắng giải quyết một số câu hỏi dựa trên kinh nghiệm cụ thể của tôi (YMMV):

Các thành phần phải truy cập dữ liệu của các thành phần khác. Ví dụ: phương thức vẽ của thành phần kết xuất phải truy cập vào vị trí của thành phần biến đổi. Điều này tạo ra sự phụ thuộc trong mã.

Đừng đánh giá thấp số lượng và độ phức tạp (không phải mức độ) của khớp nối / phụ thuộc ở đây. Bạn có thể xem xét sự khác biệt giữa điều này (và sơ đồ này đã được đơn giản hóa một cách lố bịch đến mức giống như đồ chơi và ví dụ trong thế giới thực sẽ có giao diện ở giữa để nới lỏng khớp nối):

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

... và điều này:

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

... Hoặc cái này:

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

Các thành phần có thể là đa hình mà tiếp tục giới thiệu một số phức tạp. Ví dụ: Có thể có một thành phần kết xuất sprite ghi đè phương thức vẽ ảo của thành phần kết xuất.

Vì thế? Tương tự (hoặc bằng chữ) tương đương với một công văn vtable và ảo có thể được gọi thông qua hệ thống thay vì đối tượng ẩn trạng thái / dữ liệu cơ bản của nó. Đa hình vẫn rất thực tế và khả thi với việc triển khai ECS "thuần túy" khi vtable hoặc con trỏ hàm tương tự biến thành "dữ liệu" các loại để hệ thống gọi.

Vì hành vi đa hình (ví dụ để kết xuất) phải được thực hiện ở đâu đó, nên nó chỉ được gia công vào các hệ thống. (ví dụ: hệ thống kết xuất sprite tạo nút kết xuất sprite kế thừa nút kết xuất và thêm nó vào công cụ kết xuất)

Vì thế? Tôi hy vọng điều này không xảy ra như một sự mỉa mai (không phải ý định của tôi mặc dù tôi đã bị buộc tội thường xuyên nhưng tôi ước tôi có thể giao tiếp cảm xúc tốt hơn qua văn bản), nhưng hành vi đa hình "gia công" trong trường hợp này không nhất thiết phải phát sinh thêm Chi phí đến năng suất.

Việc liên lạc giữa các hệ thống có thể khó tránh. Ví dụ, hệ thống va chạm có thể cần hộp giới hạn được tính từ bất kỳ thành phần kết xuất cụ thể nào.

Ví dụ này có vẻ đặc biệt kỳ lạ với tôi. Tôi không biết tại sao trình kết xuất sẽ xuất dữ liệu trở lại cảnh (tôi thường xem xét trình kết xuất chỉ đọc trong ngữ cảnh này) hoặc để trình kết xuất tìm ra AABB thay vì một số hệ thống khác thực hiện điều này cho cả trình kết xuất và va chạm / vật lý (tôi có thể bị treo lên tên "kết xuất thành phần" ở đây). Tuy nhiên, tôi không muốn quá bận tâm về ví dụ này vì tôi nhận ra đó không phải là điểm bạn đang cố gắng thực hiện. Tuy nhiên, việc liên lạc giữa các hệ thống (ngay cả ở dạng đọc / ghi gián tiếp vào cơ sở dữ liệu ECS trung tâm với các hệ thống phụ thuộc trực tiếp vào các biến đổi do người khác thực hiện) không cần phải thường xuyên, nếu cần thiết. Cái đó'

Điều này có thể dẫn đến các biểu tượng nếu thứ tự gọi các chức năng cập nhật của hệ thống không được xác định.

Điều này hoàn toàn nên được xác định. ECS không phải là giải pháp cuối cùng để sắp xếp lại thứ tự đánh giá xử lý hệ thống của mọi hệ thống có thể có trong cơ sở mã và lấy lại chính xác loại kết quả cho người dùng cuối xử lý các khung và FPS. Đây là một trong những điều, khi thiết kế ECS, ít nhất tôi nên đề xuất một cách mạnh mẽ nên được dự đoán trước một chút (mặc dù có rất nhiều phòng thở tha thứ để thay đổi suy nghĩ sau đó miễn là nó không làm thay đổi các khía cạnh quan trọng nhất của trật tự gọi / đánh giá hệ thống).

Tuy nhiên, tính toán lại toàn bộ tilemap mỗi khung hình là đắt tiền. Do đó, cần có một danh sách để theo dõi tất cả các thay đổi được thực hiện để sau đó cập nhật chúng trong hệ thống. Theo cách OOP, điều này có thể được gói gọn bởi thành phần bản đồ ô vuông. Ví dụ, phương thức SetTile () sẽ cập nhật mảng đỉnh bất cứ khi nào nó được gọi.

Tôi hoàn toàn không hiểu điều này ngoại trừ việc nó là mối quan tâm theo hướng dữ liệu. Và không có cạm bẫy nào trong việc đại diện và lưu trữ dữ liệu trong ECS, bao gồm ghi nhớ, để tránh những cạm bẫy hiệu suất đó (những lỗi lớn nhất với ECS có xu hướng liên quan đến những thứ như hệ thống truy vấn các trường hợp cụ thể có sẵn của một loại thành phần cụ thể là một trong những các khía cạnh thách thức nhất của việc tối ưu hóa một ECS tổng quát). Thực tế là logic và dữ liệu được phân tách trong một ECS "thuần túy" không có nghĩa là bạn đột nhiên phải tính toán lại những thứ mà bạn có thể đã lưu vào bộ nhớ cache / ghi nhớ trong một đại diện OOP. Đó là một điểm moot / không liên quan trừ khi tôi che đậy điều gì đó rất quan trọng.

Với ECS "thuần túy", bạn vẫn có thể lưu trữ dữ liệu này trong thành phần bản đồ ô vuông. Sự khác biệt lớn duy nhất là logic để cập nhật mảng đỉnh này sẽ di chuyển đến một hệ thống ở đâu đó.

Bạn thậm chí có thể dựa vào ECS để đơn giản hóa việc vô hiệu hóa và xóa bộ đệm này khỏi thực thể nếu bạn tạo một thành phần riêng như thế nào TileMapCache. Tại thời điểm đó khi bộ đệm được mong muốn nhưng không có sẵn trong một thực thể có TileMapthành phần, bạn có thể tính toán và thêm nó. Khi nó bị vô hiệu hoặc không còn cần thiết, bạn có thể xóa nó thông qua ECS mà không phải viết thêm mã cụ thể cho việc vô hiệu hóa và xóa đó.

Sự phụ thuộc giữa các thành phần vẫn tồn tại mặc dù bị ẩn đi trong các hệ thống

Không có sự phụ thuộc giữa các thành phần trong một đại diện "thuần túy" (Tôi không nghĩ hoàn toàn đúng khi nói rằng các phụ thuộc đang bị các hệ thống ẩn ở đây). Dữ liệu không phụ thuộc vào dữ liệu, có thể nói như vậy. Logic phụ thuộc vào logic. Và một ECS "thuần túy" có xu hướng thúc đẩy logic được viết theo cách phụ thuộc vào tập hợp con tối thiểu tuyệt đối của dữ liệu và logic (thường không có) mà một hệ thống yêu cầu hoạt động, không giống như nhiều lựa chọn thay thế thường khuyến khích nhiều chức năng hơn yêu cầu cho các nhiệm vụ thực tế. Nếu bạn đang sử dụng quyền ECS thuần túy, một trong những điều đầu tiên bạn nên đánh giá cao là lợi ích tách rời đồng thời đặt câu hỏi cho tất cả mọi thứ bạn từng học để đánh giá cao trong OOP về việc đóng gói và ẩn thông tin cụ thể.

Bằng cách tách rời, tôi đặc biệt có nghĩa là hệ thống của bạn cần ít thông tin như thế nào. Hệ thống chuyển động của bạn thậm chí không cần biết về một thứ phức tạp hơn nhiều như một Particlehoặc Character(nhà phát triển hệ thống thậm chí không nhất thiết phải biết những ý tưởng thực thể như vậy thậm chí tồn tại trong hệ thống). Nó chỉ cần biết về dữ liệu tối thiểu trần như một thành phần vị trí có thể đơn giản như một vài float trong một cấu trúc. Nó thậm chí còn ít thông tin hơn và ít phụ thuộc bên ngoài hơn so với những gì một giao diện thuần túy IMotioncó xu hướng mang theo cùng với nó. Chủ yếu là do kiến ​​thức tối thiểu này mà mỗi hệ thống yêu cầu phải hoạt động khiến ECS thường tha thứ để xử lý các thay đổi thiết kế rất khó lường trong nhận thức mà không phải đối mặt với sự phá vỡ giao diện tầng ở mọi nơi.

Cách tiếp cận "không trong sạch" mà bạn đề xuất phần nào làm giảm lợi ích kể từ bây giờ logic của bạn không được định vị nghiêm ngặt cho các hệ thống mà các thay đổi không gây ra sự phá vỡ tầng. Logic bây giờ sẽ được tập trung ở một mức độ nào đó trong các thành phần được truy cập bởi nhiều hệ thống hiện phải đáp ứng các yêu cầu giao diện của tất cả các hệ thống khác nhau có thể sử dụng nó, và bây giờ nó giống như mọi hệ thống sau đó cần phải có kiến ​​thức về (phụ thuộc vào) nhiều hơn thông tin hơn nó cần nghiêm túc để làm việc với thành phần đó.

Phụ thuộc vào dữ liệu

Một trong những điều gây tranh cãi về ECS là nó có xu hướng thay thế những gì có thể phụ thuộc vào các giao diện trừu tượng chỉ bằng dữ liệu thô và thường được coi là một hình thức ghép nối ít mong muốn và chặt chẽ hơn. Nhưng trong các loại lĩnh vực như trò chơi mà ECS có thể rất có lợi, việc thiết kế biểu diễn dữ liệu trước dễ dàng hơn và giữ ổn định hơn là thiết kế những gì bạn có thể làm với dữ liệu đó ở cấp trung tâm của hệ thống. Đó là điều mà tôi đã quan sát một cách đau đớn ngay cả trong số các cựu chiến binh dày dạn trong các cơ sở mã hóa sử dụng nhiều cách tiếp cận giao diện thuần túy theo phong cách COM với những thứ như thế IMotion.

Các nhà phát triển tiếp tục tìm lý do để thêm, xóa hoặc thay đổi các chức năng cho giao diện trung tâm này và mỗi thay đổi đều rất khủng khiếp và tốn kém vì nó sẽ có xu hướng phá vỡ mọi lớp duy nhất được triển khai IMotioncùng với mọi vị trí trong hệ thống được sử dụng IMotion. Trong khi đó, toàn bộ thời gian với rất nhiều thay đổi đau đớn và xếp tầng, các đối tượng thực hiện IMotiontất cả chỉ lưu trữ một ma trận nổi 4 x 4 và toàn bộ giao diện chỉ liên quan đến cách chuyển đổi và truy cập các phao đó; việc biểu diễn dữ liệu đã ổn định ngay từ đầu và rất nhiều nỗi đau có thể tránh được nếu giao diện tập trung này, do đó dễ bị thay đổi với nhu cầu thiết kế không dự đoán được, thậm chí không tồn tại ngay từ đầu.

Tất cả điều này có thể nghe có vẻ kinh tởm như các biến toàn cầu, nhưng bản chất của cách ECS tổ chức dữ liệu này thành các thành phần được truy xuất rõ ràng bằng cách thông qua các hệ thống khiến nó trở nên như vậy, trong khi trình biên dịch không thể thực thi bất cứ điều gì như ẩn thông tin, những nơi truy cập và biến đổi dữ liệu nói chung rất rõ ràng và đủ rõ ràng để vẫn duy trì hiệu quả các bất biến và dự đoán loại biến đổi và tác dụng phụ nào xảy ra từ hệ thống này sang hệ thống tiếp theo (thực sự theo những cách có thể đơn giản và dễ dự đoán hơn OOP trong một số lĩnh vực nhất định hệ thống biến thành một loại đường ống phẳng).

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

Cuối cùng, tôi muốn đặt câu hỏi về cách tôi sẽ xử lý hoạt hình trong một ECS thuần túy. Hiện tại tôi đã định nghĩa một hoạt hình là một functor thao túng một thực thể dựa trên một số tiến trình từ 0 đến 1. Thành phần hoạt hình có một danh sách các hoạt hình có một danh sách các hoạt hình. Trong chức năng cập nhật của nó, sau đó nó áp dụng bất kỳ hình ảnh động nào hiện đang hoạt động cho thực thể.

Chúng ta đều là những người thực dụng ở đây. Ngay cả trong gamedev, bạn có thể sẽ nhận được những ý tưởng / câu trả lời mâu thuẫn. Ngay cả ECS thuần túy nhất cũng là một hiện tượng tương đối mới, lãnh thổ tiên phong, mà mọi người không nhất thiết phải đưa ra ý kiến ​​mạnh mẽ nhất về cách làm da mèo. Phản ứng ruột của tôi là một hệ thống hoạt hình làm tăng loại tiến trình hoạt hình này trong các thành phần hoạt hình để hệ thống kết xuất hiển thị, nhưng điều đó bỏ qua rất nhiều sắc thái cho ứng dụng và bối cảnh cụ thể.

Với ECS, đó không phải là viên đạn bạc và tôi vẫn thấy mình có xu hướng đi vào và thêm các hệ thống mới, loại bỏ một số, thêm các thành phần mới, thay đổi một hệ thống hiện có để chọn loại thành phần mới đó, v.v. mọi thứ ngay lần đầu tiên vẫn còn Nhưng sự khác biệt trong trường hợp của tôi là tôi không thay đổi bất cứ điều gì trung tâm khi tôi không lường trước được nhu cầu thiết kế nhất định. Tôi không nhận được hiệu ứng gợn sóng của các vụ vỡ tầng đòi hỏi tôi phải đi khắp nơi và thay đổi rất nhiều mã để xử lý một số nhu cầu mới tăng lên, và đó là cách tiết kiệm thời gian. Tôi cũng thấy nó dễ dàng hơn trong não vì khi tôi ngồi xuống với một hệ thống cụ thể, tôi không cần biết / nhớ nhiều về bất cứ thứ gì khác ngoài các thành phần có liên quan (chỉ là dữ liệu) để hoạt động trên nó.

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.