PUCRS/FACIN
Curso
de Especialização
em Desenvolvimento de Jogos
Digitais
Computação
Gráfica 2D
Profs.
Márcio Sarroglia Pinho
Isabel Harb Manssour
Carga de Imagens e Animação
O objetivo desta aula é criar um programa que carregue uma conjunto de imagens e gera uma animaçao contradada pelo usuário.
Primeiramente baixe do Moodle o projeto para a aula de hoje e abra no Visual Studio.
Versão Local do Framework
Mantenha no projeto os fontes listados abaixo, removendo os demais, se houver.
CImage.cpp
GameBasico.cpp
TextureManager.cpp
Inicializando a SDL
Abra o arquivo GameBasico-CImage.cpp e observa a função main. Nela a SDL e inicializada e ao final o usuário deve digitar um número e teclar ENTER.
int main( int argc, char* args[] ) {
//Start SDL
SDL_Init( SDL_INIT_EVERYTHING );
// Define o titulo da janela
SDL_WM_SetCaption("Jogo Basico", "Jogo Basico - Minimizado");
// Request double-buffered OpenGL
SDL_GL_SetAttribute (SDL_GL_DOUBLEBUFFER, 1);
// create a new window on screen
int flags = SDL_OPENGL | SDL_RESIZABLE ;
SDL_Surface* screen = SDL_SetVideoMode(728, 454, 32, flags);
if ( !screen ) {
printf("Impossivel inicializar a janela. Erro: %s\n", SDL_GetError());
return 1;
} else {
cout << "Janela Inicializada !" << endl;
cout << "Largura: " << screen->w
<< " - Altura: " << screen->h << endl;
}
CarregaImagem();
CarregaSprite();
cout << "Digite um numero e pressione ENTER para encerrar." << endl;
int s;
cin >> s;
cout << "Programa Encerrando..." << endl;
SDL_Quit();
return 0;
}
Tratando eventos
A SDL fucniona baseada em eventos que devem
ser gerenciados pelo programador atra'ves de um laço de trtamento de
eventos..
Copie o código abaixo e cole clogo após a chamada da função CarregaSprite(). Teste o programa.
cout << "Inicio do Laco de Render" << endl;
// main loop
bool done = false;
bool precisaRedesenhar = true;
// Inicializa contador de tempo.
while (!done) {
// message processing loop
SDL_Event event;
while (SDL_PollEvent(&event)) {
// check for messages
switch (event.type) {
// exit if the window is closed
case SDL_QUIT:
done = true;
break;
} // end switch
}// end of message processing
}
Tente fechar a janela do programa. Note que logo a seguir, você terá de
digitar o número como no exemplo anterior.
Remova o código responsável pela leitura da tecla. Tente fechar a janela do programa novamente.
Tratando teclas
Para realizar a leitura de teclas a SDL também utiliza-se dos eventos. Dentro do comando switch, da função main, adicione o teste para as teclas, conforme o exemplo abaixo.
case SDL_KEYDOWN:
// exit if ESCAPE is pressed
if (event.key.keysym.sym == SDLK_ESCAPE) {
done = true;
break;
} else if (event.key.keysym.sym == 276) { // LEFT
ARROW
cout <<
"Tecla ESQUERDA"<< endl;
} else if (event.key.keysym.sym == SDLK_RIGHT) {
posX++;
precisaRedesenhar = true;
}
Teste o programa pressiondo ESC e a tecla de seta para esquerda.
Modifique o programa para verificar o valor dos codigos de outras teclas.
Habilitando Repetição de Teclas
Para habilitar a repetição automátivca da teclas insira o comando abaixo, antes do laço while (!done).
// enable automatic key repetition
SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
Juntando OpenGL com a SDL
Para desenhar com OpenGL em um programa com a SDL, os comandos de
OpenGL devem ser colocados logo após o laço de tratamento de eventos.
No caso do exemplo que você está construindo, coloque a chamada
da função Desenha, logo após este laço, conforme o exemplo abaixo.
} // end switch
}// end of message processing
Desenha(screen->w, screen->h);
Note que a função desenha recebe o tamanho da tela.
Substitua a função Desenha, pelo código abaixo.
void Desenha(int w, int h) {
cout << "DESENHA: Largura: " << w << " - Altura: " << h << endl;
glClearColor(0, 0, 1, 0);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, w, h, 0, 1, -1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glViewport(0, 0, w, h);
glEnable(GL_TEXTURE_2D); // isto é necessário quando se deseja desenhar com texturas
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor3f(1,1,0);
glLineWidth(3);
glBegin(GL_LINES);
glVertex2i(0,0);
glVertex2i(100,100);
glEnd();
SDL_GL_SwapBuffers();
}
Altere os valores dos comandos glVertex. Tente controlar este valores com teclas.
Tratando o Rezise da Janela
Experimente redimensionar a janela do programa feito até este ponto. Você irá notar que não funciona direito.
Adicione os tratadores de eventos abaixo no comando switch do seu programa.
// SDL_APPACTIVE: When the
application is either minimized/iconified
// (gain=0) or restored
('gain'=1) this type of activation event occurs
case SDL_APPACTIVE:
if (event.active.gain==0) {
precisaRedesenhar = true;
}
break;
case SDL_VIDEORESIZE:
cout << "Janela alterada !" << endl;
SDL_Surface* screen =
SDL_SetVideoMode(event.resize.w, event.resize.h, 32, flags);
precisaRedesenhar = true;
break;
Evitando o Redesenho desnecessário da tela
Utilize a variável precisaRedesenhar para evitar que a tela seja redesenhada a todo o momento. Procure controlar para que isto seja feito somente quando necessário.
Copie deste link o código fonte desenvolvido até aqui.
Carregando uma imagem de fundo
Para carregar uma imagem iremos utilizar a classe CImage. Abra o arquivo Cimage.h e analise os métodos disponíveis.
Substitua o código da função CarregaImagem,
pelo
trecho apresentado abaixo. Talvez, em sua máquina, seja necessário
alterar o diretório onde a imagem está armazenada. No trecho de código
abaixo, o nome deste diretório está sendo passado por parâmetro para o
método loadImage.
Para testar, altere a função Desenha, removendo o
cout que está em seu início. Assim você poderá ver as mensagens do
método de carga da imagem.
void CarregaImagem() {
Img1 = new CImage();
if (Img1->loadImage("../../../bin/data/img/fundo.png")) {
cout << "Imagem de fundo carregada ! "
<< "Largura: " << Img1->getWidth() << " Altura: "
<< Img1->getHeight()<< endl;
} else {
cout << "Nao encontrou imagem de fundo !" << endl;
delete Img1;
Img1 = NULL;
}
cout << "Imagens Carregadas" << endl;
}
Exibindo uma Imagem
Para a exibição efetiva da imagem utilizamos o método draw. O código abaixo faz o desenho da imagem carregada. Coloque-o na função Desenha, logo após o comando glClear.
Analise a chamada do método setPosition que reposiciona a imagem na tela.
// Posiciona a imagem centralizada na janela
Img1->setPosition((w/2)-(Img1->getWidth()/2),(h/2)-(Img1->getHeight()/2));
Img1->draw();
Note que ao
inserir a imagem de fundo, a linha desenhada em OpenGL desapareceu,
embora tenha sido desenhada depois da imagem.
Misturando Imagens e Desenhos OpenGL
Para poder
misturar desenhos em OpenGL(linhas, retângulos, polígonos, etc) com
imagens carregadas pela classe Cimage, são necessários alguns cuidados.
O primeiro deles é desabilitar o uso de texturas através do comando glDisable(GL_TEXTURE_2D) antes da chamada do comando glBegin. Além disto, após o desenho dos objetos, é preciso setar cor da OpenGL para branco, com o comando glColor3f(1,1,1).
No trecho de código abaixo são feitas estas alterações.
glDisable(GL_TEXTURE_2D); // isto é necessário quando se deseja desenhar SEM texturas
glColor3f(1,1,0);
glLineWidth(3);
glBegin(GL_LINES);
glVertex2i(0,0);
glVertex2i(100,100);
glEnd();
glColor3f(1,1,1); // necessário para que a textura não seja misturada com a cor de desenho
Carregando uma sequência de imagens
Remova da função Desenha o bloco de código glBegin() ---> glEnd().
Para a apresentar uma animação com imagens é necessário carregar várias
imagens e exibi-las em sequência de forma a simular um movimento.
Neste programa, o trecho de código abaixo utiliza uma estrutura de dados do tipo vector, da biblioteca Standard Template Library, criada no início do código através da declaração
vector<CImage *> sprite;
As imagens carregadas por este trecho de código se chamam fig1.png, fig2.png, ...., fig6.png. Após a carga, um ponteiro apra o objeto usado para leitura, é armazenado no vector através do método push_back().
void CarregaSprite() {
CImage *aux;
char s[200];
for (int i=0; i<6; i++) {
sprintf(s,"../../../bin/data/img/fig%d.png",(i+1));
aux = new CImage();
if (aux->loadImage(s)) {
cout << "Imagem carregada !
" << "Largura: " << Img1->getWidth() << " Altura:
" << Img1->getHeight()<< endl;
sprite.push_back(aux);
} else {
cout << "Nao encontrou imagem." << endl;
delete aux;
aux = NULL;
}
}
cout << "Sprites carregadas !" << endl;
}
Exibindo uma sequência de imagens
Coloque dentro da funcão Desenha o incremento da variável currentFrame
e use esta variável para desenhar cada uma das imagens carregadas,
conforme o exemplo abaixo. Coloque este código depois do desenho da
imagem de fundo.
currentFrame++;//atualiza a sprite
if (currentFrame==6)
currentFrame=0;
if (sprite[currentFrame]) {
cout << "Posiciona e desenha imagem " << currentFrame << endl;
sprite[currentFrame]->setPosition(posX,Img1->getHeight()-100);
sprite[currentFrame]->draw();
}
SDL_GL_SwapBuffers();
Note que o trecho acima utiliza o método setPosition para posicionar as imagens a partir do valor da variável posX e da altura da imagem, dada pelo método getHeight() da classe CImage.
Adicione, no início da Desenha, os comandos de transparência para obter um efeito melhor na sobreposição das imagens
void Desenha(int w, int h) {
//cout << "DESENHA: Largura: " << w << " - Altura: " << h << endl;
// Enable transparency through blending
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Adicionando controle de tempo
Para controlar a velocidade de exibição dos frames da animação, pode-se utilizar a função SDL_GetTicks() da SDL. No exemplo abaixo, na variável currentFrame só é atualizada se o tempo decorrido desde da última exibição de um quadro da animação for maior que o valor da variável tickInterval.
// Inicializa contador de tempo.
// SDL_GetTicks() tells how many milliseconds have past since an arbitrary point in the past.
double lastTime = SDL_GetTicks();
double tickInterval = 1000;
// check update time
//cout
<< "Diferenca de Tempo: " << SDL_GetTicks()-lastTime
<< "milisegundos." << endl;
if (SDL_GetTicks()-lastTime>tickInterval)
{
currentFrame++; //atualiza a sprite
if (currentFrame==6)
currentFrame=0;
precisaRedesenhar = true;
lastTime = SDL_GetTicks();
}
Após o laço de eventos, coloque um teste para a variável precisaRedesenhar.
// Verifica quanto tempo já passou desde a última atualização
if (precisaRedesenhar) {
Desenha(screen->w, screen->h);
precisaRedesenhar = false;
}
Classe CSprite
Da maneira como o código está implementado até aqui, é necessário que cada
frame da sprite
esteja em um arquivo separado. Entretanto, normalmente todos os frames de uma sprite são
desenhados em um único arquivo. Além disso, para cada sprite é necessário controlar um
conjunto de parâmetros, tais como frame corrente e velocidade de deslocamento.
Portanto, para facilitar a carga e utilização de sprites foi implementada a classe
CSprite. Para sua utilização
a partir de agora, inclua no projeto os seguintes
fontes que estão disponíveis neste zip:
CMultiImage.cpp
CSprite.cpp
Portanto, as classes incluídas no projeto até aqui são:
- CImage:
representa e permite o desenho de uma imagem, com posição, orientação e
escala.
- CMultiImage:
representa e permite o desenho de imagens compostas por vários frames,
para usar em animação de personagens (sprites) ou tiles, por exemplo.
- CSprite:
subclasse de CMultiImage, é responsável por
gerenciar a animação e movimento, acrescentando controles para a exibição seqüencial
e controlada dos diversos frames.
- TextureManager:
gerencia as texturas, evitando carregar a mesma imagem duas vezes.
Abra os arquivos CSprite.h
e CMultiImage.h e analise seus métodos.
A classe CMultiImage
possui o método loadMultiImage
que recebe o seguinte conjunto de parâmetros:
- nomeArq: nome do
arquivo a ser carregado.
- w,
h: largura e altura de cada imagem individual (frame ou tile).
- hSpace, vSpace: espaçamento entre frames/tiles
(em pixels).
- xIni, yIni: posição inicial (na imagem) onde os frames/tiles
estão.
- column, row: quantidade de colunas e linhas.
- total:
quantidade de frames/tiles a serem carregados.
A partir da carga, é possível desenhar um frame específico,
através do método drawFrame(int frame). Também é possível indicar que as imagens
devem ser desenhadas com espelhamento (no eixo X), através do método setMirror.
A classe CSprite,
possui o método loadSprite
que chama o loadMultiImage
além de inicializar os seus atributos, que são:
- double xspeed,yspeed: velocidade em pixels por segundo.
- int updateCount: contagem
das atualizações.
- int firstFrame, lastFrame: interval de frames.
- int curframe: frame corrente.
- double curFrameD: frame corrente
como valor double.
- int framecount,framedelay: diminui a animação dos frames.
O método setFrameRange(int first, int last)
permite especificar o intervalo de frames a serem exibidos na animação. A
velocidade do sprite
é controlada através dos métodos setXspeed e setYspeed
(recebem os valores em pixels/segundo). Finalmente, a velocidade da animação é
definida através do método setAnimRate (recebe o
valor em frames/segundo). O método update recebe o intervalo de tempo que passou (em milisegundos) e atualiza a posição e frame sendo exibido,
conforme a necessidade. Os métodos setCurrentFrame(int c), frameForward() e frameBack() são usados para controlar
qual é o frame corrente, isto é, que está sendo desenhado no momento. O método draw é reponsável pelo desenho da sprite.
Utilizando a classe CSprite
Agora que já sabemos as funcionalidades da classe CSprite,
ao invés de manipularmos um conjunto de imagens de uma sprite separadamente, vamos
instanciar um objeto CSprite. Para isto, faça as
alterações conforme descrito a seguir:
1)
Inicialmente coloque o
include para o arquivo CSprite.h:
#include "CSprite.h"
2) No código implementado
na classe GameBasico, remova
a declaração de um vector de imagens:
vector<CImage
*> sprite;
e inclua a declaração de um ponteiro para CSprite, da
seguinte maneira:
CSprite *sprite;
2) 3) Na função void Desenha(int w, int h) remova o código
responsável pelo desenho das imagens da sprite:
if (sprite[currentFrame]) {
cout
<< "Posiciona e desenha imagem " << currentFrame
<< endl;
sprite[currentFrame]->setPosition(posX,Img1->getHeight()-100);
sprite[currentFrame]->draw();
}
e inclua simplesmente:
sprite->draw();
4) Remova o conteúdo da
função void CarregaSprite()
e coloque o trecho de código a seguir:
sprite = new CSprite();
sprite->loadSprite("data/img/char9.png",
128, 128, 0, 0, 0, 0, 4, 4, 16);
sprite->setAnimRate(15); // taxa de
animação em frames por segundo(troca dos frames dele)
sprite->setScale(1);
sprite->setPosition(0, Img1->getHeight()-138);
Observe que pode ser
necessário alterar o diretório onde a imagem está armazenada. Note também que
agora foi carregada apenas uma imagem que contém a sequência
de animação da sprite.
5) Dentro do while que trata os eventos (while (SDL_PollEvent(&event))) substitua:
if (SDL_GetTicks()-lastTime>tickInterval)
{
currentFrame++;//atualiza a
sprite
if
(currentFrame==6)
currentFrame=0;
precisaRedesenhar = true;
lastTime = SDL_GetTicks();
}
pelo seguinte trecho de
código:
if (SDL_GetTicks()-lastTime>tickInterval)
{
currentFrame++;//atualiza a sprite
if (currentFrame==6)
currentFrame=0;
sprite->setCurrentFrame(currentFrame);
precisaRedesenhar
= true;
lastTime
= SDL_GetTicks();
}
Desta
maneira o frame corrente da sprite é atualizado, pois o método draw() da classe CSprite
desenha apenas o frame corrente.
6) Para tratar os eventos
de seta para direita e seta para esquerda, que são programadas para fazer com
que a sprite se desloque para a esquerda e para a
direita, troque o seguinte trecho de código:
case SDL_KEYDOWN:
// exit if ESCAPE is pressed
if (event.key.keysym.sym
== SDLK_ESCAPE) {
done = true;
break;
} else if (event.key.keysym.sym
== 276) { // LEFT ARROW
} else if (event.key.keysym.sym
== SDLK_RIGHT) {
posX++;
precisaRedesenhar
= true;
}
break;
por
case SDL_KEYDOWN:
// exit if ESCAPE is
pressed
if
(event.key.keysym.sym == SDLK_ESCAPE) {
done = true;
break;
}
else if (event.key.keysym.sym
== SDLK_RIGHT) {
sprite->setX(sprite->getX()+1);
sprite->setMirror(false);
precisaRedesenhar = true;
}
else if (event.key.keysym.sym
== 276) { // LEFT ARROW
sprite->setX(sprite->getX()-1);
sprite->setMirror(true);
precisaRedesenhar = true;
}
break;
Note que sempre que as
teclas de seta são pressionadas, a posição da sprite
e o espelhamento no eixo x são atualizados.
Finalmente, execute e teste o código que agora utiliza a classe CSprite!