Ứng Dụng Chức Năng Phân Nhóm Trong Hệ Thống Gắn Kết Đối Tượng
Trong giai đoạn phát triển gần đây, chúng tôi đã tích hợp chức năng phân nhóm đối tượng vào hệ thống engine. Mục tiêu ban đầu của tính năng này là tạo ra cơ chế lọc nhanh các tập hợp đối tượng cần xử lý (hiển thị) khi số lượng đối tượng trong thế giới game vượt xa số lượng đối tượng cần xử lý đồng thời. Trong quá trình vận hành, tôi đã khám phá ra nhiều ứng dụng thú vị khác của chức năng này.
Trong một dự án game trước đây, chúng tôi từng đối mặt với yêu cầu đặc biệt: tạo avatar người chơi từ nhiều thành phần như chân dung, tên, cấp độ và các biểu tượng trang trí. Avatar này cần xuất hiện đồng thời ở nhiều vị trí trên màn hình. Nếu sử dụng cấu trúc cây cảnh quan truyền thống, mỗi vị trí hiển thị avatar sẽ là một nhánh con chứa toàn bộ thành phần. Điều này dẫn đến việc sao chép nhiều bản sao hoàn toàn giống nhau trong cảnh quan, gây khó khăn khi cập nhật thuộc tính người chơi. Mỗi thay đổi đều yêu cầu sửa đổi tất cả các nhánh con tương ứng, buộc chúng tôi phải duy trì danh sách tham chiếu phức tạp cho từng người chơi.
Giải pháp tự nhiên là chuyển đổi cấu trúc cảnh quan thành đồ thị có hướng không chu trình (DAG). Khi đó, avatar chỉ cần tồn tại duy nhất một lần trong hệ thống, nhưng có thể được tham chiếu bởi nhiều nút cha khác nhau. Tuy nhiên, trong quá trình triển khai trên engine Ejoy2D, tôi nhận thấy giải pháp này vẫn còn tồn tại độ phức tạp nhất định trong cấu trúc dữ liệu底层.
Trong dự án game mới đang phát triển trên engine hiện đại, chúng tôi lại gặp phải vấn đề tương tự. Đây là một game theo phong cách “factory simulation” với hàng loạt chi tiết nhỏ. Các yếu tố cảnh quan như nhà máy, tay máy, drone vận chuyển… đều cần gắn kết các linh kiện nhỏ để thể hiện quá trình vận hành. Ví dụ: tay máy nhặt tấm sắt, đưa vào container mục tiêu; hoặc người chơi quan sát sự thay đổi lượng nguyên liệu tại khu vực trống bên ngoài nhà máy.
Ban đầu, chúng tôi sử dụng phương pháp tạo đối tượng mới và gắn vào điểm kết nối tương ứng, sau đó xóa khi không cần thiết. Tuy nhiên, khi số lượng linh kiện tăng lên hàng nghìn, việc tạo/xóa đối tượng trở thành điểm nghẽn hiệu năng. Điều này hoàn toàn dễ hiểu vì engine được thiết kế với giả định rằng các thao tác này không xảy ra thường xuyên, nên chưa được tối ưu. Đặc biệt, mỗi linh kiện nhỏ có thể là tổ hợp của nhiều đối tượng底层, khiến việc tạo mới trở thành thao tác “nặng”.
Phân tích kỹ, chúng tôi nhận ra giải pháp tối ưu là tái sử dụng đối tượng. Thay vì tạo mới hàng trăm bản sao của cùng một linh kiện (như bánh răng), chỉ cần duy trì một thể hiện duy nhất có thể gắn kết vào nhiều điểm khác nhau. Ban đầu tôi nghĩ đến giải pháp DAG như trước, nhưng quyết định thử nghiệm với chức năng phân nhóm mới triển khai.
Hiện tại, việc gán tag cho các đối tượng cùng nhóm có độ phức tạp O(n) với n là số phần tử cần gán. Các linh kiện như bánh răng, tấm sắt được tạo độc lập, không nằm trong cây cảnh quan chính mà thuộc về các nhóm riêng biệt. Việc truy xuất các đối tượng cùng nhóm có độ phức tạp O(1) trong đa số trường hợp (khi mỗi nhóm chỉ chứa một đối tượng).
Giải pháp cuối cùng là tạo loại đối tượng cảnh quan “nút ảo” đặc biệt. Ngoài các thành phần cảnh quan tiêu chuẩn, nút này chỉ chứa tham chiếu đến ID nhóm. Khi render, hệ thống sẽ tìm tất cả đối tượng trong nhóm tương ứng (thường chỉ có một), sử dụng ma trận thế giới đã tính toán sẵn và nhân với ma trận của nút ảo để render chính xác vị trí.
Tổng kết: Thế giới game hiện tại là một “rừng” gồm nhiều cây cảnh quan. Chỉ có một cây chính cần render, trong khi các cây phụ chứa các đối tượng được phân nhóm. Mỗi cây phụ tương ứng với một nhóm, các đối tượng trong nhóm được lưu trữ phẳng trong tập hợp tương ứng. Các nút ảo trên cây render chính đóng vai trò “cửa sổ” tham chiếu đến các nhóm đối tượng này.