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:
- Processar os eventos externos (entradas do usuário, dados da rede);
- Executar a lógica do jogo;
- 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:

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.