Tutorial de SDL – Parte 4 – Captura de eventos do teclado

Continuando o tutorial sobre SDL publicado originalmente no site lazyfoo, iremos ver agora como tratar a entrada de dados do usuário pelo teclado.

Fechar a janela através do botão X é somente um dos eventos que o SDL é capaz de tratar. Outro tipo de entrada utilizada bastante em jogos é o teclado. Nesse tutorial iremos fazer diferentes imagens serem exibidas dependendo da tecla que for pressionada.

//Key press surfaces constants
enum KeyPressSurfaces
{
    KEY_PRESS_SURFACE_DEFAULT,
    KEY_PRESS_SURFACE_UP,
    KEY_PRESS_SURFACE_DOWN,
    KEY_PRESS_SURFACE_LEFT,
    KEY_PRESS_SURFACE_RIGHT,
    KEY_PRESS_SURFACE_TOTAL
};

Na parte superior do código, declaramos uma enumeração de diferentes superfícies que teremos. Enumerações são um atalho para constantes simbólicas ao invés de termos de declarar const int KEY_PRESS_SURFACE_DEFAULT = 0; const int KEY_PRESS_SURFACE_UP = 1; const int KEY_PRESS_SURFACE_DOWN = 2; e assim em diante. Elas por padrão começam a contagem de 0 e vai-se somando 1 para cada enumeração declarada. Isso significa que KEY_PRESS_SURFACE_DEFAULT é 0, KEY_PRESS_SURFACE_UP é 1, KEY_PRESS_SURFACE_DOWN é 2, KEY_PRESS_SURFACE_LEFT é 3, KEY_PRESS_SURFACE_RIGHT é 4, e KEY_PRESS_SURFACE_TOTAL é 5. É possível associar a cada uma delas um valor inteiro explicitamente, mas não veremos isso aqui. Uma pesquisa rápida no Google sobre esse assunto deve ser suficiente.
Um hábito ruim de programadores iniciantes é usar números arbitrários ao invés de contantes simbólicas. Por exemplo, usar 1 para menu principal, 2 para opções, etc o que é serve para programas pequenos, mas quando é preciso lidar com milhares de linhas de código ter um linha “if(option == 1)” irá causar muita dor de cabeça, ao invés de usar algo como “if(option == MAIN_MENU)”.

//Starts up SDL and creates window
bool init();
//Loads media
bool loadMedia();
//Frees media and shuts down SDL
void close();
//Loads individual image
SDL_Surface* loadSurface( std::string path );
//The window we'll be rendering to
SDL_Window* gWindow = NULL;
//The surface contained by the window
SDL_Surface* gScreenSurface = NULL;
//The images that correspond to a keypress
SDL_Surface* gKeyPressSurfaces[ KEY_PRESS_SURFACE_TOTAL ];
//Current displayed image
SDL_Surface* gCurrentSurface = NULL;

Junto com nossos protótipos normais de funções, teremos uma nova função chamada loadSurface. Existe uma regra geral que diz se você copiar/colar código, irá fazer algo errado. Ao invés de copiar/colar o código de carregamento de imagens a cadas vez, iremos usar uma função que faz isso.
O que importa para nosso exemplo em particular é que teremos um array de ponteiros para superfícies SDL chamado gKeyPressSurfaces que irá conter todas as imagens que usaremos. Dependendo de que tecla o usuário pressionar, iremos ajustar o valor de gCurrentSurface (que será a imagem a ser inserida na tela) para uma dessas superfícies.

SDL_Surface* loadSurface( std::string path )
{
    //Load image at specified path
    SDL_Surface* loadedSurface = SDL_LoadBMP( path.c_str() );
    if( loadedSurface == NULL )
    {
        printf( "Unable to load image %s! SDL Error: %s\n", path.c_str(), SDL_GetError() );
    }
    return loadedSurface;
}

Aqui temos a função loadSurface que carrega as imagens e reporta um erro se algo der errado. É basicamente o mesmo que vimos em artigos anteriores, mas colocar o carregamento da imagem e o tratamento de erros em uma função torna mais fácil adicionar e depurar esse processo.
Antes que algum programador C++ questione isso, cabe ressaltar que essa função não causa nenhum vazamento de memória. Ela aloca memória para carregar uma nova superfície SDL e retorna essa superfície sem liberar a memória, mas qual seria o ponto de alocar espaço para uma nova superfície e imediatamente desaloca-la? O que essa função faz é criar a superfície e retornar esta superfície recém criada de forma que quem chamou essa função pode desaloca-la depois de te-la usado. No exemplo, a superfície alocada é liberada na função close.

