Tutorial SDL 2 en Español - Capítulo 5

Capítulo 5: Control de Teclas

SDL es capaz de controlar muchas cosas, una de ellas es el control del teclado. El control de teclas y botones es muy utilizado en gran medida en los juegos. En este tutorial voy a explicar cómo mostrar diferentes imágenes al presionar algunas teclas. NOTA: En este capítulo reestructuré y cambié algunos aspectos del código fuente para adaptarlos al lenguaje C++ estándar, ya que yo no soy partidario de trabajar mezclando C++ y C en un mismo código, por lo tanto voy a tratar de explicar lo más importante del código fuente y señalar los cambios con respecto a los códigos anteriores. Comencemos por los archivos de encabezado:
//Inclusión de los encabezados utilizados en nuestros programas
#include <SDL.h>
#include <iostream>
#include <string>
Como puedes ver, he quitado #include <stdio.h> que es un encabezado de C estándar, y lo cambié por #include <iostream> que es un encabezado de C++ estándar, que también es utilizado para la entrada y salida de cadena de textos, totalmente compatible con la programación orientada a objeto de C++. También incluimos también el encabezado <stream> que se utiliza trabajar con cadenas de textos (stream), para manejar los mensajes y los nombres de los archivos de una manera más cómoda. Todo esto lo cambié porque voy a trabajar de ahora en adelante por motivos de compatibilidad totalmente con C++. Observe también que modifiqué el comentario. Luego de esto, hacemos una enumeración de las superficies de las imágenes que vamos a mostrar en pantalla.
//Enumeramos las superficies
enum ImgSuperficies{
    IMG_SUPERF_PREDETERMINADO,
    IMG_SUPERF_ARRIBA,
    IMG_SUPERF_ABAJO,
    IMG_SUPERF_IZQUIERDA,
    IMG_SUPERF_DERECHA,
    IMG_SUPERF_TODAS
};
Las enumeraciones (enum) es una forma rápida de hacer constantes. En vez de colocar por ejemplo: const int IMG_SUPERF_PREDETERMINADO = 0; const int IMG_SUPERF_ARRIBA = 1; const int IMG_SUPERF_ABAJO = 2, etc, simplemente hacemos la enumeración y se asignan automáticamente un valor creciente a las constantes que comienza desde 0. También se pueden colocar valores explícitos. Yo hago esto porque es más fácil recordar cuando se trabajan con varios elementos enumerados, por ejemplo, supongamos que en nuestro juego debemos saber que el tipo de imagen arriba es 1, la imagen abajo es 2, la imagen izquierda es 3 y la imagen derecha es 4, y el juego tiene miles de líneas de código donde se requiera saber el tipo de imagen, tendrá menos dolores de cabeza usando if(img.type == IMG_LEFT) que if(img.type == 3).
Luego después de las declaraciones de las superficies tenemos una nueva sentencia:
//Inicialización de la ventana, la superficie y la imagen
SDL_Window* Ventana         = NULL; //La ventana donde se renderizará la imagen
SDL_Surface* Superficie     = NULL; //La superficie que contendrá la ventana
SDL_Surface* ImagenMostrada = NULL; //La imagen que se mostrará en la superficie
SDL_Surface* ImagenSuperficie[IMG_SUPERF_TODAS]; //Las imagenes que corresponden con la tecla presionada
Después de declarar la ventana y las superficies hemos declarado un puntero a una matriz unidimensional llamada ImagenSuperficie de tipo SDL_Surface donde vamos a cargar todas las imágenes que iremos a mostrar cuando el usuario presione ciertas teclas del teclado.
Como mencioné anteriormente, al cambiar el archivo de cabecera stdio.h por iostream debemos cambiar las funciones que dependían del archivo de cabecera anterior por funciones de la cabecera nueva. Por ese motivo vamos a cambiar la función printf que es una función de C por cout que es una función de C++ como se puede observar:
//Cambiamos las funciones printf por cout
printf("ERROR: No se pudo inicializar SDL, Error SDL: %s\n", SDL_GetError());

