Cải Tiến Mô-Đun Quản Lý Tài Nguyên - nói dối e blog

Cải Tiến Mô-Đun Quản Lý Tài Nguyên

Bài viết này là phần tiếp nối của chủ đề “Quản lý vòng đời tài nguyên trong động cơ game”.

Gần đây, chúng tôi đã tiến hành tái cấu trúc mô-đun tài nguyên của động cơ game, quá trình này kéo dài khoảng một tuần với 2 ngày tập trung vào phát triển cốt lõi. Giải pháp mới đơn giản hơn đáng kể so với phương án trước đó, được xây dựng dựa trên những nguyên tắc then chốt sau:

  1. Thiết kế lấy dữ liệu làm trung tâm - Mô hình ECS (Entity-Component-System) bản chất là mô hình được điều khiển bởi dữ liệu. Toàn bộ hệ thống được xây dựng xung quanh việc xử lý dữ liệu một cách hiệu quả.

  2. Tính nhất quán trong cấu trúc dữ liệu - Tất cả dữ liệu đều được biểu diễn dưới dạng cấu trúc đồng nhất để dễ dàng xử lý tập trung. Trong hệ thống của chúng tôi, mọi dữ liệu đều là bảng (table) Lua. Động cơ không chỉ đơn thuần được phát triển bằng Lua mà thực chất là được xây dựng dựa trên cấu trúc dữ liệu Lua. Ngay cả khi một số mô-đun được triển khai bằng C/C++ vì lý do hiệu năng, chúng vẫn thao tác trực tiếp với bảng Lua. Hiệu năng đọc/ghi bảng Lua không thua kém đáng kể so với struct/array C.

  3. Xóa nhòa ranh giới dữ liệu tĩnh và động - Hệ thống được thiết kế để không phân biệt rõ ràng giữa dữ liệu tĩnh không thể sửa đổi từ bên ngoài và dữ liệu động thay đổi trong quá trình chạy chương trình.

  4. Tối ưu hóa quy trình tải tài nguyên - Các cơ chế như tải trì hoãn (lazy loading), tải không đồng bộ, hoặc thay thế tài nguyên nên được ẩn giấu hoàn toàn, không cần tiết lộ chi tiết cho bên ngoài. Mức độ can thiệp từ bên ngoài được giảm thiểu tối đa.

  5. Hạn chế phụ thuộc vào đặc tính ngôn ngữ - Dù metatable trong Lua là công cụ mạnh để thống nhất cách xử lý các loại dữ liệu khác nhau, chúng tôi vẫn cố gắng không quá lạm dụng các tính năng đặc trưng của ngôn ngữ.

Kiến trúc dữ liệu thống nhất

Các Component trong hệ thống本质上 là cấu trúc dữ liệu dạng cây phân cấp. Đối với Lua, đây là các bảng có hoặc không có phân cấp. Tương tự, mọi dữ liệu bên ngoài cũng được coi là bảng cây phân cấp đồng nhất.

Một loader tài nguyên có thể được viết đơn giản như: một máy trạng thái khởi tạo từ tên tệp tin, đầu ra là bảng Lua chứa các kiểu dữ liệu cơ bản (number, string) hoặc các handle do động cơ xử lý (texture, vb, ib, framebuffer…).

Dữ liệu runtime có thể biểu diễn dưới hai dạng:

  • Bảng Lua thông thường
  • Tham chiếu đến nút cây trong tệp tài nguyên thông qua đường dẫn chuỗi

Thông qua cơ chế metatable, cả hai dạng này có thể được truy cập theo cách nhất quán. Tuy nhiên, hành vi của metatable sẽ thay đổi tùy trạng thái dữ liệu (có trong RAM hay không).

Chúng tôi chọn cách nhúng thông tin loại dữ liệu trực tiếp vào tệp tin, không dùng schema mô tả riêng. Điều này cho phép mọi dữ liệu lưu trữ đều có cách biểu diễn đồng nhất, đồng thời cho phép xây dựng các loader chuyên dụng cho từng loại tài nguyên. Ví dụ: loader texture sẽ chuyển đổi dữ liệu thô thành handle texture trong runtime.

Tích hợp công cụ vào quy trình phát triển

Vì hệ thống lấy dữ liệu làm trung tâm, việc tạo Entity thực chất là quá trình khởi tạo từ prefab - tệp dữ liệu chứa cấu hình ban đầu của Entity. Ở cấp độ sử dụng động cơ, chúng tôi không cung cấp API tạo Entity thuần bằng code. Thay vào đó, API duy nhất cho phép là tạo từ tệp tin, sau đó cho phép người dùng sửa đổi tiếp.

Điều này khiến công cụ tạo prefab trở thành phần không thể thiếu trong quy trình làm việc hàng ngày. Trước đây, chúng tôi tập trung phát triển底层 bằng code thủ công, tách biệt với công cụ, dẫn đến tình trạng “không ăn cùng con chó” (don’t eat your own dog food). Các nhà phát triển động cơ không phụ thuộc vào công cụ do nhóm khác làm, nhưng lại tạo ra khoảng cách với người dùng cuối thực tế - những người làm việc với công cụ phát triển chứ không phải thư viện động cơ thuần.

Sự thay đổi này bắt đầu từ việc hoàn thiện hệ thống khung xương (scaffolding). Trong hơn một năm phát triển, chúng tôi đã tích lũy đủ độ ổn định để tháo dỡ dần các công cụ hỗ trợ tạm thời, tạo nền tảng cho quy trình làm việc tích hợp công cụ.

Cơ chế Proxy cho dữ liệu runtime

Đối với dữ liệu runtime tham chiếu đến tệp tài nguyên, chúng tôi triển khai dưới dạng đối tượng Proxy. Các Proxy này được phân loại và quản lý theo tệp tài nguyên gốc. Mỗi nút cây trong tệp tin sẽ sinh ra một Proxy tương ứng.

Lợi ích của cơ chế này:

  • Tổng số Proxy trong game là cố định
  • Proxy không chứa dữ liệu thật, chiếm dụng bộ nhớ ít
  • Không cần xóa Proxy, giảm độ phức tạp quản lý

Khi dữ liệu có trong RAM, Proxy trỏ đến bảng dữ liệu đã tải. Thông qua metatable, chúng tôi có thể theo dõi tần suất sử dụng dữ liệu. Khi phát hiện ít dùng (ví dụ: texture không được truy cập trong 100 lần kiểm tra ngẫu nhiên qua 5 phút), hệ thống có thể xóa dữ liệu khỏi RAM để tối ưu bộ nhớ.

Khi dữ liệu không có trong RAM, các Proxy thuộc cùng tệp tin có thể chia sẻ metatable. Điều này cho phép thay đổi hành vi của toàn bộ Proxy chỉ với độ phức tạp O(1).

Chiến lược tải tài nguyên linh hoạt

Tùy loại tài nguyên, chúng tôi áp dụng các chiến lược khác nhau:

  1. Tải đồng bộ trì hoãn (Lazy Loading) - Với tệp nhỏ, hệ thống sẽ chặn thread hiện tại để tải dữ liệu ngay khi có truy cập đầu tiên.

  2. Tải không đồng bộ (Asynchronous Loading) - Với tệp lớn (

0%