Front End

11 dez, 2013

Criando um Game Loop em HTML5

Publicidade

Um Game Loop é o processo base de todo jogo, que basicamente representa seu ciclo de vida. Resumindo, podemos separá-lo em três etapas: processamento de entradas, atualização da lógica do jogo e renderização. Estas etapas se repetem até o jogador finalizar a execução do aplicativo. Neste artigo iremos trazer uma das maneiras de escrever um game loop utilizando os recursos disponíveis na nova especificação HTML5, utilizando JavaScript.

O Loop

Como o próprio nome já diz, um Game Loop é basicamente uma iteração. Em muitas linguagens poderíamos escrever o loop da seguinte maneira:

while (gameRunning)  {
        processInput();
        updateGame();
        render();
}

Porém, teremos problemas ao rodar este código em Javascript, uma vez que o navegador espera o script terminar para atualizar a página. O script travaria a execução e a página nunca seria atualizada.

Para resolvermos este problema podemos utilizar uma função nativa JavaScript chamada setTimeout. Nela podemos agendar a execução de uma função. Veja o próximo exemplo.

/**
        1s = 1000ms
        Rodando em 60 frames por segundo
*/
var FRAME = 1000 / 60 ; // Taxa de atualização
var loop = function() {
        processInput();
        updateGame();
        render();
        setTimeout(loop, FRAME);
};
setTimeout(loop, FRAME);

Neste exemplo, a cada iteração do nosso loop agendamos a execução da próxima iteração. A taxa de atualização é baseada na frequência usada na maioria dos dispositivos atuais: 60 frames por segundo.

O setTimeout é uma função muito útil, porém ela tem alguns problemas quando utilizada em um game loop. Ela não está sincronizada com a taxa de renderização da página, além de que, o agendamento da função pode não ser tão preciso e a rotina ser executada fora dos milisegundos requisitados.

Neste caso, podemos usar o requestAnimationFrame no lugar do setTimeout. O código a seguir demonstra a alteração:

var loop = function() {
 updateGame();
 render();
 window.requestAnimationFrame(loop);
};
window.requestAnimationFrame(loop);

Esta função foi criada pensando em animações. Ao requisitarmos um frame de animação, o navegador irá nos responder quando ele estiver pronto para atualizar a página! Desta maneira nosso loop fica totalmente sincronizado com a taxa de atualização do navegador, mantendo a nossa animação mais suave.

Desenhando na tela

De nada adianta um game loop sem alguma maneira de desenharmos na tela. Existem várias maneiras de realizar esta atividade; nesta seção iremos utilizar a tag canvas para exibir a animação de nosso game loop.

A Tag canvas foi introduzida com a especificação do HTML5. Com ela é possível marcar uma área da página para ser renderizado formas geométricas e imagens programaticamente por meio de uma API JavaScript.