std::cout << "ERROR: No se pudo inicializar SDL, Error SDL: " << SDL_GetError() << "\n";
El prefijo std:: se debe a que la función cout pertenece al espacio de nombres std. Si desea utilizar la función cout sin el prefijo, puede "usar" el espacio de nombres incluyendo la sentencia using namespace std; al inicio del código, después de incluir las cabeceras.
Ahora vamos a ver como cargar cada imagen en la superficie, esto lo hacemos en la función cargarMedios.
bool cargarMedios(){
    //Bandera de carga correcta
    bool correcto = true;

    //Establecemos los nombres de los archivos que se van a abrir en una matriz unidimensional
    std::string NombreArchivos[IMG_SUPERF_TODAS] = {"Predeterminado.bmp","Arriba.bmp","Abajo.bmp", "Izquierda.bmp","Derecha.bmp"};

    //Carga cada una de las imágenes
    for(int i=0; i <IMG_SUPERF_TODAS;i++){
        ImagenSuperficie[i] = SDL_LoadBMP(NombreArchivos[i].c_str());
        if(ImagenSuperficie[i] == NULL){
            std::cout << "No se pudo cargar la imagen " << NombreArchivos[i] << " SDL Error: " << SDL_GetError() << "\n";
            correcto = false;
        }
    }

    //Salimos de la función
    return correcto;
}
En esta función, declaramos una matriz unidimensional de tipo string con todos los nombres de los archivos llamada NombreArchivos. Con una sentencia for, cargamos cada una de las imágenes que vamos a utilizar en la matriz ImagenSuperficie usando la función SDL_LoadBMP, donde especificamos en su parámetro la matriz NombreArchivos. La variable i usada como contador en el for especifica el índice de la matrices a medida que esta va aumentando. Esto lo hacemos para automatizar y ahorrar líneas de código. En la función cerrar también automatizamos la liberación de los recursos.
void cerrar(){
    //Liberamos la superficie utilizada
    for(int i =0; i < IMG_SUPERF_TODAS; i++){
        SDL_FreeSurface(ImagenSuperficie[i]);
        ImagenSuperficie[i] = NULL;
    }

    //Destruimos la Ventana para liberar recursos.
    SDL_DestroyWindow(Ventana);

    //Quitamos el subsistema de SDL.
    SDL_Quit();
}
También en esta función usamos for para ir liberando una a una las superficies y de esta forma liberamos los recursos.
Luego en la función main antes de entrar al bucle principal se establecerá la imagen predeterminada:
//Establece la imágen predeterminada
ImagenMostrada = ImagenSuperficie[IMG_SUPERF_PREDETERMINADO];
Esta sentencia establece la imagen de la matriz ImagenSuperficie especificada con el subíndice IMG_SUPERF_PREDETERMINADO en ImagenMostrada. De este modo se establece la imagen que se mostrará de forma predeterminada. Luego de esto en el bucle principal veremos como captar los eventos para realizar una acción.
            //Bucle principal
            while(!quitar){
                //Maneja la cola de eventos
                while(SDL_PollEvent(&e) != 0){
                    //Si el usuario quiere salir
                    if(e.type == SDL_QUIT){
                        quitar = true;
                    }
                    //Establece la imagen cuando el usuario presiona las teclas
                    else if(e.type == SDL_KEYDOWN){
                        switch(e.key.keysym.sym){
                            case SDLK_UP:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_ARRIBA];
                                break;
                            case SDLK_DOWN:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_ABAJO];
                                break;
                            case SDLK_LEFT:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_IZQUIERDA];
                                break;
                            case SDLK_RIGHT:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_DERECHA];
                                break;
                            case SDLK_ESCAPE:
                                quitar = true;
                                break;
                            default:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_PREDETERMINADO];
                                break;
                        }
                    }
                }
Al procesar la cola de eventos, observamos si el usuario quiere salir con SDL_QUIT, sino, verificamos si fue presionado una tecla con SDL_KEYDOWN, si es así, veremos cual es la tecla con la sentencia switch comprobando a e.key.keysym.sym que contiene información acerca la tecla que fue presionada y dependiendo de ello, se establecerá la imagen que corresponda. Luego de esto, nos queda el procesamiento del resto del código, en este caso las funciones SDL_BlitSurface y SDL_UpdateWindowSurface.

El código fuente debe quedar de la siguiente forma:
//Inclusión de los encabezados utilizados en nuestros programas
#include <SDL.h>
#include <iostream>
#include <string>

//Enumeramos las superficies
enum ImgSuperficies{
    IMG_SUPERF_PREDETERMINADO,
    IMG_SUPERF_ARRIBA,
    IMG_SUPERF_ABAJO,
    IMG_SUPERF_IZQUIERDA,
    IMG_SUPERF_DERECHA,
    IMG_SUPERF_TODAS
};

//Declaración de la funciones
bool Inicializar();  //Inicialización de SDL y creación de Ventanas
bool cargarMedios(); //Carga los medios
void cerrar();       //Libera los medios y cierra SDL

