PUCRS/FACIN
Curso de Especiali
zaçã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:

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:

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:

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!