Tutorial HTML 5 – Parte 2/3

Na primeira parte do tutorial de HTML 5 vimos como usar a tag <canvas> e como mostrar imagens nela, criando uma pequena biblioteca para facilitar o trabalho.

Nesta segunda parte ampliaremos nossa biblioteca adicionando um motor simples, com um gameloop bem básico e tratamento de entrada (mouse e teclado).

Na parte anterior criamos um biblioteca básica para utilização do <canvas>, a qual ficou em um arquivo chamado aojcanvas.js. As rotinas dela servem de atalhos para procedimentos que requerem mais de uma linha de código, como desenhar um retângulo preenchido ou desenhar uma imagem rotacionada na tela.

No entanto isso tudo referia-se apenas ao uso básico do objeto canvas, sem nada que lembrasse um jogo de fato.

A arquitetura comum de um jogo sempre consiste em uma repetição onde temos o tratamento da entrada, atualização da lógica do jogo e atualização da parte visual (mostrar o jogo na tela). Estes procedimentos ficam sendo repetidos indefinidamente, até que o jogo seja desligado. A esta repetição chamamos de game loop e cada volta que este loop dá chamamos de pulso.

Dito isso, vejamos como criar um game loop em javascript para rodar no navegador.

Game loop

O nosso “motor”, por assim dizer, ficará em um arquivo chamado aojgame.js e nele teremos o código abaixo.

var _aojFPS = 25;
var _aojPulseCallback = null;
var _aojPulsesCount = 0;
var _aojPulsesInLastSecond = 0;
var _aojLastMilisecond = 0;
 
function aojFPS(optionalNewValue) {
    if (optionalNewValue) {
        _aojFPS = optionalNewValue;
    }
    return _aojFPS;
}
 
function aojSetPulseCallback(callbackFunction) {
    _aojPulseCallback = callbackFunction;
}
 
function aojStartGameLoop() {
    if (!_aojPulseCallback) {
        alert('É preciso especificar a função a ser chamada a cada pulso, com aojSetPulseCallback. GameLoop não iniciado.');
    } else if (!_aojCanvas) {
        alert('É preciso especificar o canvas a ser usado, com aojSetCanvas. GameLoop não iniciado.');
    } else {
        _aojLastMilisecond = new Date().getTime();
        setInterval('aojPulse()', 1000 / _aojFPS);
    }
}
 
function aojPulse() {
    if (_aojPulseCallback) {
        _aojPulsesCount ++;
        var d = new Date();
        if (d.getTime() - _aojLastMilisecond &gt; 1000) {
            _aojLastMilisecond = d.getTime();
            _aojPulsesInLastSecond = _aojPulsesCount;
            _aojPulsesCount = 0;
        }
        _aojPulseCallback();
        aojFillRect(6,5,50,20, 'blue');
        aojSetFont('');
        aojDrawText(_aojPulsesInLastSecond + ' fps', 10, 20, 'yellow');
    }
}

Temos apenas quatro rotinas, sendo as duas primeiras apenas de configuração.

aojFPS() serve para indicar qual a taxa de frames por segundo desejada para o jogo. Importante notar que este motor simples não tem separação entre update e render – ele chama apenas uma rotina de pulso.

O nome da rotina de pulso é definido por você, fora do motor, e passado como parâmetro para a segunda rotina de configuração que temos: aojSetPulseCallback().

Callback é como chamamos a técnica de, ao chamar uma rotina (geralmente de uma biblioteca), passar para ela uma rotina nossa para que seja chamada de volta (daí o nome call back). No caso deste nosso motor, a rotina informada em aojSetPulseCallback será chamada a cada pulso.

A terceira rotina do código acima é aojStartGameLoop(), que verifica se temos um callback e um canvas especificados e em caso positivo, utiliza o método setInterval (padrão do Javascript) para agendar a execução do método aojPulse a cada X milisegundos.

X, neste caso, equivale a 1000 dividido pela taxa de FPS informada, resultado no tempo de duração de cada pulso. Por exemplo, se informamos 20 FPS, X será 1000/20 = 50 milisegundos. Ou seja, ocorrerá um pulso a cada 50 milisegundos para manter a taxa de 20 por segundo.

