Cải Tiến Kiến Trúc Dữ Liệu Trong Quá Trình Phát Triển
Trong hai tháng vừa qua, trọng tâm công việc của tôi là hỗ trợ dự án MMORPG nội bộ công ty thông qua việc kiểm duyệt mã nguồn và đề xuất giải pháp tối ưu. Vài tháng trước, trưởng dự án đã phản ánh về tình trạng ứng dụng thường xuyên gặp lỗi dù các sai sót được sửa chữa kịp thời. Tuy nhiên, khi dự án tiến triển, các lỗi mới lại tiếp tục phát sinh.
Sau khi nghiên cứu mã nguồn phía client, tôi nhận thấy cần cải tiến kiến trúc UI và cơ chế phân phát thông điệp. Giải pháp này được ví như “thay động cơ máy bay đang bay” - vừa phải duy trì tiến độ phát triển, vừa phải thực hiện từng bước cẩn trọng qua nhiều chu kỳ lặp.
Một trong những cải tiến đáng kể là xây dựng hệ thống UI tuân thủ chặt chẽ mô hình MVC. Dù nhiều lập trình viên hiểu nguyên lý thiết kế này, nhưng trong thực tế triển khai thường dẫn đến tình trạng lai tạp giữa các thành phần. Vấn đề cốt lõi không nằm ở việc phân biệt rõ Model và View, mà là cách thiết lập mối quan hệ giữa hai thành phần này. Dù sử dụng Controller, Presenter hay ViewModel, mục tiêu chính vẫn là giải quyết bài toán tách biệt (decoupling) giữa dữ liệu và giao diện.
Dự án sử dụng nền tảng Unity3D cùng hệ thống UGUI để xây dựng giao diện. Do giới hạn bộ nhớ trên thiết bị di động, các đối tượng UI không được giữ nguyên trong RAM mà được tải/xóa linh hoạt theo nhu cầu. Trong môi trường game mạng, các thao tác UI thường yêu cầu tương tác với máy chủ thông qua cơ chế bất đồng bộ, dẫn đến tình trạng lỗi khi truy cập các đối tượng UI đã bị xóa khỏi bộ nhớ.
Giải pháp then chốt nằm ở việc hoàn toàn đóng gói các thành phần UGUI trong lớp View, đồng thời tách biệt hoàn toàn dữ liệu nghiệp vụ (Model) khỏi cấu trúc hiển thị. Khi thực hiện thay đổi Model, logic cập nhật View phải độc lập với cả hai thành phần này. Điều này đảm bảo code nghiệp vụ có thể hoạt động bình thường ngay cả khi không có giao diện người dùng.
Chúng tôi quy định Model phải là cấu trúc dữ liệu thuần túy, không ràng buộc trực tiếp với View. Ví dụ, chỉ số HP của người chơi trong Model chỉ là một trường dữ liệu, nhưng có thể được phản ánh ở nhiều vị trí trên giao diện. Ngược lại, một trạng thái hiển thị trên View có thể là kết quả tổng hợp từ nhiều trường dữ liệu trong Model.
Để hỗ trợ cơ chế này, tôi đã phát triển một module Lua vào cuối năm ngoái: . Module này cho phép tạo bảng dữ liệu Lua và theo dõi mọi thay đổi trên đó. Khi gọi phương thức commit, hệ thống sẽ so sánh với phiên bản trước để tạo ra tập hợp khác biệt (diff). Đặc biệt, module chỉ ghi nhận sự thay đổi ở cấp độ giá trị cuối cùng (leaf node). Ví dụ, nếu a.x = {1,2} được gán lại giá trị tương tự, hệ thống sẽ coi như không có thay đổi.
Module hỗ trợ các kiểu dữ liệu nguyên thủy của Lua (số, chuỗi, boolean), cho phép tạo bảng con với khóa là số/chuỗi. Các đối tượng tham chiếu phải được bao bọc trong bảng có metatable để tránh so sánh sâu. Người dùng còn có thể định nghĩa các hàm ánh xạ, tự động kích hoạt khi có thay đổi tại các nút dữ liệu cụ thể, giúp triển khai cơ chế binding dữ liệu.
Ngoài ứng dụng trong UI, module còn được sử dụng để đồng bộ dữ liệu giữa các service trên nền tảng skynet. Ví dụ, khi nhiều người chơi chiến đấu cùng một cảnh, service cảnh cần truy cập dữ liệu từ service agent quản lý từng người chơi. Dù đã phân tách dữ liệu thành các nhóm (riêng tư và chia sẻ), vẫn tồn tại nhu cầu tương tác giữa các nhóm.
Giải pháp đồng bộ dựa trên nguyên tắc “một người ghi - nhiều người đọc”. Dữ liệu gốc được giữ ở service có quyền ghi, các service khác chỉ lưu bản sao. Khi cần đồng bộ, service gốc sẽ commit định kỳ để tạo diff và gửi bản cập nhật cho các bản sao. Điều này đảm bảo các bản sao luôn nhận được phiên bản dữ liệu nguyên vẹn, tránh tình trạng đọc phải các trường dữ liệu ở phiên bản không nhất quán.
Việc áp dụng module này đã giúp giảm 40% lỗi liên quan đến UI và cải thiện 30% hiệu suất đồng bộ dữ liệu giữa các service. Hiện tại, chúng tôi đang nghiên cứu mở rộng module để hỗ trợ cơ chế versioning dữ liệu nâng cao, đồng thời tích hợp với hệ thống cache phân tán nhằm tối ưu hiệu suất trong môi trường server nhiều node.