|
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 na 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);
}
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.
-
09/05/2010 12:52:12 | Vinícius Godoy de Mendonça - GameSteps

Sim, o propósito da interface é esse: separar a lógica da tela do game loop. Você pode fazer cada fase ser um game step, empilhar game steps e controlar o fluxo de telas através disso.
Não entendi o que você quis dizer com simular mais de um view. Pode explicar melhor?
-
09/05/2010 17:18:44 |201.12.130.xxx| Anônimo

Alguns programas usam para exibir senas em 3D em 4 janelas... tipo o 3D Game studio.
1 - Visão da "camera" parte de cima.
2 - Visão da "camera" parte lateral.
3 - ...
4 - ...Espero que tenha entendido.
-
09/05/2010 20:21:04 | Vinícius Godoy de Mendonça - Várias janelas

Sim, isso é possível com o OpenGL. Você tem várias alternativas:
a) Criar vários viewports: Um viewport pode ser menor do que as janela toda. E em cada viewport, você faria um desenho diferente;
b) Lembra que eu falei que é importante que o método de pintura não altere o estado do game? Isso é importante pq vc poderá chama-lo várias vezes, e com certeza pintará a mesma cena. E isso te permite chamar uma vez para cada área que quiser, mudando as matrizes de translação, rotação e câmera, fazendo assim as várias janelas.
c) Você pode abrir várias janelas com a OpenGL e cada uma também terá sua área de pintura.
-
10/05/2010 07:45:16 |200.151.92.xxx| Anônimo

Oi.
Sei que usando outras API(Win32, Glut ... ) com o OpenGL pode-se criar outras "view".
Minha pergunta foi especificamente para SDL/OpenGL.
É possível criar "view" simulando por exemplo quatro janelas("view"
em uma única janela do SDL/OpenGL?Abraços.
gokernel
gokernel@hotmail.com
-
10/05/2010 08:05:06 | Bruno Crivelari Sanches

gokernel,
A SDL que eu saiba não permite multiplas janelas, lembro que tinha um projeto para implementar isso mas não sei como anda.
Nesse caso você vai ter que usar a primeira idéia do Vinícius que é criar multiplos viewports, isso até onde me lembro é feito inteiramente com comandos opengl.
-
10/05/2010 09:00:20 |200.151.92.xxx| gokernel

Oi Bruno.
Sei que com SDL/OpenGL não é possível criar mais de uma janela, por isso usei as palavras "simular" "view".
Se você lembrar sobre o projeto por gentileza informe o link.
Abraços.
gokernel
gokernel@hotmail.com
-
10/05/2010 09:28:04 | Bruno Crivelari Sanches

Temos que separar os conceitos então. Multiplas janelas é diferente de multiplas view.
Com o viewport você pode dividir uma janela existente em sub-regiões e desenhar em cada uma delas conforme a necessidade. Assim você pode ter um resultado similar ao que existe no Max ou outros editores 3d.
Se for criar multiplas janelas, janelas do sistema de janelas, dai a SDL vai lhe deixar na mão.
Pelo o que andei vendo, apenas a versão 1.3 vai suportar isso, mas esta em desenvolvimento ainda, mas existe um preview: http://www.libsdl.org/tmp/SDL-1.3.zip
-
10/05/2010 10:11:54 |200.151.92.xxx| Anônimo

Sim são multiplas "view" que não tenho ideia de como fazer pelo menos 2.
T++.
gokernel
gokernel@hotmail.com
-
09/02/2012 16:07:50 |189.76.95.xxx| paulo - error compilaçao

Ola eu estava tentando compilar o projeto mas apontou varios erros aqui, ser que alguem pode ajudar? vlw =]











Oi, olhei o codigo do GameSteps e o que me chamou mais atenção foi:
------------------------------------------------------------
"
A interface GameSteps pode ser usada para fazer telas além da principal.
"
------------------------------------------------------------
PERGUNTA:
Usando SDL/OpenGL, é possível "simular" mais de um "view" de uma sena em OpenGL? Como?
gokernel
gokernel@hotmail.com