//Inicialización de la ventana, la superficie y la imagen
SDL_Window* Ventana         = NULL; //La ventana donde se renderizará la imagen
SDL_Surface* Superficie     = NULL; //La superficie que contendrá la ventana
SDL_Surface* ImagenMostrada = NULL; //La imagen que se mostrará en la superficie
SDL_Surface* ImagenSuperficie[IMG_SUPERF_TODAS]; //Las imagenes que corresponden con la tecla presionada

bool Inicializar(){
    //Bandera de inicialización es correcta
    bool correcto = true;

    //Inicializa el subsistema de Video
    if(SDL_Init(SDL_INIT_VIDEO) < 0){
        std::cout << "ERROR: No se pudo inicializar SDL, Error SDL: " << SDL_GetError() << "\n";
        correcto = false;
    }
    else{
        //Se crea la ventana principal
        Ventana = SDL_CreateWindow("Tutorial SDL 2", 50, 50, 640, 480, SDL_WINDOW_SHOWN);
        if(Ventana == NULL){
            std::cout << "ERROR: No se pudo crear la ventana, SDL_Error: " << SDL_GetError() << "\n";
            correcto = false;
        }
        else{
            //Se crea la superficie para la ventana principal
            Superficie = SDL_GetWindowSurface(Ventana);
        }
    }

    //Salimos de la función
    return correcto;
}

bool cargarMedios(){
    //Bandera de carga correcta
    bool correcto = true;

    //Establecemos los nombres de los archivos que se van a abrir en una matriz unidimensional
    std::string NombreArchivos[IMG_SUPERF_TODAS] = {"Predeterminado.bmp","Arriba.bmp","Abajo.bmp", "Izquierda.bmp","Derecha.bmp"};

    //Carga cada una de las imágenes
    for(int i=0; i <IMG_SUPERF_TODAS;i++){
        ImagenSuperficie[i] = SDL_LoadBMP(NombreArchivos[i].c_str());
        if(ImagenSuperficie[i] == NULL){
            std::cout << "No se pudo cargar la imagen " << NombreArchivos[i] << " SDL Error: " << SDL_GetError() << "\n";
            correcto = false;
        }
    }

    //Salimos de la función
    return correcto;
}

void cerrar(){
    //Liberamos la superficie utilizada
    for(int i =0; i < IMG_SUPERF_TODAS; i++){
        SDL_FreeSurface(ImagenSuperficie[i]);
        ImagenSuperficie[i] = NULL;
    }

    //Destruimos la Ventana para liberar recursos.
    SDL_DestroyWindow(Ventana);

    //Quitamos el subsistema de SDL.
    SDL_Quit();
}

int main(int argc, char* args[]){
    //Inicializa SDL y crea las ventanas
    if(!Inicializar()){
        std::cout << "No se pudo inicializar\n";
    }
    else{
        //Carga los medios
        if(!cargarMedios()){
            std::cout << "No se pudo cargar los medios\n";
        }
        else{
            //Bandera del bucle principal
            bool quitar = false;

            //Controlador de Eventos
            SDL_Event e;

            //Establece la imágen predeterminada
            ImagenMostrada = ImagenSuperficie[IMG_SUPERF_PREDETERMINADO];

            //Bucle principal
            while(!quitar){
                //Maneja la cola de eventos
                while(SDL_PollEvent(&e) != 0){
                    //Si el usuario quiere salir
                    if(e.type == SDL_QUIT){
                        quitar = true;
                    }
                    //Establece la imagen cuando el usuario presiona las teclas
                    else if(e.type == SDL_KEYDOWN){
                        switch(e.key.keysym.sym){
                            case SDLK_UP:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_ARRIBA];
                                break;
                            case SDLK_DOWN:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_ABAJO];
                                break;
                            case SDLK_LEFT:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_IZQUIERDA];
                                break;
                            case SDLK_RIGHT:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_DERECHA];
                                break;
                            case SDLK_ESCAPE:
                                quitar = true;
                                break;
                            default:
                                ImagenMostrada = ImagenSuperficie[IMG_SUPERF_PREDETERMINADO];
                                break;
                        }
                    }
                }

                //Aplicamos la imagen en la superficie de la ventana
                SDL_BlitSurface(ImagenMostrada, NULL, Superficie, NULL);

                //Actualizamos la superficie de la ventana
                SDL_UpdateWindowSurface(Ventana);
            }
        }
    }

    //Liberamos los recursos y cerramos SDL
    cerrar();

    //Salimos de main
    return 0;
}
Hasta aquí hemos visto como saber cual tecla fue presionada con SDL 2. Puedes descargar el código fuente completo aquí.

1 comentario:

  1. Muy buen tutorial, esepro que lo sigas algun día, aunque ya con estos capitulos es suficiente como para introducirse a la documentación en parte, saludos.

    ResponderEliminar