Ghi Chú Phát Triển (17): Xử Lý Công Thức Bảng Dữ Liệu Thiết Kế - nói dối e blog

Ghi Chú Phát Triển (17): Xử Lý Công Thức Bảng Dữ Liệu Thiết Kế

Trước một thời gian ngắn, tôi đã hỗ trợ kỹ thuật cho nhóm thiết kế bằng cách xây dựng một ngôn ngữ DSL đơn giản. Tuy nhiên, khi số lượng nhà thiết kế gia tăng, tôi nhận ra cách tiếp cận này không còn hiệu quả như mong đợi.

Đa phần các nhà thiết kế đều quen thuộc với việc thể hiện mối quan hệ số liệu thông qua bảng Excel, thay vì sử dụng các biểu thức lập trình truyền thống. Điều này thúc đẩy tôi nghiên cứu một phương pháp thay thế linh hoạt và thực tế hơn, nhằm chuyển đổi tư duy thiết kế thành mã thực thi hiệu quả.

Sau khi phân tích các bảng dữ liệu lịch sử từ dự án trước, tôi xác định được các yêu cầu cốt lõi cần giải quyết. Quá trình có thể chia làm hai giai đoạn: (1) Chuyển đổi bảng Excel thành cấu trúc dữ liệu có thể xử lý; (2) Biến các mối quan hệ dữ liệu thành mã lập trình chạy được.

Yêu cầu về xử lý số học

Phần lớn nhu cầu liên quan đến các biểu thức khai báo (declarative) thay vì hướng thủ tục (imperative). Ví dụ, nhà thiết kế có thể định nghĩa “Máu” = “Tấn công * 10”. Trong trường hợp này, biểu thức “Tấn công * 10” chính là công thức cần xử lý.

Các bảng dữ liệu thường thể hiện mối quan hệ dẫn xuất giữa các thuộc tính. Tại thời điểm chạy chương trình, chỉ cần cung cấp một số thuộc tính cơ bản, hệ thống có thể tính toán tự động các giá trị còn lại thông qua chuỗi công thức. Việc triển khai cơ bản không quá phức tạp – chỉ cần đọc và phân tích các dòng dữ liệu trong bảng.

Giải pháp xử lý khai báo

Công việc chính của hệ thống là thực hiện sắp xếp tô-pô (topological sorting) để xác định thứ tự tính toán. Các thuộc tính không phụ thuộc vào ai sẽ được xử lý trước, tiếp theo là các thuộc tính có mối quan hệ phụ thuộc. Kết quả là một chuỗi lệnh thủ tục có thể thực thi.

Một dạng bảng dữ liệu khác là bảng tra cứu (lookup table), thể hiện mối quan hệ ánh xạ. Ví dụ:

Phép thuật Vật lý
Chiến binh 0.5 1
Pháp sư 1 0.5

Khi công thức cần giá trị “Phép thuật”, hệ thống sẽ tra cứu dựa trên thuộc tính “Nghề nghiệp”. Trong lập trình C, nghề nghiệp có thể biểu diễn dưới dạng enum, còn giá trị phép thuật lưu trữ trong mảng 1 chiều với enum làm chỉ số. Với ngôn ngữ động, sử dụng dictionary linh hoạt hơn, dù hiệu năng không cao.

Giải pháp tổng thể

Dù là biểu thức toán học hay bảng tra cứu, mục tiêu đều là: Biến đổi một tập hợp thuộc tính đầu vào thành các giá trị mới thông qua chuỗi phép biến đổi. Mỗi bảng dữ liệu cung cấp một tập quy tắc chuyển đổi, từ đó tạo thành hệ thống dẫn xuất toàn diện.

Tối ưu hiệu năng

Việc giải quyết yêu cầu trên không khó, đặc biệt với ngôn ngữ động. Tuy nhiên, thách thức thực sự nằm ở hiệu năng. Đối với game MMORPG hành động thời gian thực, mỗi đòn tấn công đều đòi hỏi chuỗi tính toán phức tạp. Nếu phụ thuộc quá nhiều vào bảng tra, hiệu năng hệ thống sẽ bị ảnh hưởng nghiêm trọng.

Giải pháp triệt để nhất là dịch các mối quan hệ số liệu thành mã máy gốc (native code). Tôi chọn công cụ Tiny C Compiler (TCC), cho phép tạo mã C từ Lua rồi biên dịch tức thì.

Cơ chế tương tác Lua-TCC

Đầu tiên, tôi xây dựng giao diện tương tác giữa Lua và TCC. Để đơn giản, chỉ hỗ trợ kiểu dữ liệu float. Hàm C có dạng nguyên mẫu:

1
void func(float input[], float output[]);

Việc triển khai giao diện này thông qua viết tiện ích mở rộng C cho Lua.

Mô-đun Lua tạo mã C

Tôi xây dựng một mô-đun Lua tên “formula” với khả năng quản lý biểu thức và bảng tra cứu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
f = require "formula"
test = f()
test:table("ThuocTinh", { "Chiến binh", "Pháp sư" }, 
  { ["Vật lý"] = { 1, 0.5 }, ["Phép thuật"] = { 0.5, 1 } })
test:expression("Sức mạnh", "Lực * 2")
test:expression("Máu", "Sức bền + 1")
test:expression("Sát thương", "Sức mạnh / Máu * Vật lý")
test:lookup("Vật lý", "ThuocTinh", "Nghề nghiệp")
test:compile { "Sát thương" }
print(test { ["Lực"] = 2, ["Sức bền"] = 3.5, ["Nghề nghiệp"] = "Chiến binh" })

Mã C được sinh ra

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
float table_1[] = {1, 0.5}; 
void main(float input[], float output[]) { 
    float t[7]; 
    t[0] = input[0]; 
    t[2] = input[1]; 
    t[4] = input[2]; 
    t[1] = t[0] + 1; 
    t[3] = t[2] * 2; 
    t[5] = table_1[(int)t[4]]; 
    t[6] = t[3] / t[1] * t[5]; 
    output[0] = t[6]; 
} 

Cơ chế ẩn danh

Hệ thống coi 7 biến trung gian như 7 thanh ghi. Quá trình phân tích tương đương với phân bổ thanh ghi. Bảng tra cứu được trích xuất thành mảng tĩnh, đồng thời chuyển đổi enum nghề nghiệp từ Lua sang số nguyên trước khi truyền vào hàm C.

Triển vọng tối ưu

Mã C hiện tại có thể tối ưu thêm về hiệu năng và bộ nhớ. Đây sẽ là hướng phát triển trong tương lai.

Lưu ý: Kho lưu trữ Git phiên bản mới nhất của TCC có tại đây

0%