Quán Tính Tư Duy - nói dối e blog

Quán Tính Tư Duy

Buổi tối lang thang trong văn phòng, đồng nghiệp đối diện đang cắm cúi viết code tăng ca. Tôi ghé lại xem anh ta đang xử lý gì. Sau khi trò chuyện, tôi hiểu ra vấn đề: Có lẽ là việc triển khai hiệu ứng tăng/bớt thuộc tính nhân vật trên server.

Thuật ngữ “BUFF” hiện rất phổ biến trong các game online. Khi nói “gắn BUFF cho nhân vật”, thông thường là ám chỉ việc điều chỉnh tạm thời các thông số ảo của nhân vật: Ví dụ như Tấn công +5, Phòng thủ giảm 10%, Tốc độ tăng gấp đôi, v.v…

Những ai từng chơi World of Warcraft hẳn sẽ dễ hình dung. Trên thực tế, hệ thống BUFF trong game thường phức tạp hơn ví dụ tôi nêu rất nhiều.

Lần này không bàn về thiết kế game, mà hãy nói về cách triển khai kỹ thuật.

Khung chương trình (framework) mà đồng nghiệp đang xây dựng dành sẵn vài interface cho BUFF, trong đó có hai cái khiến tôi đặc biệt chú ý.
Một cái tên là start, cái còn lại là stop. Chúng lần lượt xử lý logic khi BUFF xuất hiện và khi BUFF biến mất. Đây là thiết kế khá tự nhiên, đặc biệt quen thuộc với lập trình C++ - giống như quá trình khởi tạo và hủy đối tượng.

Ví dụ, với BUFF “Tấn công +5”, ta sẽ tăng 5 điểm công trong sự kiện start, rồi trừ lại 5 điểm khi stop.

Bỗng dưng tôi có linh cảm rằng cách thiết kế này có điểm chưa hợp lý. Vì nó bắt buộc lập trình viên phải viết hai hàm logic ngược nhau. Dạo gần đây, tôi đặc biệt phản cảm với các thiết kế yêu cầu xử lý đôi ngược này.

Thứ nhất, nhiều phép toán tưởng chừng khả nghịch thực chất lại ẩn chứa lỗ hổng. Ví dụ như phép cộng/trừ số thực (floating point) trong những điều kiện nhất định có thể gây sai số không thể đảo ngược (tôi chưa thử dựng ví dụ cụ thể, nhưng tin rằng hoàn toàn có thể xảy ra); hay phép nhân/chia số nguyên định điểm (fixed-point) cũng dễ dẫn đến kết quả không chính xác.

Nghiêm trọng hơn, nếu có BUFF đặt một thông số thành giá trị cố định (ví dụ reset về 0 hoặc đẩy lên max), thì không có biến đệm lưu trữ giá trị gốc, hiệu ứng này sẽ không thể hoàn tác.

Dĩ nhiên, các策划 số liệu (numerical designer) giàu kinh nghiệm thường tách riêng quy tắc nhân/chia, sắp xếp thứ tự tính toán hợp lý. Chẳng hạn, đa số game sẽ cộng dồn các hệ số nhân rồi mới áp dụng một lần duy nhất (ví dụ: 20% + 20% = 40%, chứ không phải 44% như phép nhân (1.2x1.2=1.44)).

(Nhân tiện nhớ lại, ngày mới tiếp xúc Diablo, tôi từng không hiểu tại sao hai món đồ +20% lại chỉ cộng dồn tuyến tính. Cũng có game thiết kế phức tạp hơn, như EVE với cơ chế “overheat penalty” - à, hình như lạc đề rồi…)

Tôi liền hỏi đồng nghiệp về cách xử lý các trường hợp đặc biệt: Ví dụ khi người chơi ngắt kết nối mạng trong lúc có BUFF, làm sao khôi phục số liệu khi đăng nhập lại? Hay nếu trong thời gian BUFF tồn tại, nhân vật nâng cấp hoặc đổi trang bị làm thay đổi thông số gốc, làm sao đảm bảo phép nghịch đảo khi hủy BUFF vẫn chính xác?

Tất nhiên, với framework hiện tại đều có cách giải quyết. Nhưng điều kiện tiên quyết là người lập trình phải lường hết các tình huống, hoặc lưu thêm nhiều dữ liệu trung gian. Khi hai thao tác liên quan (xuất hiện và biến mất của BUFF) nằm ở hai đoạn code khác nhau, lỗi lầm rất dễ nảy sinh.

Tôi tiếp tục hỏi thêm đồng nghiệp ở các dự án khác xem họ triển khai hệ thống tương tự ra sao. Thật bất ngờ, tất cả đều dùng cách tương tự! Hóa ra tư duy con người quả thật có quán tính mạnh mẽ.

Giải pháp tôi nghĩ ra sau đó thực ra rất đơn giản: Hủy bỏ cặp interface start/stop, thay bằng một phương thức apply duy nhất. Nhân vật luôn lưu trữ hai bộ số liệu: Một là thông số gốc, hai là thông số tạm thời đang được áp dụng.

Mỗi khi trạng thái thay đổi (BUFF mới được thêm vào, thông số gốc biến động, hoặc thay đổi trang bị…), toàn bộ BUFF hiện hữu sẽ kích hoạt phương thức apply để tính toán lại thông số tạm thời dựa trên giá trị gốc mới nhất.

Cách này có thể tốn thêm chút tài nguyên tính toán, nhưng thiết kế hệ thống sẽ gọn gàng và minh bạch hơn nhiều.

0%