Note que não é a nossa rotina de callback que é passada para o setInterval, mas sim outra rotina, interna ao motor, chamada aojPulse(). De dentro de aojPulse é que é chamada nossa callback.

Isso é feito assim porque o motor tem algumas ações a serem feitas a cada pulso, além de chamar nossa rotina própria. Estas ações consistem em calcular a taxa real de frames por segundo e mostrar essa informação na tela.

A contagem de pulsos é feita da seguinte maneira (conforme podemos ver no código de aojPulse):

  1. A cada pulso incrementamos um contador (_aojPulsesCount);
  2. A cada pulso obtemos o tempo atual e verificamos sua diferença para o tempo armazenado anteriormente (_aojLastMilisecond). Se a diferença é maior que 1000, ou seja, se 1 segundo se passou, então:
    1. Armazenamos o tempo atual em _aojLastMilisecond;
    2. Armazenamos a quantidade de pulsos contada pelo contador na variável _aojPulsesInLastSecond;
    3. Zeramos nosso contador, reiniciando a contagem para o próximo segundo.

Veja que após fazer isso, a rotina aojPulse chama a callback (que vai executar a lógica do jogo) e depois mostra o valor de _aojPulsesInLastSecond na tela com aojDrawText.

Usando o game loop

Para usar este game loop, criamos uma página HTML e inserimos os dois scripts que temos até agora:

<script src="aoj/aojcanvas.js"><!--mce:0--></script>
<script src="aoj/aojgame.js"><!--mce:1--></script>

E dentro da tag <body> declaramos a tag <canvas> com o ID que desejarmos (no exemplo abaixo usei “canvas” mesmo).

 

Se tiver dúvidas sobre isso, reveja a primeira parte do tutorial.

Na página HTML declaramos também uma seção de script para a lógica específica do nosso jogo. Veja o exemplo abaixo (que está disponível dentro do ZIP no final do post).

        <script type="text/javascript"><!--mce:2--></script>

Vamos analisar este código. Inicia com a declaração de um objeto chamado carro, que contém variáveis utilizadas para manter a posição e a direção de movimento de um sprite na tela.

Em seguida vem uma rotina chamada pulse(). Esta é nossa rotina de callback, que será chamada pelo motor. Dentro dela chamamos duas outras, update() e render(), que utilizamos para separar a lógica da parte de desenho na tela.

Dentro de update temos o código clássico para mover o objeto, fazendo ele rebater nas extremidades da tela. Cada coordenada do carro (x e y) são incrementadas com seu incremento correspondente (dx e dy). Quando atingem o limite da tela, dx (ou dy) tem o sinal invertido e o movimento naquela direção se inverte. Assim temos o efeito de rebater no canto da tela.

Dentro de render() limpamos a tela com aojFillRect() e desenhamos o sprite com aojDrawImage(). O sprite é desenhado na posição (x, y) armazenada no objeto carro.

O próximo código do script é a definição de uma função para ser chamada no evento onload da janela. Nela utilizamos aojNewImage() para carregar a imagem do carro. Lembre-se que esta rotina recebe uma outra como callback, que será chamada quando a imagem for completamente carregada. Nossa rotina de callback para esse caso é imagemCarregada() (a última do script).

Dentro de imagemCarregada() temos o código de inicialização, que consiste em informar o FPS desejado, a callback do pulso, o canvas a ser utilizado e finalmente dar o start no game loop.

Importante notar como essa inicialização não é feita no onload da janela, mas sim após a imagem carregar. Sempre precisamos primeiro carregar as imagens (e outros recursos) do jogo antes de colocar ele para rodar. Isso evita que o game loop começe, eventualmente chamando render() que tentaria desenhar o sprite que ainda não teria sido carregado.

Veja esse exemplo em execução aqui.

Entrada com teclado e mouse

Passamos para a parte de input. Os navegadores expõem alguns eventos em Javascript para obtermos acesso ao teclado e mouse (em breve teremos para controles de jogo também). Estes eventos são:

  • document.onkeydown, chamado sempre que uma tecla é pressionada. Neste evento vamos armazenar o código da tecla pressionada em uma variável;
  • document.onkeyup, chamado quando uma tecla é solta. Neste evento limparemos a variável que tinha o código desta tecla.
  • document.onmousedown, quando o botão esquerdo do mouse é pressionado. Neste evento armazenaremos essa informação em uma variável;
  • document.onmouseup, quando o botão do mouse é solto. Neste evento limparemos a variável acima;
  • document.onmousemove, ocorre quanto o mouse se move. Neste evento vamos atualizar duas variáveis que armazenarão a posição X e Y do mouse.

