Quản Lý Prefab Và Tập Hợp Đối Tượng - nói dối e blog

Quản Lý Prefab Và Tập Hợp Đối Tượng

Trong quá trình phát triển dự án bằng engine tự xây dựng, gần đây tôi đã gặp phải một số vấn đề cần giải quyết. Trong quá trình khắc phục, thiết kế ban đầu cũng được điều chỉnh theo hướng tối ưu hơn. Từ những sửa đổi nhỏ ban đầu, dần dần tiến triển thành một cuộc tái cấu trúc mã quy mô lớn.

Khởi nguồn từ việc tôi thiết kế lại khung làm việc ECS. Trong thiết kế mới, có thể kết hợp C/Lua để tổ chức dữ liệu, tạo tiền đề cho việc tối ưu hiệu năng trong tương lai. Dịp này cũng giúp chúng tôi xem xét lại cách tổ chức mã trong khung ECS. Một điểm mấu chốt được nhận ra: cần loại bỏ tối đa mối quan hệ tham chiếu giữa các đối tượng trong hệ thống. Mỗi loại đối tượng nên được xử lý theo nhóm, mỗi module chỉ thực hiện công việc đơn giản nhất có thể, nhưng xử lý đồng thời lượng dữ liệu lớn nhất.

Ví dụ về quá trình khởi tạo và hủy đối tượng - điều vốn trở nên phức tạp khi cấu trúc đối tượng ngày càng đa dạng. Trong thiết kế ECS trước đây, chúng tôi đã xây dựng nhiều cơ chế để đảm bảo quy trình khởi tạo Entity diễn ra chính xác. Một Entity có thể được tổ hợp động từ nhiều loại Component, nhưng không phải Component nào cũng khởi tạo độc lập. Chúng thường liên kết với nhau - ví dụ khi kết hợp A, B, C để tạo Entity, cần khởi tạo A và B trước, sau đó mới dùng kết quả này để khởi tạo C.

Khi đưa khái niệm Prefab vào, quá trình này càng trở nên phức tạp. Prefab thực chất là tập hợp chứa nhiều đối tượng. Việc khôi phục đối tượng từ Prefab không chỉ là khởi tạo từng đối tượng riêng lẻ, mà còn phải tái tạo các mối liên kết giữa chúng như khi thiết kế. Ví dụ, toàn bộ cảnh game có thể là một Prefab duy nhất. Việc tải cảnh chính là quá trình tái tạo toàn bộ đối tượng trong Prefab ở thời gian chạy.

Trong khung làm việc OOP truyền thống, chúng ta thường khởi tạo từng đối tượng theo thứ tự, sau đó khôi phục các mối quan hệ (như quan hệ cha-con trong cây cảnh). Nhưng với khung ECS, chúng ta quản lý các tập hợp Component. Việc khởi tạo theo loại Component có thể đơn giản hơn, vì các mối liên kết thường giới hạn trong cùng loại Component (ví dụ nút cảnh), không liên quan đến Entity.

Sau khi tái cấu trúc, chúng tôi loại bỏ cơ chế khởi tạo/hủy Entity theo đơn vị Entity trước đây. Thay vào đó, mỗi module tự thêm System xử lý quy trình khởi tạo vào Pipeline phù hợp. Trong cơ chế mới, Entity dường như được tạo ra đồng loạt. Nếu quy trình tạo Entity có 3 bước A-B-C, framework sẽ xử lý toàn bộ Entity ở bước A trước, sau đó mới chuyển sang xử lý hàng loạt ở bước B và C.

Hệ quả là toàn bộ quá trình tạo Entity trở thành bất đồng bộ. Chúng ta không thể thực hiện thao tác đồng bộ như:

  1. E = CreateEntity()
  2. S = Getstate(E)
  3. CreateEntity(S)

Việc không thể tạo Entity đồng bộ gây phiền toái đến đâu cho người dùng engine? Chúng tôi đã thảo luận kỹ về vấn đề này. Thực tế, với sự tồn tại của Prefab, việc tạo Entity từ đầu ít được sử dụng - thay vào đó là phương thức prefab:instance() từ dữ liệu Prefab. Vấn đề chuyển thành: liệu phương thức instance của Prefab có nên là bất đồng bộ?

Hiện tại, chúng tôi quyết định quy trình instance nên bất đồng bộ, nhưng có thể cung cấp khả năng đồng bộ giới hạn. Đối với engine game, việc tái tổ chức dữ liệu đã được chuẩn bị sẵn trong runtime hoàn toàn đồng bộ là không thực tế, vì nó liên quan đến lượng lớn thao tác IO tài nguyên và phân tích dữ liệu. Nếu chờ toàn bộ tài nguyên sẵn sàng mới trả về kết quả, cho phép code phía sau đọc/ghi đối tượng ngay lập tức, sẽ gây ra độ trễ lớn trong một lần gọi hàm.

Ngay cả khi tải cảnh, chúng tôi vẫn mong muốn quá trình này được chia nhỏ, tạo cơ hội hiển thị thanh tiến độ hoặc xử lý phức tạp hơn. Nếu dùng giao diện đồng bộ, sẽ phải dựa vào cơ chế đa luồng phức tạp và dễ lỗi. Dĩ nhiên, có thể tách riêng việc tải tài nguyên, dùng cơ chế thông báo khi hoàn tất, còn phần còn lại tạo đồng bộ. Tuy nhiên, việc phân định thế nào là “dữ liệu tài nguyên” không dễ dàng - nếu texture/mô hình được coi là tài nguyên, thì bản thân Prefab có được tính không?

Hiện tại, chúng tôi áp dụng cách tiếp cận: phương thức instance của Prefab đồng bộ trả về một nút ảo đơn giản trên cảnh. Nút này có cấu trúc xác định, có thể tạo đồng bộ trong World với các thuộc tính cơ bản như SRT. Toàn bộ cây cảnh cần khôi phục từ Prefab đều được xây dựng bất đồng bộ, phân bố trong pipeline của framework, nhanh nhất là đến frame render tiếp theo mới hoàn tất.

Giao diện bên ngoài sử dụng cơ chế callback để kiểm soát tập hợp đối tượng được tạo từ Prefab. Khi gọi tạo đối tượng, cần truyền vào các hàm ready (hoàn tất tạo), update (cập nhật mỗi frame), message (xử lý tin nhắn).

Từ góc nhìn bên ngoài, tập hợp đối tượng này được ẩn sau nút gốc ảo. Chúng tôi không cung cấp API để liệt kê các đối tượng cụ thể bên trong qua nút gốc. Mọi thao tác với đối tượng phải thực hiện trong callback. Do đó, muốn truy cập đối tượng cụ thể phải thông qua “tin nhắn”, hoặc chỉ có thể kiểm soát nút gốc. Tuy nhiên, nút gốc có thể được điều khiển đồng bộ ngay sau khi tạo xong, không cần chờ toàn bộ tập hợp đối tượng hoàn tất.

0%