Tìm hiểu vòng lặp game (Game loop)

Phần cốt lõi của hầu hết các game chính là vòng lặp được dùng để cập nhật và hiển thị trạng thái của game. Trong bài viết này, sẽ minh họa các phương pháp tạo vòng lặp game với ngôn ngữ javascript.

Vòng lặp cơ bản

Một vòng lặp game cơ bản bao gồm các việc được thực hiện theo thứ tự sau:

while(gameRunning)
{
	processInput(); // keyboard, mouse,...
	updateGame();
	draw();
	// checkGameOver();
}

Minh họa:

Basic Game Loop

Trong javascript, thay vì dùng vòng lặp, ta sẽ thay thế bằng setInterval() hoặc setTimeout(). Thông thường bạn chỉ cần xác định một giá trị interval thích hợp theo tốc độ của game.

function gameLoop()
{
	processInput();
	updateGame();
	draw();
	setTimeout(gameLoop,100); // 10 fps
}

Hợp lý hơn khi bạn muốn xác định rõ số khung hình/giây (fps):

function gameLoop()
{
	processInput();
	updateGame();
	draw();
	setTimeout(gameLoop,100); // 10 fps
}

Vòng lặp có tính toán thời gian

Tuy nhiên, không phải lúc nào công việc update và draw cũng hoàn thành trước khi lần gọi kế tiếp được thực hiện. Như vậy tốc độ của game sẽ không đồng nhất trên các thiết bị có cấu hình khác nhau.

Để giải quyết, ta sẽ sử dụng đến thời gian hệ thống để so sánh và quyết định thời điểm update/draw.

const FPS = 60;
const TICKS = 1000/FPS;
var lastUpdateTime;

function gameLoop()
{
	var now = Date.now();
	var diffTime = now - lastUpdateTime;
	if(diffTime >= TICKS)
		processInput();
		updateGame();
		lastUpdateTime = now;
	}
	draw();
	var sleepTime = TICKS - diffTime;
	if(sleepTime<0)
		sleepTime = 0;
	setTimeout(gameLoop,sleepTime);
}

Phương pháp trên chạy ổn với giá trị diffTime nhỏ hơn TICKS. Nghĩa là tốc độ của game không vượt quá giá trị TICKS cho phép. Tuy nhiên trong trường hợp diffTime lớn, việc cập nhật sẽ diễn ra chậm.
Giải pháp cuối cùng
Giải pháp của ta là sẽ thực hiện update với số lần dựa vào tỉ lệ diffTime/TICKS trong một lần lặp của game. Sẽ hiệu quả hơn nếu ta bỏ qua việc draw và thực hiện update liên tiếp vì sẽ giúp tăng tốc độ game để bù vào khoảng thời gian bị trì hoãn.

const FPS = 60;
const TICKS = 1000/FPS;

var lastUpdateTime;

function gameLoop()
{
	var diffTime = Date.now() - lastUpdateTime;
	var numOfUpdate = Math.floor(diffTime/TICKS);
	for(var i = 0;i < numOfUpdate;i++){
		processInput();
		updateGame();
	}
	if(diffTime >= TICKS)
		draw();

	lastUpdateTime += TICKS * numOfUpdate;
	diffTime -= TICKS * numOfUpdate;
	var sleepTime = TICKS - diffTime;
	setTimeout(gameLoop,sleepTime);
}

Nếu bạn sử dụng requestAnimationFrame cho vòng lặp game, bạn sẽ không cần quan tâm đến việc tính toán giá trị sleepTime.
Ví dụ hoàn chỉnh
Kiểm tra ví dụ này và so sánh với các phương pháp trước đó, thực hiện một vài công việc “quá tải” nào đó trên trình duyệt và bạn sẽ thấy khác biệt:

const FPS = 6;
const TICKS = 1000 / FPS;
var startTime;
var expectedCounter = 0;
var lastUpdateTime;
var actualCounter = 0;
var output;

function gameLoop()
{
    var diffTime = Date.now() - lastUpdateTime;
    var numOfUpdate = Math.floor(diffTime/TICKS);
    for(var i = 0;i < numOfUpdate;i++){
        updateGame();

    }
    if(diffTime >= TICKS)
        draw();

    lastUpdateTime += TICKS * numOfUpdate;
    diffTime -= TICKS * numOfUpdate;
    var sleepTime = TICKS - diffTime;
    setTimeout(gameLoop,sleepTime);
}

function updateGame() {
    actualCounter++;
}

function draw() {
    var s = "Actual updates: "+actualCounter;
    s += "<br/>Expected updates: "+Math.floor((Date.now()-startTime)/TICKS);
    output.innerHTML = s;
}
// onLoad
output = document.getElementById("output");
startTime = Date.now();
lastUpdateTime = startTime;
gameLoop();

Output:

Actual updates: 1323
Expected updates: 1323