Những Thiếu Sót Trong Các Khái Niệm ECS - nói dối e blog

Những Thiếu Sót Trong Các Khái Niệm ECS

Sau một thời gian dài suy nghĩ và thực hành, đặc biệt là trong hơn một tháng qua, chúng tôi đã tiến hành điều chỉnh lớn cho khung ECS mà chúng tôi đang xây dựng. Một phần công việc đã được trình bày trong bài viết trước về cơ chế đăng ký đăng xuất tin nhắn, phần còn lại thực ra đã bắt đầu từ trước nhưng vì muốn tích lũy thêm kinh nghiệm nên tôi mới viết bài tổng kết này vào tuần này sau khi hoàn tất các chỉnh sửa cơ bản.

Khung ECS hầu như chỉ được đề xuất trong lĩnh vực phát triển game, theo tôi nghĩ lý do chính là vì hiện tại chỉ có trong ngành công nghiệp game, việc thay đổi trạng thái của lượng lớn đối tượng theo chu kỳ mới là hành vi chủ đạo. Trong khi đó ở các lĩnh vực tương tác người-máy khác, hành vi phản ứng các sự kiện bên ngoài mới là luồng chính. Đây chính là lý do vì sao khái niệm System lại đặc biệt quan trọng trong lĩnh vực game.

Tuy vậy, việc khung ECS phản ánh lại các khung lập trình hướng đối tượng từng phổ biến trước đây, chủ yếu tập trung vào lập trình hướng dữ liệu/được dẫn dắt bởi dữ liệu. Đây cũng chính là nguyên nhân khiến Component trở thành khái niệm được nhấn mạnh. Component chính là các đối tượng mà hành vi đã bị tách ra, điều quan trọng là bản thân dữ liệu. Đối với một khung làm việc, điều cần giải quyết là việc kết hợp các thành phần này được thuận tiện, có cơ chế phản xạ (reflection) hoàn chỉnh, để từ đó có thể lập trình trực tiếp dựa trên tập dữ liệu. Bởi trong công nghiệp game, phần việc của lập trình viên chỉ chiếm tỷ lệ nhỏ, phần lớn còn lại là các công việc của nhà thiết kế và họa sĩ xoay quanh công cụ phát triển để bổ sung dữ liệu.

Vì hầu hết các khung nền tảng trong ngành game đều xây dựng trên C++, nên hiện nay phần lớn các bài diễn thuyết, bài viết và mã nguồn mở liên quan đến ECS đều xoay quanh việc triển khai cụ thể trong ngôn ngữ C++. Nhiều vấn đề mà họ giải quyết thực chất là do giới hạn của bản thân C++, chứ không phải là vấn đề thiết kế của ECS. Ví dụ như C++ không hỗ trợ mixin cho lớp, khuyến khích kế thừa, khả năng xây dựng kiểu dữ liệu động hạn chế, thiếu phản xạ nguyên sinh…

Khi chúng ta xây dựng khung ECS bằng ngôn ngữ động (ví dụ như Lua), sẽ thấy nhiều điểm trọng tâm trong các khung C++ trở nên không đáng kể. Ví dụ như trong bài diễn thuyết nổi tiếng tại GDC 2018 của đội ngũ phát triển Overwatch về khung ECS, họ dành nhiều thời gian trình bày về những sai lầm trong xử lý Singleton. Điều này đã từng khiến tôi hiểu sai khi thiết kế khung làm việc bằng Lua. Thực tế, Lua vốn đã có khả năng tạo sandbox tốt, hoàn toàn không mắc sai lầm duy nhất toàn cục kiểu này; chúng ta không cần cố ý xây dựng khái niệm Singleton trong khung ECS.

Tuy nhiên điểm nhấn của bài diễn thuyết đó hoàn toàn chính xác: điểm mấu chốt khi sử dụng khung ECS chính là giải kết nối (decoupling). Còn hiệu năng chỉ là sản phẩm phụ. Chúng ta nên tập trung vào cách thức mà khung làm việc phân rã vấn đề rõ ràng hơn, độc lập hơn giữa các yếu tố. Đây chính là chìa khóa cho việc tách biệt Component và System.

