Thiết Kế Prefab Trong Động Cơ Game - nói dối e blog

Thiết Kế Prefab Trong Động Cơ Game

Khái niệm Prefab được Unity phổ biến rộng rãi, nhưng trước đó Unreal Engine đã có một công nghệ tương tự gọi là Blueprint. Dù có nhiều điểm chung, hai công nghệ này về bản chất không hoàn toàn giống nhau. Trong quá trình tự thiết kế động cơ game, tôi ưa chuộng mô hình Prefab của Unity hơn, tuy nhiên xét về mặt ngữ nghĩa thì cái tên Blueprint lại phản ánh chính xác bản chất hơn.

Khi nói đến việc tạo một Prefab, thực chất chúng ta đang xây dựng một mẫu khuôn dùng để sản xuất các đối tượng trong runtime. Dữ liệu trong động cơ sẽ được sinh ra dựa trên mẫu khuôn này. Nếu xét về bản chất, công cụ tạo ra Prefab chính là một “máy tạo dữ liệu” (generator), hay nói cách khác là một bản thiết kế kỹ thuật (blueprint). Tuy nhiên do thuật ngữ “Prefab” đã quá quen thuộc với cộng đồng, tôi vẫn quyết định giữ nguyên tên gọi này trong hệ thống của mình.

Trong hệ thống ECS, Prefab là một tập hợp dữ liệu thường được lưu trữ dưới dạng file. Khi sử dụng tài nguyên này, chúng ta có thể tạo ra một nhóm Entity theo khuôn mẫu đã định. Cần lưu ý rằng Prefab khác biệt so với các loại tài nguyên truyền thống như texture hay mesh. Các tài nguyên này khi nạp vào bộ nhớ sẽ trở thành các khối dữ liệu có thể chia sẻ, nhưng Prefab chỉ là khuôn mẫu để sản xuất dữ liệu, không phải dữ liệu thực tế. Nếu coi Prefab là một loại tài nguyên đặc biệt, thì thay vì lưu trữ dưới dạng bảng dữ liệu (table), nó nên được biểu diễn như một hàm tạo (function) chuyên dụng.

Prefab được tạo ra thông qua các công cụ của động cơ. Nếu xem toàn bộ cảnh game như một sản phẩm được lắp ráp từ nhiều Prefab lồng ghép, thì lúc này trình chỉnh sửa cảnh chính là trình chỉnh sửa Prefab. Quá trình chuyển đổi từ cảnh đã thiết kế sang mẫu Prefab chính là nghịch đảo của quá trình tạo Entity từ Prefab - một cặp thao tác tuần tự hóa (serialization) và phản tuần tự hóa (deserialization) dành cho các Component của Entity.

Với tư cách là tập hợp các Component, việc tuần tự hóa từng Component của một Entity không quá phức tạp. Tuy nhiên thách thức thực sự nằm ở việc mô tả mối quan hệ giữa các Entity. Trong cấu trúc cảnh game, các đối tượng thường được tổ chức theo dạng phân cấp cây. Để biểu diễn mối quan hệ cha-con, chúng ta thường dùng entity ID. Tuy nhiên các ID này không thể lưu trữ trực tiếp vào file.

Trong thiết kế trước đây, chúng ta đã chuyển đổi entity ID sang GUID trước khi tuần tự hóa, sau đó chuyển ngược lại khi phản tuần tự hóa. Trong lần tái cấu trúc gần đây, tôi nhận ra phương pháp này có những hạn chế:

  1. Vấn đề schema dữ liệu: Việc tự động chuyển đổi entity ID yêu cầu hệ thống phải có schema mô tả chi tiết cấu trúc Component. Tuy nhiên việc duy trì schema phức tạp trong khi sử dụng Lua - ngôn ngữ không yêu cầu schema - lại gây ra nhiều phiền toái hơn lợi ích.

  2. Thiết lập quan hệ liên Entity: Chỉ đơn giản điền đúng ID chưa đủ, nhiều trường hợp cần thực hiện logic bổ sung khi xây dựng. Với nhiều Entity, phải đợi tất cả được tạo xong mới thiết lập quan hệ. Hiện tại chúng ta dùng cơ chế message để giải quyết, nhưng vẫn cần một quá trình bổ sung để gửi message này.

  3. Tham chiếu nội bộ và ngoại bộ: Prefab có thể tạo nhiều Entity với các tham chiếu lẫn nhau. Các tham chiếu nội bộ không cần GUID toàn cục, còn tham chiếu ngoại bộ thì GUID cũng không giải quyết được vấn đề. Vì Prefab là mẫu tạo dữ liệu chứ không phải dữ liệu cụ thể, nên GUID ở đây không đại diện cho đối tượng cụ thể.

  4. Prefab lồng ghép: Prefab có thể chứa các Prefab khác, nhưng không thể tham chiếu đến một phần cụ thể của Prefab khác. Vì mỗi Prefab mô tả phương pháp tạo tập hợp Entity liên kết, nên một phần của nó không mang ngữ nghĩa hoàn chỉnh.

Giải pháp thiết kế mới

Chúng ta cần giải quyết hai vấn đề then chốt:

  1. Cách tạo file dữ liệu Prefab
  2. Hành vi của API khi nạp file và tạo đối tượng runtime

Cấu trúc file Prefab

Tôi đề xuất chia file Prefab thành hai phần chính:

Phần 1: Dữ liệu Entity

  • Chứa danh sách các Entity và dữ liệu Component của chúng
  • Dữ liệu tự mô tả (schema-less) để tăng tính linh hoạt
  • Không chứa thông tin quan hệ giữa các Entity

Phần 2: Chuỗi hành động (Actions)

  • Gồm các lệnh thao tác quan hệ giữa Entity
  • Mỗi lệnh có dạng: <tên_hành_động> <id1> <id2>...
  • Ví dụ: mount 1 0 - gắn Entity 1 vào vị trí 0 (gốc cảnh)

API tạo Entity từ Prefab

Hàm chính: produce(prefab, entity_id)

  1. Tạo các Entity từ dữ liệu phần 1 → nhận được danh sách entity ID
  2. Thực thi các hành động trong phần 2, chuyển đổi ID nội bộ thành entity ID thực tế
  3. Trả về danh sách entity ID cuối cùng

Xử lý trong trình chỉnh sửa

Để tạo Prefab từ editor:

  • Phần dữ liệu: Tuần tự hóa các Component của Entity
  • Phần hành động: Đăng ký các hàm callback tự động phát hiện quan hệ. Ví dụ:
    • Hàm mount sẽ kiểm tra cấu trúc phân cấp và tạo lệnh mount parent_id child_id

Xử lý Prefab lồng ghép

Khi một Entity được tạo từ Prefab khác:

  • Thêm lệnh produce <id_prefab> vào phần hành động
  • Cho phép xây dựng đệ quy các cấp độ Prefab

Các trường hợp đặc biệt

  • Đối tượng không thuộc cảnh: Như “đèn song song” chỉ tồn tại như thuộc tính cảnh. Chúng cần được xử lý riêng biệt nhưng vẫn nằm trong hệ thống quản lý Prefab.
  • Prefab kế thừa: Một Prefab có thể được chỉnh sửa từ Prefab khác. Có thể lưu dưới dạng “Prefab + patch” hoặc tạo Prefab mới độc lập.

Với thiết kế này, Prefab không chỉ là công cụ tạo đối tượng mà tiến gần đến khái niệm Blueprint của Unreal, cho phép tích hợp cả logic và dữ liệu cảnh game một cách

0%