Com o código abaixo renderizamos uma quadrado centralizado no meio da área do canvas (demonstração online: http://jsfiddle.net/erickzanardo/X7WgY/)

HTML

<canvas id="mycanvas" width="300" height="300">

Javascript

<canvas id="mycanvas" width="300" height="300">
JavaScript
var canvas = document.getElementById('mycanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#000';
ctx.fillRect(125, 125, 50, 50);

Na primeira linha recuperamos o objeto dom do canvas e em seguida pegamos o objeto de contexto ‘2d’. Este objeto nos da acesso a inúmeras funções usadas para renderizar as formas no canvas. Neste exemplo utilizamos o fillStyle para determinar a cor de preenchimento dos objetos que serão renderizados a seguir e abaixo desenhamos um quadrado na posição utilizando as coordenadas (x: 125, y: 125) com 50 de largura e 50 altura.

Com este recurso é muito simples desenharmos uma imagem também. Veja o exemplo (demonstração online:http://jsfiddle.net/erickzanardo/gd63S/):

var canvas = document.getElementById('mycanvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = 'http://www.freephotosbank.com/photographers/photos1/5/med_bb124s2577.jpg';
img.onload = function() {
 ctx.drawImage(img, 0, 0);
};

Este exemplo é bem semelhante ao anterior: após recuperamos o objeto de contexto 2d do canvas, criamos um objeto Image, atribuímos o caminho de origem da imagem e uma função de callback que será invocada quando a imagem for carregada assincronamente. Após ter sido carregada nossa função do onload irá desenhar a imagem no canvas utilizando o método drawImage, na posição (x: 0, y: 0).

Outro recurso muito útil do canvas no desenvolvimento de jogos é a possibilidade da pré-renderização de gráficos para a utilização em outro canvas. Isto é muito útil em um game loop, podemos por exemplo desenhar algum objeto que será utilizado várias vezes antes do jogo começar, e então reaproveitar a imagem pré renderizada nas próximas iterações do game loop, poupando assim processamento da CPU.

No exemplo abaixo, um simples carro é desenhado em um canvas a parte e, então, ele é desenhado como se fosse uma imagem (demonstração online: http://jsfiddle.net/erickzanardo/RHZL6/):

var rendered = document.createElement('canvas');
rendered.width = 100;
rendered.height = 70;
var renderedCtx = rendered.getContext('2d');
renderedCtx.fillStyle = '#E5E5E5';
renderedCtx.fillRect(5, 30, 90, 30); // Lateral
renderedCtx.fillRect(30, 5, 40, 30); // Lateral
renderedCtx.fillStyle = '#000';
// Roda 1
renderedCtx.beginPath();
renderedCtx.arc(30, 60, 10, 0, Math.PI*2, true);
renderedCtx.closePath();
renderedCtx.fill();
// Roda 2
renderedCtx.beginPath();
renderedCtx.arc(75, 60, 10, 0, Math.PI*2, true);
renderedCtx.closePath();
renderedCtx.fill();
var canvas = document.getElementById('mycanvas');
var ctx = canvas.getContext('2d');
ctx.drawImage(rendered, 10 ,10);

Montando o Game Loop

Agora que sabemos como desenhar na tela, vamos terminar no nosso game loop e criar uma simples interação com o jogador. Iremos criar um simples carro que se move na tela utilizando as teclas ASDW. Veja o código abaixo com comentários sobre cada comando (demonstração online: http://jsfiddle.net/erickzanardo/tLBDr/):

var renderCar = function() {
    // Omitido
}
var car = renderCar(); // Pré renderiza a imagem de nosso carro
var ctx = document.getElementById("game").getContext("2d");
document.body.onkeypress = function(e) {
    processInput(e.keyCode || e.which);
}
// KEYS - Constantes para referenciar o código a tecla (lower case)
var A = 97
var D = 100
var S = 115
var W = 119
/*
    Controla nosso input, para este exemplo simplesmente armazenamos
    a última tecla pressionada pelo jogador
*/
var lastKeyPressed;
var processInput = function(keyCode) {
    lastKeyPressed = keyCode;
};
/*
    A Lógica de nosso jogo é bem simples, precisamos de duas variáveis
    para armazenar a posição atual do carro e de acordo com a tecla pressionada
    incrementamos ou decrementamos as coordenadas para mover o objeto
*/
var x = 0, y = 0;
var updateGame = function() {
    if (lastKeyPressed == A) {
        x -= 10;
    } else if (lastKeyPressed == D) {
        x += 10;
    } else if (lastKeyPressed == W) {
        y -= 10;
    } else if (lastKeyPressed == S) {
        y += 10;
    }
    lastKeyPressed = null;
};
var render = function() {
    // Limpamos o que havia sido desenhado na última iteração
    ctx.clearRect(0, 0, 300, 300);
    // Desenhando a imagem do carro
    ctx.drawImage(car, x, y);
};
var loop = function() {
    updateGame();
    render();
    window.requestAnimationFrame(loop);
};
window.requestAnimationFrame(loop);

Precisamos fazer apenas mais um ajuste em nosso game loop para que o mesmo fique totalmente funcional.

Repare na função update. Estamos utilizando um valor fixo de 10 pixel para mover nosso carro. Isto pode ser um problema, uma vez que nem todo dispositivo tem a mesma capacidade computacional e gerar a sensação de lentidão no jogo.

Podemos reduzir este problema criando um controle de tempo entre as nossas atualizações. Este controle consiste em gerar um delta entre a atualização anterior e a atual é usar este delta para determinar qual o valor em pixels deve ser movido o objeto. Para isso precisamos determinar a velocidade desejada para o objeto (100 pixels por segundo por exemplo) e calcular a quantidade de pixel a ser movida em relação ao delta. Veja os trechos de código modificado (demonstração online: http://jsfiddle.net/erickzanardo/tLBDr/)

var calcSpeed = function(delta, pixelsPerSec) {
 return ((pixelsPerSec * delta) / 1000);
};
var x = 0, y = 0;
var updateGame = function(delta) {
 if (lastKeyPressed == A) {
 x -= calcSpeed(delta, 100);
 } else if (lastKeyPressed == D) {
 x += calcSpeed(delta, 100);
 } else if (lastKeyPressed == W) {
 y -= calcSpeed(delta, 100);
 } else if (lastKeyPressed == S) {
 y += calcSpeed(delta, 100);
 }
 lastKeyPressed = null;
};
var lastUpdate;
var loop = function() {
 var t = new Date().getTime();
 var delta = t - lastUpdate;
 updateGame(delta);
 render();
 lastUpdate = new Date().getTime();
 window.requestAnimationFrame(loop);
};
lastUpdate = new Date().getTime();
window.requestAnimationFrame(loop);

Nosso game loop está finalizado e funcionando, temos várias coisas para melhorarmos em nosso loop, Neste link tem o exemplo exibido neste artigo melhor organizado, sintam-se a vontade para se basear nele e criar o seu próprio game loop.

Dúvidas ou sugestões? Fiquem a vontade para comentar.

Leituras adicionais