Trong hơn một năm thực tiễn, chúng tôi nhận thấy Component bản chất là để mô tả một khía cạnh của đối tượng. Khái niệm Lập trình hướng khía cạnh (AOP) không phải là mới, trong lĩnh vực phát triển web đã có từ lâu, mục tiêu và phương pháp giải quyết vấn đề hoàn toàn khác biệt so với ECS trong game. Tuy nhiên theo bản chất, cả hai đều hướng đến việc phân rã logic nghiệp vụ thành các mô-đun độc lập, để dễ tổ hợp và tái sử dụng, mang lại tính linh hoạt. Trong AOP gọi là “concern”, trong ECS gọi là System, mục đích cuối cùng (giải kết nối nghiệp vụ) là như nhau. Chỉ khác ở chỗ AOP thiên về mở rộng hành vi bổ sung vào luồng xử lý hiện tại, còn ECS nhấn mạnh tổ hợp các hành vi đã có theo nhu cầu.

Trong thực tiễn của chúng tôi, dữ liệu tiềm năng của đối tượng được chia thành từng Component riêng biệt. Ví dụ để thực hiện một vật thể có thể di chuyển, cần có các Component như da, xương, lưới, ma trận biến đổi… Các Component này có thể được tái sử dụng khi triển khai các đặc tính khác. Đồng thời tồn tại nhiều System xử lý dữ liệu của các Component tương ứng. Đôi khi một Component chỉ được một System xử lý, đôi khi lại có nhiều System cùng xử lý một Component, cũng có trường hợp một System xử lý nhiều Component khác nhau.

Kết quả cuối cùng là, một quy trình render tiêu biểu thường sử dụng hàng chục System, mỗi đối tượng renderable được cấu thành từ hàng chục Component. Nếu trực tiếp xây dựng nghiệp vụ dựa trên Component và System, gần như không thể tránh khỏi thiếu sót hoặc tạo ra các thành phần dư thừa không mong muốn.

Nếu việc sử dụng khung làm việc làm gia tăng độ phức tạp trong phát triển, chắc chắn đã có điều gì đó sai lệch. ECS thành công trong việc giải kết nối các mô-đun chức năng nhưng lại khiến việc sử dụng trở nên phức tạp, điều này rõ ràng là không đúng.

Chính vì vậy, chúng tôi quyết định đưa vào một khái niệm mới gọi là Policy, dùng để biểu đạt một mô-đun chức năng cụ thể.

Khi tạo Entity, chúng tôi mô tả Entity sở hữu những Policy nào, thay vì mô tả nó bao gồm những Component nào. Số lượng Policy luôn ít hơn nhiều so với Component, đồng thời cũng mô tả quy trình khởi tạo tổ hợp các Component. Vì chúng tôi đang theo đuổi lập trình dựa trên dữ liệu, nên Entity được cấu tạo từ dữ liệu - một phần dữ liệu trực tiếp đến từ kết quả lưu trữ bền (persistent), nguồn gốc dữ liệu ban đầu xuất phát từ việc người dùng tạo ra trong editor; một phần dữ liệu khác lại được xây dựng từ các Component khác.

Ví dụ, dữ liệu lưới bền (persistent) có thể chỉ là URI của một tệp dữ liệu, nhưng lúc thực thi lại cần tải tệp này vào cấu trúc bộ nhớ; dữ liệu hoạt ảnh có thể cần kết hợp dữ liệu xương và da mới có thể tạo ra…

Quy trình khởi tạo dữ liệu là một quá trình phức tạp, có thể coi như kết quả của chuỗi thao tác liên hoàn tác động lên một Entity cụ thể. Tôi cho rằng mỗi phần của quy trình này có thể được xem như một phép biến đổi, chuyển dữ liệu từ nguồn này sang tập dữ liệu khác. Miễn là định nghĩa được đầu vào và đầu ra của phép biến đổi, chúng ta có thể xây dựng chính xác quy trình này.

Chính vì vậy, chúng tôi định nghĩa các yếu tố này trong Policy. Policy giải quyết việc đưa vào những Component nào và quy trình khởi tạo các phép biến đổi bổ sung, người sử dụng chỉ cần đưa vào một Policy là có thể giải quyết trọn vẹn vấn đề về các thành phần cần thiết để xây dựng đúng đặc tính cho đối tượng.

Thứ

0%