Até agora, vimos como inicializar a janela da nossa aplicação, como funciona o loop principal, como capturar eventos. Entretanto, embora didáticos, os exemplos anteriores são pouco reutilizáveis. Em primeiro lugar, não existe uma separação clara entre a lógica da janela e a lógica do “jogo” (que no nosso caso resume-se a mover o triângulo. Um jogo não muito divertido, mas talvez o triângulo colorido agrade várias crianças de 3 anos).
Neste artigo, damos uma pausa no OpenGL e na SDL para voltar ao bom e velho C++ e organizar tudo em classes reutilizáveis.
A estrutura até agora
Se observarmos bem o código dos artigos anteriores, poderemos notar o seguinte:
- Existe uma janela principal. Essa janela possui alguns atributos como um título, altura, largura, bits por pixel, etc. O loop principal está associado a lógica da janela.
- Todos os jogos devem ser capazes de: preparar seu ambiente, processar eventos, processar a lógica, desenhar e limpar o seu ambiente. Esses são passos de qualquer entidade que queira fazer parte do loop principal deve implementar;
- Nosso triângulo rodando é um jogo, ou pelo menos, segue sua estrutura.
Isso nos leva até a seguinte estrutura:

A interface GameSteps
A interface GameSteps define o contrato básico que qualquer classe que queira ser submetida ao loop principal deve obedecer. Como já constatamos, a classe que roda ali deve ser capaz de processar os eventos (processEvents), sua lógica (processLogics) e se desenhar (draw). Também foram adicionados métodos para inicialização (setup) e finalização (teardown), que serão executados uma única vez, antes e depois do loop. Por fim, o método ended() deve retornar verdadeiro, sempre que a classe quiser que o loop termine.
Em C++, interfaces são modeladas através de classes onde todos os seus métodos são virtuais puros (sem implementação) e seu destrutor é virtual e vazio. Essas classes também são conhecidas como Abstract Base Classes ou, simplesmente, ABCs.
No nosso caso, nossa ABC GameSteps seria:
union SDL_Event;
class GameSteps
{
public:
virtual void setup() = 0;
virtual void processEvents(const SDL_Event& event) = 0;
virtual void processLogics() = 0;
virtual void draw() const = 0;
virtual bool ended() = 0;
virtual void teardown() = 0;
virtual ~GameSteps() {};
};
Um detalhe importante dessa implementação: note que definimos o método draw() como const. Isso significa, que o método draw() não deve alterar o estado do jogo. Assim, garantimos que o processamento da lógica está restrito aos métodos “process”. Embora isso pareça purista, essa forma ajuda muito a avaliar o desempenho de sua aplicação usando um profiler.
A classe GameWindow
A classe GameWindow é praticamente idêntica ao nosso antigo main, mudanças foram feitas apenas para delegar parte das tarefas ao GameSteps, seja ele qual for.
Primeiramente, alteramos o método processEvents, para apenas capturar os eventos e delegar o processamento propriamente dito para o GameSteps:
void GameWindow::processEvents(GameSteps* game)
{
SDL_Event event;
while (SDL_PollEvent(&event))
game->processEvents(event);
}
Note a simplicidade do método, já que a janela não é mais responsável por sequer tratar o evento de SDL_Quit. Em segundo lugar, alteramos o loop principal para também delegar atividades ao GameSteps.
void GameWindow::show(GameSteps* game)
{
lastTicks = SDL_GetTicks();
try
{
game->setup();
while (!game->ended())
{
Uint32 thisTicks = SDL_GetTicks();
ticks = thisTicks - lastTicks;
lastTicks = thisTicks;
processEvents(game);
game->processLogics();
game->draw();
SDL_GL_SwapBuffers();
}
}
catch (std::exception &e)
{
std::cout << "Problems while running game logic: ";
std::cout << std::endl << e.what();
exit(2);
}
game->teardown();
delete game;
}
Perceba que o objeto do game é deletado ao final do loop, foi por isso que no diagrama UML definimos a relação como composição e não como uma relação de dependência simples.Em terceiro lugar, definimos a classe GameWindow como um Singleton. Para implementarmos o Singleton, colocamos o construtor, o construtor de cópia e o operador de atribuição na sessão privada da classe. Isso impede que novas instâncias sejam criadas externamente à classe. Depois, criamos uma única variável estática que define um ponteiro para nossa classe (chamada nesse caso de myInstance) e a inicializamos com NULL. Finalmente, criamos o método estático getInstance(), que cria uma única instância da classe e a retorna.Veja os trechos de código relevantes:
//GameWindow.h
class GameWindow
{
public:
//Obtém a única instância da classe
static GameWindow& getInstance();
//RESTO DA SESSÃO PUBLICA OMITIDO
private:
static GameWindow* myInstance;
//Desabilita criação, cópia e assinalação
GameWindow(const GameWindow& other);
GameWindow& operator=(const GameWindow& other);
GameWindow() : window(NULL), ticks(0) {}
//RESTO DA SESSÃO PRIVADA OMITIDO
};
//GameWindow.cpp
//Ponteiro para a instância única
GameWindow* GameWindow::myInstance = NULL;
GameWindow& GameWindow::getInstance()
{
//Cria a instância, se ainda não foi criada
if (myInstance == NULL)
myInstance = new GameWindow();
//Retorna a única instância
return *myInstance;
}
Alguns atributos dessa classe certamente serão usados em vários pontos do programa, tais como o tempo entre duas iterações do loop (fornecido pelo método getTicks() ou getSecs()), a altura e a largura da janela ou mesmo a razão entre os dois (fornecida por getRatio()). O fato da classe ser Singleton nos ajuda muito nesse ponto. Afinal, ela não precisa ser passada como parâmetro, simplificando as interfaces, e nem dependências dela precisam ser incluídas em qualquer arquivo .h, o que poupa tempo de compilação. Acessar qualquer atributo da classe então é uma questão usar o método estático getInstance(), como no exemplo:
cout << "Largura: " << GameWindow::getInstance().getWidth();
Como a idéia é poupar trabalho, e também por preguiça, também criamos o seguinte define:
#define GAMEWINDOW GameWindow::getInstance()
Assim, podemos simplificar o código acima para:
cout << "Largura: " << GAMEWINDOW.getWidth();
A classe RotatingTriangle
Finalmente, podemos implementar a nossa aplicação, que faz o triângulo girar. Embora tudo tenha parecido um pouco complicado até aqui, pense que futuras aplicações precisarão apenas realizar esse passo. Toda complicação da janela já está pronta e a ABC GameSteps já até dá uma pequena dica de como implementar a estrutura principal do nosso jogo.
A classe RotatingTriangle implementa a interface GameSteps e contém a lógica que faz o triângulo girar. Ela define apenas dois atributos privados:
- degreesToRotate: Usado no mesmo contexto anterior, para indicar quantos graus o triângulo deve ser rotacionado;
- exit: Que indica se a aplicação terminou ou não.
O .h dela é praticamente uma cópia do GameSteps.h, apenas com esses dois atributos declarados na sessão private e sem nenhum método definido como virtual puro (ou seja nenhum método =0). A implementação contém o código específico do desenho do triângulo e do processamento de teclas e finalização da aplicação:
#include "RotatingTriangle.h"
#include "SDL.h"
#include "GL/GL.h"
#include "GameWindow.h"
RotatingTriangle::RotatingTriangle()
: exit(false), degreesToRotate(0) {}
void RotatingTriangle::setup() {}
void RotatingTriangle::processEvents(const SDL_Event& event)
{
switch (event.type)
{
case SDL_QUIT:
exit = true;
break;
}
}
void RotatingTriangle::processLogics()
{
//Distância para girar (em graus) =
//velocidade (0.180f) * tempo (ticks)
float distance = 0.180f * GAMEWINDOW.getTicks();
//Lemos o estado das teclas
Uint8* keys = SDL_GetKeyState(NULL);
//Está com a seta esquerda pressionada?
if (keys[SDLK_LEFT])
degreesToRotate += distance;
//Está com a seta direita pressionada?
else if (keys[SDLK_RIGHT])
degreesToRotate -= distance;
}
void RotatingTriangle::draw() const
{
glPushMatrix();
//Limpa a tela
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_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();
}
bool RotatingTriangle::ended()
{
return exit;
}
void RotatingTriangle::teardown() {}
Note o uso da GameWindow no método processLogics(). Repare também que o código ficou muito mais coeso. A classe restringe-se apenas ao desenho do triângulo e por isso ficou muito mais fácil de entender. Da mesma forma, a classe GameWindow restringe-se ao que se espera da janela principal.Outro detalhe interessante é a forma como a classe trata eventos. Note que agora ela recebe os eventos prontos, já capturados, precisando apenas identifica-los e trata-los. Houve a efetiva separação entre as etapas de captura de eventos e o seu processamento.
Usando a solução
Com as classes prontas, nosso novo main se resume a:
#include "GameWindow.h"
#include "RotatingTriangle.h"
int main(int argc,char* argv[])
{
GAMEWINDOW.setup("Rotating triangle", 800, 600);
GAMEWINDOW.show(new RotatingTriangle);
}
Simples não? A partir de hoje, precisamos nos preocupar apenas com a classe que implementa GameSteps. Por isso, não publicarei mais códigos relacionados a GameWindow, a menos que alguma modificação mais profunda seja feita.
Outras modificações interessantes
Você pode encontrar o código fonte completo neste link. Nesse fonte, também adicionei uma forma de alterar o título da janela. Isso é feito através da função SDL_WM_SetCaption encapsulada no método setCaption.
Também defini a taxa de repetição de teclas para zero, durante a fase de setup. Isso evita que eventos sejam disparados em falso, caso o usuário mantenha qualquer tecla pressionada. Podemos definir a quanto tempo levará para a repetição a ocorrer e o intervalo entre repetições através da função SDL_EnableKeyRepeat.
Finalmente, também durante o setup, ocultei o cursor do mouse através da função SDL_ShowCursor.
Como tarefa para você, fica baixar o código fonte, tentar compila-lo e estudar a solução completa. Não se assuste, não é assim tão diferente do que vimos até agora. Preste especial atenção no método setup, onde esses novos detalhes foram adicionados. Certifique-se também de ter entendido os conceitos.
Veja também
Um jogo não está restrito à uma só tela. A interface GameSteps pode ser usada para fazer telas além da principal. Nesse caso, leia o artigo Pilha de Telas, do Vertex Buffer, para ver uma forma muito interessante de gerenciar várias telas.
Cara eu também tenho uma classe chamada Frame para fazer essas coisas, mas a sua é muito mais pura ! C++, POO e UML ao pé da letra… isso é bem complicado.
Mas prometo que vou estudar sua classe, mas só depois do Natal !!!
Opa… então, eu manjo de classes e talz, mas nao entendi bulhufas desse código maluco… Essas classes abstratas estranhas ae… E esse teardown??? q n tem em lugar nenhum uma definição para ele, e mesmo assim ele eh chamado em game->teardown()… o compilador eh adivinho, magico ou algo do tipo?
Nossa confundi mto aqui… hehehe
Essa é a mágica do polimorfismo, meu caro Bruno.
A classe abstrata apenas diz: “Eu preciso ter um método chamado tearDown”. Mas ela não tem implementação, porque, para o GameLoop não interessa “como” foi implementado. Só interessa saber que o método “está lá”.
A classe abstrata é uma forma do GameWindow dizer o que ele espera que esteja lá.
Pois bem, criamos um filho dessa classe abstrata. Essa classe contém as implementações desses métodos, certo? Então, passamos um ponteiro desse filho para a classe do GameWindow (que está esperando alguém do tipo GameSteps, e seu filho de GameSteps é um caso).
Quanto a classe GameWindow chamar o método, tearDown do GameStep que está com ela, que tearDown será chamado? O do seu filho, que desenha o triângulo.
Se você ainda está em dúvida, procure se informar desse recurso muito poderoso da OO, chamado Polimorfismo. Existe um artigo no wiki sobre ele (que por sinal eu que escrevi) que cobre mais detalhes. Você também pode procurar num bom livro de C++ ou de OO.
PS: Esse é realmente um dos códigos mais avançados postados nesse blog até agora.
[...] outro caso para o uso da mutable é na implementação do design Organizando a janela em classes, de nosso amigo Vinícius, onde temos à seguinte declaração do método [...]
Excelente artigo!
Só tenho uma ressalva a fazer, baixei o código e compilei no linux usando g++ e obtive erro no arquivo main.cpp na linha:
GAMEWINDOW.setup("Rotating triangle", 800, 600);Acrescentei as informações de bpp e fullscreen e funcionou perfeito:
GAMEWINDOW.setup("Rotating triangle", 800, 600, 24, false);Abraços
Estranho, porque o GameWindow tem valores default para esses campos. Por que será que seu g++ os ignorou?
Os defaults são 32 e false, pode verificar no .h.
Será que ele exige a declaração dos defaults no .cpp também?
Obrigado por citar esse detalhe!
Olá de novo!
Voltei aqui pra comentar sobre o bug da janela no meu Linux. O erro que deu foi:
terminate called after throwing an instance of ’std::runtime_error’
what(): Unable to create OpenGL window!
Reason:Couldn’t find matching GLX visual
Aborted
Depois de algumas “googladas” descobri que na verdade esse erro tem a ver com o suporte do meu GLX, que por sinal, não funciona com 32bits, por isso que quando passei explicitamente no contrutor como 24 funcionou.
=)
O seu código roda perfeito pra mim com 24 bits.
Desculpa achar que era alguma incompatibilidade com o Linux, se quiser apague os meus comentários pra não confundir quem vier a ler amanhã.
Abraços!!
Que nada, acho que fica bem registrado aqui. Aí mais gente conhece essa mensagem de erro!
Mas, qualquer código que apresente problema, entre em contato.
Olá,
estou tentando rodar seu código do cubo, mas aparece o mesmo erro acima:
terminate called after throwing an instance of ’std::runtime_error’
what(): Unable to create OpenGL window!
Reason:Couldn’t find matching GLX visual
Já tentei trocar o bpp, mas continua aparecendo o erro.
Estou compilando em Linux, distro Debian Lenny, com o CodeBlocks.
Você tem alguma sugestão?
Se preferir, meu msn é o mesmo do mail que eu te passei, porém com @hotmail
E parabéns pelo blog, está me ajudando muito.
Abraços
Na verdade, não. =(
Conheço muito pouco de Linux.
Claramente, o erro está relacionado ao seu ambiente de janelas. Ou ele não está conseguindo iniciar o OpenGL numa janela, ou ele não está suportando as configurações (resolução, por exemplo), passadas.
O link para o exemplo.zip está quebrado. Tem como você hospedar o arquivo novamente? Me interessei bastante neste código, tentei recompor ele juntando esses fragmentos mostrados aqui, mas infelizmente não consegui direito.
Obrigado!