Entendendo o loop principal

17 12 2007

No código anterior, você deve ter notado que o método main estava dentro de um loop. Hoje, vamos entender qual é a importância daquele loop, quais as consequências dele e como usa-lo para produzir animações.

Todo jogo roda num loop, formado pelos seguintes passos:

  1. Processar os eventos externos (entradas do usuário, dados da rede);
  2. Executar a lógica do jogo;
  3. Desenhar a tela.

Filmes no computador

Uma animação, no cinema, é produzida por uma série de quadros estáticos, mostrados um-a-um. O olho, incapaz de captar a troca de quadros, enxerga como se tudo estivesse em movimento. Nos computadores, todas as animações são produzidas de maneira similar.

Entretanto, temos apenas a presença de dois quadros. Isso porque o computador é rápido o suficiente para pintar um quadro enquanto enxergamos outro. Quando o novo quadro está pronto, ele rapidamente troca os dois de lugar: o quadro que estava sendo pintado passa a ser visto e o quadro que estava sendo visto é apagado e repintado. Essa técnica é conhecida como double buffering. É o comando SDL_GL_SwapBuffers() quem efetivamente troca as duas imagens de lugar. Uma aplicação sem double buffering sofre um problema chamado flickering. O usuário percebe a tela sendo apagada e redenhada e, para ele, a tela parece estar piscando.

Essa troca das duas imagens também não pode ser feita de qualquer jeito. No início, observou-se que se a imagem fosse apagada e a nova repintada a qualquer momento corria-se o risco do monitor exibir algumas vezes a imagem no meio da pintura. Ou seja, parte da imagem exibiria o conteúdo do buffer novo e parte do buffer antigo. Na prática, o usuário percebia pequenas manchas na tela, um efeito conhecido como tearing. O problema foi resolvido da seguinte forma: desenha-se o buffer em segundo plano na memória de vídeo e, quando pronto, é dado um comando ao hardware do vídeo, que só irá trocar as imagens no início do próximo ciclo de desenho. Essa técnica é conhecida como page flipping (virar a página). Veja no desenho abaixo:

Double buffering e page flipping

Outra diferença do computador para o cinema é que o tempo entre os quadros não é constante. Na verdade, ele varia de acordo com o que é processado entre os quadros, com o equipamento utilizado para desenha-lo e com a complexidade do desenho sendo exibido em si. Por isso, entre um desenho e outro, devemos levar em consideração essas variações de tempo (para jogos e animações simples, é possível criar um algoritmo que mantém a taxa de atualizações entre quadros constante. Há uma extensa discussão sobre isso no livro Killer Game Programming in Java, disponível online no site do prof. doutor Andrew Davison.)

O tempo entre dois quadros

Para um jogo suave e convincente, é fundamental sabermos o tempo entre dois quadros. O OpenGL não fornece nenhuma função que nos permita saber essa informação, porém, mais uma vez, a SDL vem em nosso resgate com a função SDL_GetTicks(). Essa função retorna o número de milisegundos transcorridos desde o início da aplicação.

Dessa forma, podemos calcular facilmente o tempo entre dois quadros, observe:

//lastTicks guarda quando foi pintado o último quadro
lastTicks = SDL_GetTicks();
while (true)
{
   Uint32 thisTicks = SDL_GetTicks();
   //Calcula em ticks a diferença de tempo
   //entre o quadro atual e o anterior
   ticks = thisTicks - lastTicks;
   //Atualizamos lastTicks
   lastTicks = thisTicks;

   //Processa o jogo
   processEvents();
   processLogics();
   draw();
}

Limpando a Tela

Antes de começar a desenhar, é necessário limpar a tela. O OpenGL não faz essa operação por padrão, você pode até mesmo “reproveitar” uma tela já pintada e desenhar sobre ela apenas as diferenças. Isso, no entanto, é extremamente trabalhoso.

Primeiramente, temos que definir qual cor será aplicada na tela quando ela for apagada. Fazemos isso usando o comando:

void glColor( GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha )

Que recebe como parâmetro a quantidade de vermelho, verde e azul, mais o componente alfa (por enquanto, deixe esse último componente em 0, ele será explicado só bem mais tarde, ao falarmos de blending). Os valores de verde, vermelho e azul podem variar entre 0, para nenhuma cor, ou 1 para o máximo de cor.

Em seguida, temos que usar o comando

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

E a tela está limpa. Os parâmetros desse comando também serão tratados em detalhes mais tarde. O que você precisa saber agora, é que ele limpa os buffers de cores e de profundidade, que é onde a imagem está.

Movendo objetos pela tela

Como os objetos se movimentam pela tela? Para dar a sensação de que um objeto se move, basta desenharmos ele um pouco deslocado a cada iteração. Voltando um pouquinho na física, lembraremos que a velocidade é dada pela fórmula:

velocidade = distância / tempo

 

Se você não gosta de física, basta lembrar que o velocímetro de seu carro obedece essa fórmula te indicando a velocidade através dos quilômetros (distância) por hora (tempo) percorridos. Em nosso caso, a velocidade será medida por duas unidades comuns: pixels por milisegundos, quando quisermos faze-la andar, ou graus por milisegundos, se estivermos girando. Assim, vamos supor que queiramos que o triângulo desenhado no programa anterior rode em 360º em 2 segundos. Essa é uma velocidade efetiva de 180º por segundo ou, 0,180 graus / milisegundo.