Veja que quando ocorrem os eventos acima, não executamos nenhuma lógica de jogo. Ao invés disso, armazenamos as mudanças de estado das teclas em variáveis. Estas variáveis serão depois acessadas em nosso update, para saber o estado de determinadas teclas.

Para armazenar o estado das teclas (solta ou pressionada), ao invés de um monte de variáveis soltas teremos um array com 6 posições. Armazenaremos as setas, um botão de ação e o botão do mouse.

Para saber em qual posição do array está cada informação, definimos algumas variáveis da seguinte forma:

var AOJ_KEY_UP = 0;
var AOJ_KEY_DOWN = 1;
var AOJ_KEY_LEFT = 2;
var AOJ_KEY_RIGHT = 3;
var AOJ_KEY_ACTION = 4;
var AOJ_MOUSE = 5;
var _aojKeys = new Array();

A posição do mouse é armazenada em um objeto chamado _aojMousePos que contém as propriedades x e y.

var _aojMousePos = {
    x:0,
    y:0
};

Para facilitar o uso, criamos rotinas para retornar o estado das teclas e posição do mouse. São elas:

function aojIsPressed(key)
{
    return _aojKeys[key];
}
 
function aojIsReleased(key)
{
    return !_aojKeys[key];
}
 
function aojMouseX() {
    return _aojMousePos.x;
}
 
function aojMouseY() {
    return _aojMousePos.y;
}

Estas rotinas apenas retornam o valor armazenado no array. Quem modifica este valor são as rotinas dos eventos listados acima. Sua implementação completa é simples e pode ser vista no arquivo aojinput.js que está no ZIP no final do post.

Utilizando o input

No ZIP temos também uma página HTML que utiliza o teclado para controlar o sprite do carro. O código da página é igual ao do exemplo acima, com diferença apenas no objeto do carro e na rotina update(). Ah, e claro, ele inclui o script que contem as rotinas de input:

<script src="aoj/aojinput.js"><!--mce:3--></script>

O objeto carro possui algumas propriedades a mais, para armazenar a rotação (direção em que o carro se moverá) e a velocidade de movimento.

A rotina update(), ao invés de mover o sprite rebatendo pela tela, agora utiliza a leitura da entrada para determinar para onde mover o carro.

function update() {
    if (aojIsPressed(AOJ_KEY_UP) || aojIsPressed(AOJ_MOUSE)) {
        // se tecla para cima pressionada, aumenta a velocidade do carro
        carro.speed += 0.5;
        if (carro.speed &gt; 10) // limita a velocidade em 10
            carro.speed = 10;
    } if (aojIsPressed(AOJ_KEY_DOWN)) {
        // se tecla para baixo pressionada, diminui a velocidade do carro
        carro.speed -= 0.5;
        if (carro.speed &lt; -5) // limita em -5 - com velocidade negativa o carro vai dar marcha ré
            carro.speed = -5;
    } else {
        // se nem acima nem abaixo estão pressionadas, diminui um pouco a velocidade.
        carro.speed -= 0.1;
        if (carro.speed &lt; 0)
            carro.speed = 0
    }
    if (aojIsPressed(AOJ_KEY_LEFT)) {
        // Se pressionada para esquerda, muda a rotação de acordo
        carro.rotation -= Math.abs(carro.speed) / 100;
        if (carro.rotation &lt; 0)
            // Se o angulo diminuir de zero, recomeça em uma volta inteira (2 * PI radianos)
            carro.rotation = Math.PI * 2;
    }
    if (aojIsPressed(AOJ_KEY_RIGHT)) {
        carro.rotation += Math.abs(carro.speed) / 100;
        if (carro.rotation &gt; Math.PI * 2)
            carro.rotation = 0;
    }
    // calcula a nova posição aplicando rotação ao ponto
    carro.x += Math.cos(carro.rotation) * carro.speed;
    carro.y += Math.sin(carro.rotation) * carro.speed;
}