bool loadMedia()
{
    //Loading success flag
    bool success = true;
    //Load default surface
    gKeyPressSurfaces[ KEY_PRESS_SURFACE_DEFAULT ] = loadSurface( "04_key_presses/press.bmp" );
    if( gKeyPressSurfaces[ KEY_PRESS_SURFACE_DEFAULT ] == NULL )
    {
        printf( "Failed to load default image!\n" );
        success = false;
    }
    //Load up surface
    gKeyPressSurfaces[ KEY_PRESS_SURFACE_UP ] = loadSurface( "04_key_presses/up.bmp" );
    if( gKeyPressSurfaces[ KEY_PRESS_SURFACE_UP ] == NULL )
    {
        printf( "Failed to load up image!\n" );
        success = false;
    }
    //Load down surface
    gKeyPressSurfaces[ KEY_PRESS_SURFACE_DOWN ] = loadSurface( "04_key_presses/down.bmp" );
    if( gKeyPressSurfaces[ KEY_PRESS_SURFACE_DOWN ] == NULL )
    {
        printf( "Failed to load down image!\n" );
        success = false;
    }
    //Load left surface
    gKeyPressSurfaces[ KEY_PRESS_SURFACE_LEFT ] = loadSurface( "04_key_presses/left.bmp" );
    if( gKeyPressSurfaces[ KEY_PRESS_SURFACE_LEFT ] == NULL )
    {
        printf( "Failed to load left image!\n" );
        success = false;
    }
    //Load right surface
    gKeyPressSurfaces[ KEY_PRESS_SURFACE_RIGHT ] = loadSurface( "04_key_presses/right.bmp" );
    if( gKeyPressSurfaces[ KEY_PRESS_SURFACE_RIGHT ] == NULL )
    {
        printf( "Failed to load right image!\n" );
        success = false;
    }
    return success;
}

Aqui temos a função loadMedia que carrega todas as imagens que iremos renderizar na tela.

            //Main loop flag
            bool quit = false;
            //Event handler
            SDL_Event e;
            //Set default current surface
            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_DEFAULT ];
            //While application is running
            while( !quit )
            {

Na função main antes de entrar no loop principal, nós definimos a superfície padrão a ser exibida.

                //Handle events on queue
                while( SDL_PollEvent( &e ) != 0 )
                {
                    //User requests quit
                    if( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }
                    //User presses a key
                    else if( e.type == SDL_KEYDOWN )
                    {
                        //Select surfaces based on key press
                        switch( e.key.keysym.sym )
                        {
                            case SDLK_UP:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_UP ];
                            break;
                            case SDLK_DOWN:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_DOWN ];
                            break;
                            case SDLK_LEFT:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_LEFT ];
                            break;
                            case SDLK_RIGHT:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_RIGHT ];
                            break;
                            default:
                            gCurrentSurface = gKeyPressSurfaces[ KEY_PRESS_SURFACE_DEFAULT ];
                            break;
                        }
                    }
                }

Aqui temos o loop de eventos. Como você pode ver, tratamos o evento de fechamento da janela como fizemos no artigo anterior, e também lidamos com o evento SDL_KEYDOWN. Esse evento é disparado quando você pressiona uma tecla do teclado.
Dentro de SDL_Event temos um evento SDL_Keyboard que contém a informação do evento de pressionamento de tecla. Dentro desse evento temos um SDL_Keysym que contém a informação sobre a tecla que foi pressionada. O Keysym contém um Keycode que identifica a tecla que foi pressionada.
Como você pode ver, o que esse código faz é definir a superfície com base na tecla que foi pressionada. Dê uma olhada na documentação do SDL se quiser ver quais os Keycodes das outras teclas.

                //Apply the current image
                SDL_BlitSurface( gCurrentSurface, NULL, gScreenSurface, NULL );
                //Update the surface
                SDL_UpdateWindowSurface( gWindow );

Depois que tratamos quais teclas foram pressionadas e a superfície foi selecionada, nós exibimos a superfície selecionada na tela.

Baixe os arquivos de imagem e o código fonte para esse artigo aqui.