Quản Lý Bộ Nhớ Tài Nguyên Trong Ant
Trong quá trình xây dựng demo game hai ngày qua, tôi nhận ra rằng module quản lý tài nguyên của Ant vẫn còn một số công việc dang dở từ trước. Vấn đề cốt lõi nằm ở thời điểm giải phóng tài nguyên sau khi chúng được tải vào bộ nhớ. Hiện tại, trong module ant.asset, chúng tôi đã định nghĩa ba giao diện loader/unloader/reloader cho từng loại tài nguyên (phân biệt qua đuôi file), chịu trách nhiệm lần lượt cho việc tải, dỡ tải và tải lại. Tuy nhiên trong thực tế triển khai:
- Vấn đề thiếu unloader: Hầu hết các unloader chưa được hoàn thiện do lối suy nghĩ “tiện tay” trước đây. Vì các game trước đây của chúng tôi dù tải toàn bộ tài nguyên vào RAM cũng không chiếm nhiều dung lượng, nên việc quản lý giải phóng động không thực sự cần thiết. Ngay cả khi có triển khai unloader, hệ thống quản lý cũng chưa có chiến lược hợp lý để tự động kích hoạt chúng - người dùng chỉ có thể gọi API dỡ tải thủ công. Việc này rõ ràng không hiệu quả khi phải xử lý từng file riêng lẻ.
Giải pháp đặc biệt cho texture
Với việc texture thường chiếm phần lớn bộ nhớ, chúng tôi đã áp dụng cơ chế đặc biệt:
- Texture thay thế: Mọi texture đều có thể được thay thế bằng một texture trắng trơn. Engine có quyền chủ động giải phóng các texture không được sử dụng trong thời gian dài (thường khi hệ thống báo thiếu bộ nhớ), đồng thời cơ chế này tương thích tốt với quá trình tải bất đồng bộ. Nhờ đó, ngay cả khi chưa giải phóng, các texture cũng không gây tràn RAM.
Quản lý mesh dựng sẵn
Khi xem xét API liên quan đến hình dạng dựng sẵn (prefab) hôm nay, tôi nhớ ra chúng tôi cũng có xử lý đặc biệt cho các mô hình dựng sẵn như mặt phẳng, mũi tên, khối lập phương… Những mesh này:
- Không tải từ file: Được tạo trực tiếp bằng dữ liệu đỉnh trong code
- Không chia sẻ giữa entity: Mỗi entity sở hữu một bản sao riêng
- Quản lý vòng đời: Dữ liệu mesh sẽ bị hủy khi entity sở hữu bị phá hủy
Chúng tôi đánh dấu đặc biệt trên các cấu trúc dữ liệu này để đảm bảo chúng được dọn dẹp đúng lúc, tránh rò rỉ tài nguyên. Tuy nhiên, thiết kế này có vẻ chưa tối ưu, nên tôi quyết định tái cấu trúc toàn bộ hệ thống quản lý tài nguyên để thống nhất cách xử lý giữa tài nguyên từ file và tài nguyên sinh ra bằng code.
Khi nào nên dọn dẹp tài nguyên?
Tôi cho rằng:
- Game nhỏ/demo: Không cần dọn dẹp runtime, cứ để hệ thống giải phóng toàn bộ khi thoát chương trình
- Game lớn: Nên dọn dẹp khi chuyển cảnh, và không nên chia nhỏ quá mức vì sẽ làm tăng độ phức tạp phát triển
Thách thức trong việc dọn dẹp
Khó khăn nằm ở việc theo dõi các mối quan hệ tham chiếu giữa object và tài nguyên. Dù hiện tại mọi tài nguyên đều được tham chiếu qua entity trong hệ thống ECS, nhưng việc duy trì bảng theo dõi quan hệ này tốn kém về mặt tính toán. Tuy nhiên, khi world ECS bị hủy/được khởi tạo lại (thường xảy ra khi chuyển cảnh), đây lại là thời điểm lý tưởng để dọn dẹp toàn bộ tài nguyên. Lúc này, chúng ta có thể:
- Giải phóng toàn bộ: Nếu không cần tái sử dụng
- Cache thông minh: Giữ lại các tài nguyên có thể cần dùng lại trong tương lai gần
Hướng đi mới cho quản lý mesh
Tôi dự định bắt đầu tái cấu trúc từ quản lý mesh, sau đó mở rộng cho toàn bộ tài nguyên:
- Thống nhất cách tạo mesh: Handle mesh chỉ được tạo, không hủy riêng lẻ. Không phân biệt mesh từ file và mesh sinh bằng code
- API xóa toàn bộ mesh: Gọi khi chắc chắn không còn tham chiếu nào tồn tại (thường trong quá trình rebuild world)
- Dịch vụ ltask quản lý cache: Không xóa ngay lập tức khi có yêu cầu, mà dựa trên thuật toán cache để quyết định thời điểm thích hợp
Giải pháp cho mesh sinh bằng code
Tôi sẽ thêm API mới cho phép đặt tên chuỗi khi tạo mesh. Tên này sẽ:
- Thống nhất với mesh từ file: Cho phép quản lý tập trung
- Mã hóa tham số: Đảm bảo mỗi chuỗi tên tương ứng duy nhất với một cấu hình mesh cụ thể
Thuật toán cache đề xuất
- Phân vùng dữ liệu: Chia thành hai khu vực
new
(dữ liệu cần thiết hiện tại) vàold
(dữ liệu có thể giải phóng) - Cơ chế làm mới:
- Khi refresh, đặt dung lượng cache =
old/2 + new*2
- Chuyển toàn bộ dữ liệu hiện tại sang trạng thái
old
- Khi refresh, đặt dung lượng cache =
- Quy trình tải dữ liệu:
- Nếu dữ liệu đã tồn tại: Đánh dấu là
new
- Nếu dữ liệu mới: Tải vào, kiểm tra giới hạn
new
. Nếu vượt quá, xóa ngẫu nhiên một phần tửold
- Nếu dữ liệu đã tồn tại: Đánh dấu là
Giải pháp này hướng đến sự cân bằng giữa hiệu suất và tính đơn giản trong quản lý tài nguyên, đồng thời mở đường cho việc mở rộng sang các loại tài nguyên khác trong tương lai.