Đã Thiết Kế Một Định Dạng Dữ Liệu - nói dối e blog

Đã Thiết Kế Một Định Dạng Dữ Liệu

Gần đây tôi dành thời gian thiết kế và triển khai một định dạng dữ liệu mới cho engine game của chúng tôi. Trước đây, chúng tôi luôn sử dụng Lua để mô tả dữ liệu, nhưng khi hệ thống kiểu dữ liệu ngày càng hoàn thiện, đồng nghiệp đề xuất nên thiết kế một định dạng chuyên dụng. Định dạng mới cần đạt được nhiều mục tiêu: dễ viết và đọc hơn Lua, thân thiện với công cụ so sánh (diff), phù hợp với hệ thống kiểu dữ liệu của chúng tôi, đồng thời có hiệu suất phân tích cú pháp cao hơn.

Lua vốn đã có trình phân tích hiệu suất cao, nhưng khi mô tả cấu trúc dữ liệu phức tạp, nó phải trải qua hai bước: đầu tiên tạo bytecode xây dựng cấu trúc dữ liệu, sau đó chạy bytecode trong máy ảo để tạo ra cấu trúc cuối cùng. Quá trình hai bước này làm chậm tốc độ và tiêu tốn nhiều bộ nhớ hơn so với phương pháp quét một lượt.

Chúng tôi đã xem xét các định dạng dữ liệu phổ biến nhưng đều gặp phải những hạn chế:

  • JSON tuy phổ biến nhưng thích hợp hơn cho giao thức truyền thông do chương trình tự động tạo ra. Nó không thân thiện với người viết tay: không hỗ trợ chú thích, key trong từ điển phải dùng ngoặc kép, kiểu dữ liệu hạn chế và không biểu diễn chính xác số thực.
  • XML nghiêm ngặt hơn và mở rộng kiểu tốt hơn, nhưng yêu cầu quá nhiều thẻ định dạng ngay cả với dữ liệu đơn giản. Việc viết tay khó khăn nếu không có trình soạn thảo chuyên dụng, thông tin hữu ích bị lấn át bởi thẻ XML, gây khó khăn cho việc so sánh và đọc dữ liệu.
  • INI phù hợp với file cấu hình nhưng không thể mô tả cấu trúc phức tạp. Nó chỉ thêm cấp bậc vào cặp key-value, khiến việc biểu diễn cấu trúc đa tầng trở nên lộn xộn.
  • Lisp là lựa chọn yêu thích của tôi - Paradox và Naughty Dog đều dùng cấu trúc kiểu Lisp. Tuy nhiên, đồng nghiệp không thích quá nhiều dấu ngoặc tròn.
  • YAML có vẻ lý tưởng về tính dễ đọc/viết, nhưng trình phân tích phức tạp và hiệu suất chưa cao. Việc đảm bảo tính nhất quán giữa các ngôn ngữ lập trình khiến việc mở rộng trở nên khó khăn.

Vào một ngày cuối tuần mùa đông nắng ấm, khi con trai tôi đang ngủ trưa, tôi bắt đầu viết trình phân tích cú pháp đầu tiên. Tôi nghĩ chỉ cần một buổi chiều là xong, nhưng thực tế không đơn giản như vậy.

Phiên bản đầu tiên mô phỏng định dạng của Paradox. Ban đầu tôi định dùng Lua/LPEG nhưng sau đó quyết định viết bằng C để đạt hiệu suất cao hơn. Chỉ với vài trăm dòng C, trình phân tích có thể quét một lượt và tạo cấu trúc dữ liệu Lua. Tuy nhiên, khi đồng nghiệp xem thử, họ không hài lòng vì vẫn còn quá nhiều dấu ngoặc.

Tôi quyết định cải tiến bằng cách kết hợp các ý tưởng từ Markdown và YAML:

  • Dùng ký hiệu ### để đánh dấu cấp bậc như Markdown
  • Giữ lại ngoặc {}[] để biểu diễn từ điển/danh sách
  • Cho phép thụt lề (indent) biểu diễn cấp bậc, nhưng quy định rõ ràng:
    • Thụt lề không bắt buộc là tab hay khoảng trắng, chỉ cần chuỗi thụt lề giống nhau cho cùng cấp
    • Cấp độ sâu hơn phải có chuỗi thụt lề dài hơn
    • Cho phép dùng --- để phân đoạn như YAML

Một cải tiến quan trọng là hỗ trợ anchor reference (tham chiếu neo):

  • Cho phép tham chiếu ngược (backward reference) thay vì chỉ tham chiếu xuôi như YAML
  • Khi gặp anchor chưa định nghĩa, tạo một bảng trống trong Lua và điền sau khi anchor được định nghĩa
  • Tính năng này rất hữu ích khi serializing cây cảnh quan hoặc đồ thị có hướng không chu trình (DAG)

Đối với kiểu dữ liệu tùy chỉnh:

  • Dùng [] thay vì {} để kích hoạt hàm callback xử lý hậu kỳ
  • Ví dụ: [file path] sẽ được xử lý thành userdata biểu diễn đường dẫn file
  • So với cách Unity dùng YAML (dùng cấu trúc {fileID: …, guid: …}), cách của chúng tôi đơn giản và trực tiếp hơn

Dự án đã phát triển trong 3 tuần (khác xa với dự kiến ban đầu là một buổi chiều!). Hiện tại, chúng tôi đang chuẩn bị tích hợp vào engine chính. Định dạng mới này kết hợp những điểm mạnh của nhiều định dạng hiện có, đồng thời tối ưu hóa cho hiệu suất và tính linh hoạt trong hệ sinh thái Lua của chúng tôi.

Kho lưu trữ GitHub của dự án đã sẵn sàng. Khi ổn định, nó sẽ trở thành thành phần chính trong hệ thống dữ liệu của engine.

0%