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.
Eaeew Vini! =D
Cara… Seus artigos são demais, muito bom mesmo!
E esse artigo não fugiu da regra! Gostei muito, apesar de não usar SDL, isso vai ser muito útil para mim, por que estou querendo aprender um pouco de OpenGL, mas to usando AllegroGL (aguardando a Allegro 1.3 xD), acredito que não seja tão diferente do SDL com o OpenGL! =D
Muito obrigado, e continue com esses artigos massa, sempre que puder eu comento heeein! xD
Flww
Com certeza não é tão diferente assim. Pelo que vi, até a sintaxe do Allegro é muito similar a da SDL.
Além do mais, eu não quero falar da SDL ou OpenGL de forma isolada, mas de conceitos. No final, eles é que são importantes, a tecnologia em si é o de menos.
Obrigado por ler e comentar!
Acho que você cometeu um typo, não seria flickering em vez de flicking?
T+
Tem razão, já está corrigido. Eu tinha visto em sites as duas grafias e fiquei na dúvida.
Mas… agora que você falou, flickering faz mais sentido mesmo, pois vem de “flicker”.
Espetacular!
Sempre quis aprender a fazer jogos, li muito material ‘perdido’ sobre um ou outro assunto, mas sempre faltava algo para construir um.
Uma das minhas maiores dificuldades era sobre o loop principal e o timming do jogo.
Você conseguiu me explicar isso usando, sei lá, 20 parágrafos, incluindo o exemplo!
Meus sinceros agradecimentos!
Leonardo
Legal é muito bom receber feedback!
Essa não é a única maneira de implementar o loop principal do jogo. Para jogos simples (jogos de plataforma, jogos casuais) as vezes é melhor implementar um loop que mantenha a taxa de updates por segundo constante e numa taxa definida, enquanto a taxa de frames por segundo varia.
Assim, já que a diferença de tempo entre os updates é constante, você pode excluí-la dos cálculos. Essa técnica é citada no livro do dr. Andrew Davidson, que citei. Se quiser ler o link é esse:
http://fivedots.coe.psu.ac.th/~ad/jg/ch1/index.html
Ele também faz considerações sobre timers. Os exemplos são em Java, mas valem para qualquer linguagem (eu mesmo fiz uma implementação desse algoritmo para o C++).