Translate

Thứ Năm, 13 tháng 3, 2014

Game loop – nhịp đập con tim

Không game nào có thể chạy nếu thiếu game loop, và cũng ko thể chạy tốt nếu game loop ko được cài đặt một cách cẩn thận.
The game loop
Tất cả các game đều gồm một dãy các công việc sau được lặp đi lặp lại: lấy input từ user, cập nhật trạng thái game (game state), xử lý trí tuệ nhân tạo (AI), phát âm thanh, và vẽ lên màn hình đồ họa. Các công việc đó được thực hiện bên trong game loop. Tuy nhiên, chỉ có 2 công việc chính quan trọng nhất: update game state và render graphic.
Đây là đoạn code mẫu đơn giản cho một kiểu game loop đơn giản:
1
2
3
4
5
6
bool game_is_running = true;
while (game_is_running)
{
    update();
    render();
}
Vấn đề của đoạn code trên là nó không thỏa mãn tính “real time”, vốn là tính chất quan trọng của game. Trên phần cứng mạnh, nó chạy nhanh, và ngược lại. Thế nên, chúng ta sẽ xem xét một vài cách cài đặt game loop tốt hơn.
Trước hết, có 2 khái niệm cơ bản cần được giải thích:
  • FPS: là Frames Per Second, là số lần hàm render() được gọi trong 1 giây.
  • Game Speed: là số lần gọi hàm update() trong 1 giây.
FPS phụ thuộc vào Game Speed cố định trước
Một giải pháp rất dễ là chúng ta cố định số FPS, ví dụ là 25, tức là sau khi cả update() và render() được gọi, cho dù là xong sớm, ta vẫn phải nghỉ (sleep) một chút cho phù hợp rồi mới lặp tiếp. Điều này đảm bảo cho game loop chạy được đúng 25 lần trong 1 giây.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const int FRAMES_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;
DWORD next_game_tick = GetTickCount();
/* GetTickCount() là hàm trên Win32 trả về số milliseconds đếm được kể từ khi máy tính bật lên */
int sleep_time = 0;
bool game_is_running = true;
while (game_is_running)
{
    update();
    render();
    next_game_tick += SKIP_TICKS;
    sleep_time = next_game_tick - GetTickCount();
    if (sleep_time >= 0) {
        Sleep(sleep_time);
    } else
        // Shit, ko được nghỉ tí nào
    }
}
Kiểu game loop này có 1 ưu điểm lớn: dễ cài đặt. Do số lần update() là biết trước nên việc viết code không có gì khó khăn. Ví dụ, trong game có tính năng replay, đơn giản ta chỉ cần log lại input của user và replay khi cần sau này (tất nhiên là game ko có giá trị ngẫu nhiên được sinh ra).
Tùy vào phần cứng, ta cần chọn FPS cho phù hợp.
Slow hardware: nếu sleep_time luôn >= 0 thì ko có vấn đề gì, nhưng nếu thỉnh thoảng ko được như thế, game loop ko được nghỉ, và game sẽ có lúc chạy chậm, lúc chạy nhanh, ko ổn định.
Fast hardware: ko có vấn đề gì ngoại trừ việc ta hơi tiếc khi game có thể chạy ở mức FPS cao hơn nhiều so với 25. Tuy nhiên, điều này lại phù hợp với các thiết bị di động, vì như thế sẽ đỡ tốn pin.
Kết luận: kiểu game loop này thì code dễ nhưng cần cân nhắc khi chọn FPS.
Game Speed phụ thuộc vào biến FPS
Hàm update() có thể có 2 loại: 1 loại là cứ thế update(), không quan tâm bây giờ là thời điểm nào, chỉ quan tâm đến game state, đây là loại nãy giờ ta nhắc đến; loại thứ 2 thì ngược lại, nó quan tâm xem kể từ lần update() cuối cùng cho đến bây giờ đã là bao lâu, để từ đó biến đổi game state cho phù hợp. Với loại thứ 2 này, ta có kiểu game loop sau:
1
2
3
4
5
6
7
8
9
10
11
DWORD prev_frame_tick;
DWORD curr_frame_tick = GetTickCount();
bool game_is_running = true;
while (game_is_running)
{
    prev_time_tick = curr_time_tick;
    curr_frame_tick = GetTickCount();
    update(curr_frame_tick - prev_frame_tick);
    render();
}
Dĩ nhiên lúc này code bên trong hàm update() sẽ phức tạp hơn do ta phải tùy cơ ứng biến với tham số delta_time nhận vào. Đây là 1 kiểu game loop rất phổ biến. Nhưng vẫn có nhược điểm.
Slow hardware: làm cho render() chạy quá lâu suy ra delta_time sẽ lớn, và update() sẽ cho ra lò 1 game state trong tương lai khá xa. Game sẽ giật là cái chắc.
Fast hardware: render() chạy rất nhanh, delta_time cực nhỏ. Trông thì có vẻ tốt đấy, nhưng việc tính toán số float, double trên máy tính vốn có nhiều sai số. Việc chia nhỏ quá mức các phép tính toán sẽ xảy ra sai số lớn. Ví dụ thay vì a = a + 1 luôn một lần thì lại a = a + 0.01trong một trăm lần. Không gì đảm bảo kết quả sẽ chính xác tuyệt đối.
Kết luận: kiểu này ko được tốt cho lắm, nhưng trong vài game ít tính toán số thực, thì dùng được.
Cố định Game Speed với FPS lớn nhất có thể được
Trong kiểu game loop thứ nhất, FPS phụ thuộc vào Game Speed cố định, sẽ có vấn đề với slow hardware. Ta thử cải tiến nó, tức là khi update() xong mà quá muộn, ko được nghỉ tí nào, thì thôi khỏi cần gọi render() chi nữa, đã muộn lại càng muộn, thay vào đó, ta lại update() tiếp ngay. Điều này hy vọng vớt vát chút ít cho game khi chạy với slow hardware.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const int TICK_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / TICK_PER_SECOND;
const int MAX_FRAMESKIP = 10;
DWORD next_game_tick = GetTickCount();
int loops;
bool game_is_running = true;
while (game_is_running)
{
    loops = 0;
    while (GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
        update();
        next_game_tick += SKIP_TICKS;
        loops++;
    }
    render();
}
Game sẽ được update() đúng 25 lần trong 1 giây, qua kinh nghiệm cho thấy 25 là quá đủ. Trong khi render() sẽ được gọi nhiều lần nhất có thể. Tuy nhiên sẽ có thể thỉnh thoảng render() lại 1 vài hình ảnh như cũ.
Slow hardware: cũng có thể hy vọng game chạy ko chậm đến mức tệ lắm.
Fast hardware: giống kiểu game loop đầu tiên.
Kết luận: đây là kiểu game loop tôi hay dùng nhất khi lập trình game cho mobile. Tôi thấy khá ổn.
Ref:

Không có nhận xét nào:

Đăng nhận xét