Theo Dõi Sự Thay Đổi Của Component Trong ECS - nói dối e blog

Theo Dõi Sự Thay Đổi Của Component Trong ECS

Trong kiến trúc ECS (Entity-Component-System), mỗi hệ thống (System) thường xuyên quét qua một nhóm Component cụ thể để xử lý logic. Cách tiếp cận này phù hợp với đa số tình huống trong game, nơi mà trạng thái các đối tượng thay đổi liên tục. Tuy nhiên, việc kiểm tra từng đối tượng riêng lẻ để xác định có cần xử lý hay không lại tạo ra gánh nặng hiệu năng không đáng có.

Vấn đề hiệu năng trong ECS

Có những trường hợp đặc biệt khi chỉ một phần nhỏ Component trong một nhóm bị thay đổi. Những đối tượng không bị sửa đổi có thể giữ nguyên dữ liệu cũ, tránh việc tính toán lại tốn kém. Khi số lượng đối tượng cần xử lý nhỏ hơn đáng kể so với tổng số, việc quét toàn bộ danh sách trở nên kém hiệu quả.

Giải pháp dấu hiệu bẩn (Dirty Flag)

Để tối ưu hiệu năng, giải pháp phổ biến là sử dụng cơ chế “dấu hiệu bẩn” (Dirty Flag). Khi một Component bị sửa đổi, hệ thống sẽ đánh dấu nó. Trong quá trình xử lý, chỉ những Component được đánh dấu mới được xử lý lại. Có hai cách triển khai phổ biến:

  1. Thêm trường boolean trong Component: Mỗi Component chứa một cờ hiệu isDirty. Khi quét, hệ thống bỏ qua các Component không có cờ hiệu này.
  2. Component đánh dấu động (Tag Component): Tạo một Component đặc biệt (ví dụ: DirtyTag) để gắn vào Entity khi có sự thay đổi. Sau khi xử lý xong, Component này sẽ bị loại bỏ.

Tuy nhiên, cả hai phương pháp này đều gặp hạn chế khi triển khai trong môi trường Lua. Do chi phí gọi hàm trong Lua tương đối cao, việc thêm phương thức set để quản lý dấu hiệu bẩn không chỉ làm giảm hiệu năng mà còn đi ngược với phong cách lập trình Lua truyền thống.

Giải pháp tối ưu hóa bằng Metamethod

Để khắc phục hạn chế này, chúng tôi đề xuất sử dụng cơ chế Modify Set (Tập hợp sửa đổi) kết hợp với metaprogramming trong Lua. Ví dụ, với Component Transform, ta tạo một đối tượng Transform.modify có khả năng theo dõi sự thay đổi. Khi muốn cập nhật giá trị x của Transform, lập trình viên chỉ cần viết:

1
transform.modify.x = new_value

Cơ chế đánh dấu “bẩn” được ẩn giấu trong metamethod của đối tượng modify. Việc này không chỉ tuân thủ phong cách lập trình Lua mà còn tối ưu hiệu năng thông qua việc sử dụng bảng dữ liệu và metamethod.

Cấu trúc dữ liệu Modify Set

Giải pháp này dựa trên ba thành phần chính:

  1. Đối tượng Modify: Chứa ID của Entity, vùng dữ liệu gốc và metatable.
  2. Bảng dữ liệu sửa đổi: Lưu trữ các giá trị mới của Component.
  3. Metatable: Định nghĩa hành vi khi truy cập hoặc ghi dữ liệu.

Cơ chế hoạt động

  • Lần sửa đổi đầu tiên: Metamethod __newindex trỏ đến một hàm đánh dấu Entity vào tập hợp “bẩn”. Sau khi đánh dấu, __newindex sẽ được chuyển hướng trực tiếp đến bảng dữ liệu, tránh việc gọi hàm tốn kém cho các lần sửa đổi tiếp theo.
  • Các lần sửa đổi sau: Dữ liệu được ghi trực tiếp vào bảng mà không kích hoạt cơ chế đánh dấu, nhờ đó giảm thiểu chi phí runtime.
  • Metamethod __index: Luôn trỏ đến bảng dữ liệu hiện tại, đảm bảo việc đọc dữ liệu diễn ra nhanh chóng.

Xử lý tập hợp Modify

Chúng tôi cung cấp một System chuyên dụng để xử lý toàn bộ Modify Set trong mỗi frame. Sau khi xử lý xong, hệ thống sẽ:

  • Xóa toàn bộ Entity khỏi tập hợp “bẩn”
  • Khôi phục metatable của các đối tượng Modify về trạng thái ban đầu

Ứng dụng thực tế

Giải pháp này đặc biệt hữu ích trong việc cập nhật cây cảnh (Scene Tree). Khi một nút cha trong cây thay đổi Transform, toàn bộ nút con sẽ bị ảnh hưởng. Tuy nhiên, trong thực tế, chỉ một số ít nút thường xuyên thay đổi vị trí. Cơ chế Modify Set giúp xác định chính xác những phần của cây cần cập nhật, thay vì tính toán lại toàn bộ.

Trong bài viết tiếp theo, tôi sẽ trình bày chi tiết cách áp dụng cơ chế này để tối ưu hóa hệ thống Scene Tree, kèm theo các số liệu benchmark và ví dụ minh họa cụ thể.

0%