O código é padrão para movimento de veículos, com velocidade e rotação. Se você tiver dificuldades para entender, dê uma olhada nos nosso posts sobre rotação e aceleração.

Conclusão

Neste artigo vimos um motor de pulso muito básico feito em Javascript. Atenção para o fato de não termos um loop de fato, mas sim um agendamento utilizado o recurso de timer do navegador (setInterval). Um loop real (for, while), infinito como teria que ser, não é aceito pelo navegador. Sempre que encontra um loop destes, que tem o potencial de trancar o processamento da página, o navegador aborta o script. É uma questão de segurança, para impedir o uso deste recurso por páginas maliciosas.

No entanto o setInterval não é a melhor forma de fazer isso. Usei-o aqui por ser mais simples, porém ele apresenta um problema em máquina mais lentas (ou jogos mais pesados). Entenda porque: o setInterval agenda chamadas regulares à rotina aojPulse(), a cada X milisegundos. Até aí tudo bem, é isso que querermos para que nosso jogo pulse a cada X milisegundos.

Mas o que ocorre se a máquina for lenta a ponto da rotina aojPulse demorar mais de X milisegundos para executar? Uma nova chamada será feita a ela e por alguns milisegundos teremos duas aojPulse rodando – o que vai deixar a coisa ainda mais lenta. Com isso, dalí a X milisegundos, outra vez será chamada a rotina e isso vai acabar causando um acúmulo de rotinas em paralelo que vão degenerar totalmente o desempenho do jogo. Sem falar que as cópias da rotina rodando em paralelo estarão acessando as mesmas variáveis globais, e isso certamente vai causar todo tipo de comportamento estranho na lógica do jogo.

Na próxima parte veremos como fazer isso de uma forma diferente. Veremos também como melhorar a aparência do movimento mostrado na tela. Faremos isso deixando o render() independente do update() e utilizando interpolação das posições. Isso nos permitirá roda a lógica do jogo a taxa baixas enquanto a atualização da animação roda no máximo que a máquina permitir.

Até lá.

Autor: Luiz Nörnberg Ver todos os posts de
Sou Bacharel em Ciência da Computação pela Universidade Católica de Pelotas (UCPel), onde também atuei como professor. Desde a época da faculdade (mais de quinze anos atrás) a paixão por jogos tem sido importante no meu direcionamento profissional. Sou sócio-fundador do Izyplay Game Studio, onde exerço o cargo de Diretor de Tecnologia. Sempre tive grande foco em desenvolvimento em Java, embora tenha migrando para a tecnologia Adobe AIR em função de sua portabilidade. Ah, e é claro, dou meus palpites no game design.

5 Comentários em "Tutorial HTML 5 – Parte 2/3"

  1. Raul 05/03/2012 at 10:47 - Reply

    Estou curtindo as matérias sobre HTML5. Continuem assim. Parabéns.

  2. Diego 13/12/2012 at 16:44 - Reply

    Cara, muito show esse tutorial, mas estou com uma dúvida aqui, no meu caso, quando uso o FillRect ele está ficando por cima da imagem, existe uma maneira de definir a posição dos elementos? (eu estou executando o FillRect antes do DrawImage)

    Valeo!! Abraços!!

  3. deivison 24/09/2013 at 13:22 - Reply

    Ola! Será que você poderia explicar como criar um controle para toque virtual ? Desde ja valeu!

  4. bruno 26/08/2014 at 15:53 - Reply

    Ola Luiz, gostaria de saber se vc sabeira me indicar algo para estudar, de como fazer uma imagem, que seria um pipa, rotacionar em javascript?

    • Luiz Nörnberg 28/08/2014 at 17:04 - Reply

      Olá Bruno.

      Você deu uma olhada a parte 1 do tutorial (http://abrindoojogo.com.br/tutorial-html-5-parte-13)? Lá eu mostro mostro exemplos básicos, como escala de imagem, sendo que a rotação é parecida: utilize o método “rotate” do canvas antes de desenhar a imagem nele. Tem um arquivo de script com todas funções prontas para uso no post da parte 1.

      Elemar Dev também possui um mbom tutorial de canvas e neste post ele mostra a rotação:(http://wp.me/pZuNg-7m).

Deixar um Comentário