A distância (em graus) que deveríamos mover o triângulo entre dois quadros (com tempo de ticks milisegundos entre si) então é:

velocidade = distância / tempo
distancia = velocidade * tempo
distanciaEmGrausParaGirar = 0,180º/ms * ticks

O programa abaixo, faz essa rotação. Apenas os métodos processLogics(), draw() e o main() foram alterados, preste especial atenção neles. Além disso, duas três novas variáveis globais foram criadas. Decidi manter o código todo para você poder dar copy&paste em seu IDE:

#include "SDL.h"
#include "GL/gl.h"

#include <stdexcept>
#include <iostream>

SDL_Surface* window;

//Controle do tempo
Uint32 lastTicks;
Uint32 ticks;

//Total graus para rodar
float degreesToRotate;

int createFlags(bool fullscreen)
{

    //Iniciamos com a OpenGL e paleta de hardware
    int flags = SDL_OPENGL | SDL_HWPALETTE;

    if (fullscreen)
        flags |= SDL_FULLSCREEN;

    const SDL_VideoInfo* info = SDL_GetVideoInfo();

    //Criarmos uma superfície de hardware,
    //se este estiver disponível
    if (info->hw_available)
        flags |= SDL_HWSURFACE;
    else
        flags |= SDL_SWSURFACE;

    //Aceleração por hardware?
    if(info -> blit_hw)
        flags |= SDL_HWACCEL;

    return flags;
}

int setupOpenGL(int bpp, bool fullscreen)
{
    //Atributos do opengl
    SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 );
    SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, bpp);
    SDL_GL_SetAttribute( SDL_GL_ACCUM_RED_SIZE, 0);
    SDL_GL_SetAttribute( SDL_GL_ACCUM_GREEN_SIZE, 0);
    SDL_GL_SetAttribute( SDL_GL_ACCUM_BLUE_SIZE, 0);
    SDL_GL_SetAttribute( SDL_GL_ACCUM_ALPHA_SIZE, 0);

    return createFlags(fullscreen);
}

void setup(int width, int height, int bpp, bool fullscreen)
{
    //Inicializamos o subsistema de video.
    if (SDL_Init(SDL_INIT_VIDEO) < 0)
        throw std::runtime_error(SDL_GetError());

    //Tentamos criar a janela
    window = SDL_SetVideoMode(width, height, bpp,
    setupOpenGL(bpp, fullscreen));

    //Sem sucesso? Lançamos uma exceção com o erro.
    if (window == NULL)
        throw std::runtime_error(SDL_GetError());

    glViewport(0,0, width, height);

    //Configuramos a função de des-inicialização
    atexit (SDL_Quit);
}

/** Espera o usuário pressionar o x da janela. */
void processEvents()
{
    SDL_Event event;

    while (SDL_PollEvent(&event) != 0)
    {
        switch (event.type)
        {
            case SDL_QUIT:
                exit(0); //Fechamos a apliação
                break;
        }
    }
}

void draw()
{
    glPushMatrix();
        //Limpa a tela
        glClearColor(0.0, 0.0, 0.0, 0.0);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        //Gira o triângulo
        glRotatef(degreesToRotate, 0,0,1);

        //Desenha o triângulo
        glBegin(GL_TRIANGLES);
            glColor3f(1,0,0);
            glVertex2f(0.0f,0.5f);
            glColor3f(0,1,0);
            glVertex2f(-0.5f, -0.5f);
            glColor3f(0,0,1);
            glVertex2f(0.5f, -0.5f);
        glEnd();
    glPopMatrix();
}

void processLogics()
{
    //Distância para girar (em graus) =
    //velocidade (0.180f) * tempo (ticks)
    float distance = 0.180f * ticks;
    degreesToRotate += distance;
}

int main(int argc,char* argv[])
{
    try
    {
        setup(640, 480, 8, false); //Faz a mágica acontecer
        degreesToRotate = 0;
        lastTicks = SDL_GetTicks();

        //Loop principal do jogo
        while (true)
        {
            Uint32 thisTicks = SDL_GetTicks();
            //Calcula em ticks a diferença de tempo entre
            //o quadro atual e o anterior
            ticks = thisTicks - lastTicks;

            //Atualizamos lastTicks
            lastTicks = thisTicks;

            processEvents();
            processLogics();
            draw();

            //Trocamos a superfície de desenho
            //pela exibida na tela
            SDL_GL_SwapBuffers();
        }
    }
    catch (std::exception &e)
    {
        std::cout << "Error: " << e.what();
        exit(1);
    }
}

Revisando

Você aprendeu que:

  • Que animações feitas pelo computador tem apenas dois quadros: o que está sendo exibido e o que está sendo pintado, e esses quadros são guardados em buffers na memória de vídeo;
  • Como limpar a tela e definir a cor de fundo com o comando glClearColor e glClear;
  • Que o comando GL_SwapBuffers tem duas funções básicas: Enviar os comandos do OpenGL efetivamente para o hardware de vídeo e trocar o buffer de desenho com o que está sendo exibido na tela;
  • Que o loop de um jogo se divide entre processar eventos, processar lógica e pintar a tela;
  • Que o tempo entre duas iterações do loop (portanto, entre dois quadros) não é constante;
  • Como é possível calcular o tempo entre dois quadros usando a função SDL_GetTicks;
  • E que devemos levar em consideração esse tempo na hora de fazer nossos desenhos.