Thiết Kế Bộ Đếm Thời Gian Tối Ưu Cho Game Engine - nói dối e blog

Thiết Kế Bộ Đếm Thời Gian Tối Ưu Cho Game Engine

Trong một bài viết gần đây, tôi đã chia sẻ ý tưởng xây dựng game engine sử dụng cơ chế timer làm trục chính điều khiển luồng xử lý. Khi số lượng timer tăng lên đáng kể, việc tự xây dựng module timer chuyên dụng sẽ mang lại hiệu quả ổn định hơn so với các giải pháp hệ thống cung cấp. Trong quá trình tái cấu trúc mã nguồn dự án cũ, tôi đã dành thời gian thiết kế lại hoàn toàn hệ thống timer này.

Qua nghiên cứu tài liệu kỹ thuật, tôi tình cờ phát hiện bài phân tích xuất sắc về cơ chế ngắt định kỳ trong nhân Linux. Thiết kế này có nhiều điểm tương đồng với hướng tiếp cận của tôi, nhưng với độ tinh tế cao hơn trong các chi tiết kỹ thuật. Đặc biệt ấn tượng là cách phân cấp sự kiện theo khoảng cách thời gian tới thời điểm kích hoạt, chia thành nhiều cấp độ xử lý.

Trong thiết kế ban đầu, tôi chỉ áp dụng mô hình hai cấp do đặc thù game thường không cần xử lý các sự kiện quá xa trong tương lai. Tuy nhiên qua thực nghiệm, tôi nhận ra việc sắp xếp thông minh kích thước mảng ở từng cấp độ và sử dụng phép toán modulo sẽ mang lại hiệu suất xử lý cực kỳ nhanh chóng, không bị ảnh hưởng bởi số cấp độ phân chia.

Phiên bản mới nhất của module timer đã được viết lại hoàn toàn chỉ với 100 dòng mã, giảm 60% so với phiên bản trước. Thiết kế này có một số điểm khác biệt so với cơ chế timer của Linux, không phải vì thuật toán ưu việt hơn mà do bài toán cụ thể của chúng ta đơn giản hơn:

  1. Mảng struct list_head vec[TVR_SIZE]; thực tế chỉ cần kích thước TVR_SIZE-1. Vì phần tử đầu tiên của mỗi cấp luôn trống, các sự kiện sẽ được phân bổ đầy đủ qua các cấp độ cao hơn. Hệ thống 5 cấp độ hiện tại đã bao quát toàn bộ khoảng thời gian từ 0 đến 0xFFFFFFFF.

  2. Thao tác cascade_timers không cần thực hiện mỗi lần chạy. Thay vào đó, chúng ta chỉ cần xử lý vào thời điểm thích hợp khi thêm timer mới. Khi thêm sự kiện, chúng ta chỉ cần quan tâm đến thời điểm tuyệt đối thay vì khoảng thời gian tương đối.

Giải pháp này tồn tại hai thách thức nhỏ:

  • Khi chạy timer, hiệu năng có thể dao động do phải xử lý cascade_timers vào các thời điểm cụ thể. Tuy nhiên qua kiểm tra thực tế, độ trễ này không đủ để người dùng cảm nhận được.
  • Sử dụng kiểu 32-bit để biểu diễn thời gian tuyệt đối có thể gặp vấn đề tràn số với thời gian vận hành quá dài. Nhưng với đặc thù game client không yêu cầu hoạt động 24/7, 4 tỷ lần tick là hoàn toàn đủ dùng.

Một cải tiến quan trọng khác là loại bỏ hoàn toàn hàm del_timer. Thay vào đó, chúng ta có thể kiểm soát việc hủy bỏ sự kiện thông qua tham số truyền vào khi đăng ký. Việc này giúp đơn giản hóa giao diện API và loại bỏ cấu trúc danh sách liên kết đôi trong triển khai nội bộ.

Qua kinh nghiệm phát triển gần đây, tôi nhận thấy trong game engine thường chỉ cần một hàm callback duy nhất, các hành động khác biệt có thể xác định thông qua tham số truyền vào. Nhờ tích hợp hệ thống script, hàm callback không cần phải là hàm C độc lập mà có thể là hàm script linh hoạt.

Thiết kế cuối cùng của tôi sử dụng nguyên mẫu hàm add_timer như sau:

1
void add_timer(void *arg, int time)

Còn hàm run_timer_list cần nhận thêm tham số hàm callback:

1
void run_timer_list(void (*callback)(void*))

Ví dụ mở rộng với callback tùy biến:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct timer_arg {
    void (*callback)(int);
    int arg;
    int cancel;
};

static void timer_callback(void *arg) {
    struct timer_arg *a = (struct timer_arg*)arg;
    if (!a->cancel) {
        a->callback(a->arg);
    }
    free(a);
}

// Sử dụng: 
run_timer_list(timer_callback);

Giải pháp này mang lại tính linh hoạt vượt trội so với thiết kế gốc của Linux. Trong trường hợp cần hỗ trợ nhiều hàm callback C khác nhau, chúng ta chỉ cần đóng gói hàm và tham số vào cấu trúc dữ liệu truyền vào, đảm bảo tính nhất quán và dễ bảo trì.

0%