13 Juego de cartas

Ahora que ya conocemos bastantes características de SDL2, vamos a hacer un juego completo, aunque sencillo.

Los juegos de cartas son fáciles para empezar, ya que no tendremos que preocuparnos por el delta time ni por optimizar mucho los tiempos.

En este capítulo vamos a programar el juego de cartas clásico Memory. El juego consiste en un conjunto de parejas de cartas que se colocan sobre el tablero boca abajo formando una cuadrícula. El jugador podrá levantar dos cartas, si son iguales podrá retirarlas del tablero y continuar jugado, si son diferentes tendrá que volver a colocarlas boca abajo en sus posiciones originales. La idea es memorizar la posición de las cartas previamente levantadas para, idealmente, no volver a levantarlas en primer lugar, de modo que se hacerlo en segundo lugar cuando se descubra una carta cuya pareja se haya visto en una jugada anterior.

Se puede jugar por un único jugador, y el objetivo será levantar todas las parejas con un mínimo de jugadas, o se puede jugar entre varios jugadores, en cuyo caso el objetivo será levantar más parejas que el resto de contrincantes.

Como variante del anterior podríamos jugar contra el ordenador, aunque tendremos que simular una memoria defectuosa para evitar que nos gane siempre.

También se puede variar la dificultad de varias formas:

  • Usando diferente número de cartas, cuantas más cartas, más difícil será el juego.
  • Usando diferentes juegos de cartas, cuanto más parecidas sean las cartas, mayor será la dificultad.
  • Limitando el tiempo que las cartas levantadas que no formen una pareja se muestren antes de dales la vuelta.

En principio jugaremos con un único juego de cartas, de modo que seleccionaremos la dificultad cambiando el número de cartas. Con cincuenta parejas de cartas diferentes tendremos cien cartas que podemos mostrar en una cuadrícula de 10x10 para la dificultad máxima.

Seguiremos ampliando nuestra librería de clases para manejar más elementos de la librería.

Por ejemplo, tendremos que mostrar menús para que el usuario elija entre las opciones disponibles, puntuaciones, etc. Para ello necesitaremos mostrar texto, de modo que añadiremos una nueva clase para ello.

La clase para mostrar texto se encargará de manejar una fuente: tamaño, estilo, etc y mostrar un texto en las coordenadas y con el color indicados.

Clase Font

Esta clase nos permitirá usar una fuente, modificar sus atributos de estilo, tamaño, etc y mostrar una cadena en las coordenadas indicadas y con el color deseado.

Es posible que necesitemos instanciar varios objetos de esta clase, por lo que tendremos que asegurarnos de que sólo inicializamos y liberamos la librería para manipulación de texto TTF una única vez. Por ello modificaremos la clase SDL2 para que nos permita inicializar opcionalmente la librería TTF.

/*
 * Clases para manejar librería SDL2
 * SDL_Init
 * SDL_Quit
 * TTF_Init
 * TTF_Quit
 */
#include <SDL.h>
#include <SDL_ttf.h>
/** \brief
 * Espacio con nombre para el wrapper de SDL2.
 *
 */
namespace sdl {

/** \brief
 * La clase SDL2 contiene los métodos y datos para iniciar y liberar
 * los recursos necesarios para utilizar las librerías SDL2.
 *
 */

class SDL2 {
protected:
    bool valid = true; /**< Indica si las librerías han podido ser iniciadas correctamente.  */
    bool valid_ttf = true; /**< Indica si la librería TTF ha podido ser iniciada correctamente.  */

public:
/** \brief
 * Constructor.
 * \param flags Banderas de inicialización de la librería SDL2. El valor por defecto es SDL_INIT_EVERYTHING.
 * \param initTTF indica si se debe inicializar la librería TTF
 */
    SDL2(Uint32 flags=SDL_INIT_EVERYTHING, bool initTTF=false) {
        valid = (SDL_Init(flags) == 0);
        if(valid && initTTF) {
            valid_ttf = !TTF_Init();
        }
    }
/** \brief
 * Destructor.
 *
 */
    ~SDL2() {
        if(valid) SDL_Quit();
        if(valid_ttf) TTF_Quit();
    }
};
} // namespace

El constructor crea una fuente a partir de una ruta a un fichero TTF, y el destructor libera ese recurso.

Se añaden métodos para modificar el estilo, tamaño, borde, dirección e interior, así como un método para mostrar el texto en las coordenadas y con el color indicado. Sobrecargaremos esta función para usar un objeto Point en lugar de coordenadas.

/*
 * Clases para manejar fuentes en SDL2
 *
 */

#ifndef _SDL_FONT
#define _SDL_FONT

#include <string>
#include <SDL.h>
#include <SDL_ttf.h>
#include "sdl_rect.h"
#include "sdl_renderer.h"
#include "sdl_color.h"

namespace sdl {

class Font {
protected:
    TTF_Font *font;
    Renderer& renderer;

public:
    Font(Renderer& render, const std::string& fontfile, int ptsize=17) : renderer(render)
        { font = TTF_OpenFont(fontfile.c_str(), ptsize); }
    ~Font() { TTF_CloseFont(font); }
    void SetStyle(int style) { TTF_SetFontStyle(font, style); }
    void SetSize(int ptsize) { TTF_SetFontSize(font, ptsize); }
    void SetOutline(int outline) { TTF_SetFontOutline(font, outline); }
    void SetDirection(TTF_Direction direction) { TTF_SetFontDirection(font, direction); }
    void SetHinting(int hinting) { TTF_SetFontHinting(font, hinting); }
    Rect GetArea(const std::string& text);
    void RenderUTF8_Blended(const std::string& text, int x, int y, sdl::Color fg);
    void RenderUTF8_Blended(const std::string& text, sdl::Point& p, sdl::Color fg) {
        RenderUTF8_Blended(text, p.X(), p.Y(), fg);
    }
};

// TODO: crear variantes SetNormal, SetBold.. UnsetNormal, UnsetBold ??
// TODO: crear métodos SetLTR, SetRTL, SetTTB y SetBTT

} // namespace
#endif

Clase Menu

Cuando el programa comience, lo primero que hará será mostrar un menú en el que el jugador podrá seleccionar algunas opciones. Para ello tendremos que ser capaces de mostrar texto y manejar el ratón, de modo que comenzaremos por crear un par de clases para estas tareas.

A lo largo del juego es probable que necesitemos diferentes menús, así que será útil encapsular los menús en una clase.

Esta clase mostrará varias opciones que el usuario podrá seleccionar mediante una tecla o pulsando con el ratón.

Debido a que no queremos crear un contexto de renderizado para cada menú y para cada partida del juego, tendremos que modificar nuestra librería de clases para que otras clases, como Game o Menu, puedan usar una ventana o un contexto de renderizado existente, en lugar de crear unos propios.

Para crear la clase Menu nos basaremos en la clase Game, de modo que nuestro bucle de juego termine cuando el usuario seleccione una opción.

Debido a que un menú debe devolver un valor a la aplicación que indique qué opción ha seleccionado el usuario, modificaremos el método Run de la clase Game para que devuelve un valor de tipo char. Esto no afecta a la clase Game, que sencillamente ignorará el valor de retorno.

Para manejar menús usaremos dos clases. La primera para encapsular una opción de menú, a la que llamaremos OptionMenu, y la segunda para encapsular menús completos, a la que llamaremos Menu.

Nuestras clases quedan así:

/*
 * Clases para manejar un menu en SDL2
 */
#ifndef _SDL_MENU
#define _SDL_MENU

#include <SDL.h>
#include <string>
#include <vector>
#include "sdl_game.h"
#include "sdl_rect.h"
#include "sdl_font.h"

namespace sdl {

class MenuOption {
protected:
    Rect area;        /**< Área para activación con mouse */
    Font &font;       /**< Fuente */
    int textsize;     /**< Tamaño de fuente */
    char key;         /**< Tecla de activación */
    std::string line; /**< Línea de texto completa de la opción */
    std::string cad;  /**< Texto de la opción, excluyendo tecla de activación y marca de selección */
    bool selected;    /**< Opción seleccionada/activa */
    void Move(Point& p); /**< Método privado para cambiar la posición de una opción de menú */

public:
    MenuOption(Font& f, const std::string c, char k, int size) : font(f), textsize(size), key(k), cad(c), selected(false) {}
    char Key() { return key; }
    void SetSelected(bool s=true) { selected = s; }
    bool TestPoint(Point &p) { return SDL_PointInRect(p.Get(), area.Get()); }
    void Render(Renderer& renderer, sdl::Point& pos, sdl::Color fg) {
        Move(pos);
        font.RenderUTF8_Blended(line, pos, fg);
    }
};

class Menu : public Game{
protected:
    Font font;                       /**< Fuente */
    int textsize;                    /**< Tamaño de fuente para opciones */
    std::string title;               /**< Texto del título */
    Point titlePos;                  /**< Coordenadas del título */
    Point menuPos;                   /**< Coordenadas de la primera línea de menú */
    std::vector<MenuOption> options; /**< Opciones de menú */
    Color titleColor;                /**< Color del texto de título */
    Color optionColor;               /**< Color del texto de opciones */
    char retval;                     /**< Valor de retorno */

public:
    Menu(Window& win, Renderer& ren, const std::string& f) : Game(win, ren),
        font(ren, f), title(""), titleColor("#ffff00ff"), optionColor("#ffff00ff"), retval(0) {}
    void SetTitlePos(int x, int y) { titlePos = Point(x, y); }
    void SetTitlePos(sdl::Point p) { titlePos = Point(p); }
    void SetMenuPos(int x, int y) { menuPos = Point(x, y); }
    void SetMenuPos(sdl::Point p) { menuPos = Point(p); }
    void SetTitle(const std::string& t) { title = t; }
    void SetTitleColor(const Color& bk) { titleColor = bk; }
    void SetOptionColor(const Color& fg) { optionColor = fg; }
    void AddOption(const std::string& opt, char key, int size=17);
    void SetSel(char k) {
        for(size_t i = 0; i< options.size(); i++)
            options[i].SetSelected(options[i].Key()==k);
    }
    void Clear(Color color=Color("#000000ff"));
    Font& GetFont() { return font; }
    void SetTextSize(int pt);
    void Init() { Clear(); }
    void Update() {} /**< No es necesario */
    void Events();
    char Run()
    void Render();
};

} // namespace sdl
#endif // _SDL_MENU

Cada vez que sea necesario mostrar una de las opciones se invocará al método Render, que a su vez invoca al método Move, que generará el texto con la marca de selección adecuada y la tecla de activación, y calculará el área activa para activaciones mediante el mouse.

El método TestPoint verifica si las coordenadas pasadas como parámetro está en el interior del área de la opción.

La clase Menu se deriva de la clase Game, ya que tiene muchas funcionalidades comunes.

La mayor parte de los métodos tienen un propósito claro según su nombre. SetSel activa la marca de selección para la opción indicada mediante su tecla de activación. Events procesa los eventos de teclado y ratón para verificar si se activado alguna de las opciones. Render muestra el menú en un formato básico. En nuestro juego usaremos una clase derivada de Menu para poder decorar los menús a nuestro gusto. Run difiere algo del bucle de juego, ya que solo será necesario mostrar el menú una vez, y como no necesitamos actualizar la pantalla, no tendremos que calcular deltas.

Para nuestro ejemplo crearemos una clase derivada de Menu, llamada MemMenu, que modifica el método Render, para añadir un marco decorativo:

class MemMenu : public sdl::Menu {
public:
    MemMenu(sdl::Window& win, sdl::Renderer& ren, const std::string& font) : sdl::Menu(win, ren, font) {}
    void Render() {
        sdl::Rect re;

        window.GetSize(&re.W(), &re.H());
        re.X() = 0;
        re.Y() = 0;
        renderer.SetDrawColor(0,0,0,255);
        for(int i=0; i<9; i++) {
            SDL_RenderDrawRect(renderer, re.Get());
            re.X()++; re.Y()++; re.W()-=2; re.H()-=2;
        }
        renderer.SetDrawColor(sdl::Color(0,169,157,255));
        for(int i=0; i<22; i++) {
            SDL_RenderDrawRect(renderer, re.Get());
            re.X()++; re.Y()++; re.W()-=2; re.H()-=2;
        }
        renderer.SetDrawColor(sdl::Color(41,171,226,255));
        for(int i=0; i<22; i++) {
            SDL_RenderDrawRect(renderer, re.Get());
            re.X()++; re.Y()++; re.W()-=2; re.H()-=2;
        }
        Menu::Render();
    }
};
Gráficos para Memory
Gráficos para Memory

Clase Texture

Necesitaremos una clase para encapsular texturas. La misma clase nos podría servir para manejar una textura con un único gráfico o para manejar texturas con varios, ya sea en forma de cuadrícula o con gráficos de diferente tamaño.

Una textura con un único gráfico equivale a una cuadrícula con una fila y columna. Para texturas con gráficos de diferente tamaño habrá que especificar las coordenadas y dimensiones de cada gráfico mediante un array de objetos de la clase Rect. De momento dejaremos esta opción para una versión futura de esta clase, o para otra clase diferente.

Por ejemplo, en un juego de cartas probablemente será más eficiente usar una única textura que contenga todas las cartas, y aprovechar que las funciones para mostrar texturas permiten seleccionar un área rectangular de origen y destino.

Para la aplicación actual bastará con una clase que pueda manejar gráficos del mismo tamaño almacenados en una única textura de forma matricial.

Para ello crearemos la clase TextureArray, a cuyo constructor le pasaremos un contexto de renderizado, una cadena con el nombre del fichero que contiene la textura, y la anchura y altura de cada subtextura.

De este modo, el constructor puede cargar la textura y calcular el número de filas y columnas que contiene el gráfico, y almacenar en un vector los rectángulos correspondientes a cada subtextura.

class TextureArray {
protected:
    Renderer *renderer;     /**< Contexto de renderizado */
    SDL_Texture *texture;   /**< Textura */
    std::vector<Rect> rect; /**< Rectángulos contenedores de subtexturas */
    int w, h;               /**< Anchura y altura de cada subtextura */
    int rows, cols;         /**< Número de filas y columnas */

public:
    TextureArray(Renderer& render, const std::string file, int tileWidth=0, int tileHeight=0);
    ~TextureArray() { SDL_DestroyTexture(texture); }
    int W() { return w; }
    int H() { return h; }
    SDL_Texture* Get() { return texture; }
    void Draw(Point punto, int index) {
        Rect dest=Rect(punto.X(), punto.Y(), w, h);
        SDL_RenderCopy(renderer, texture, rect[index].Get(), dest.Get());
    }
};

El método Draw mostrará la subtextura con el índice index en el punto indicado.

Clase Credits

Aunque en principio esta clase está destinada a mostrar información sobre la aplicación y sus creadores, en la práctica puede usarse para mostrar cualquier texto. Es muy parecida a la clase Menu, pero en lugar de retornar el valor de la opción elegida, simplemente mostrará un texto y retornará cuando el usuario pulse una tecla o haga click con el ratón.

/*
 * Clases para manejar un créditos en SDL2
 */
#ifndef _SDL_CREDITS
#define _SDL_CREDITS

#include <SDL.h>
#include <string>
#include <vector>
#include "sdl_game.h"
#include "sdl_rect.h"
#include "sdl_font.h"

namespace sdl {

class CreditsLine {
protected:
    Rect area;        /**< Área para activación con mouse */
    Font &font;       /**< Fuente */
    int textsize;     /**< Tamaño de fuente */
    std::string line; /**< Línea de texto completa de la opción */
    void Move(Point& p); /**< Método privado para cambiar la posición de una opción de menú */

public:
    CreditsLine(Font& f, const std::string c, int size) : font(f), textsize(size), line(c) {}
    void Render(Renderer& renderer, sdl::Point& pos, sdl::Color fg) {
        font.RenderUTF8_Blended(line, pos, fg);
    }
};

class Credits : public Game{
protected:
    Font font;                       /**< Fuente */
    int textsize;                    /**< Tamaño de fuente para opciones */
    std::string title;               /**< Texto del título */
    Point titlePos;                  /**< Coordenadas del título */
    Point textPos;                   /**< Coordenadas de la primera línea de menú */
    std::vector<CreditsLine> line;   /**< Opciones de menú */
    Color titleColor;                /**< Color del texto de título */
    Color textColor;                 /**< Color del texto de opciones */
    int retval;

public:
    Credits(Window& win, Renderer& ren, const std::string& f) : Game(win, ren),
        font(ren, f), title(""), titleColor("#ffff00ff"), textColor("#ffff00ff"), retval(0) {}
    void SetTitlePos(int x, int y) { titlePos = Point(x, y); }
    void SetTitlePos(sdl::Point p) { titlePos = Point(p); }
    void SetTextPos(int x, int y) { textPos = Point(x, y); }
    void SetTextPos(sdl::Point p) { textPos = Point(p); }
    void SetTitle(const std::string& t) { title = t; }
    void SetTitleColor(const Color& bk) { titleColor = bk; }
    void SetTextColor(const Color& fg) { textColor = fg; }
    void AddLine(const std::string& txt, int size=17);
    void Clear(Color color=Color("#000000ff"));
    Font& GetFont() { return font; }
    void SetTextSize(int pt);
    void Init() { Clear(); }
    void Update() {} /**< No es necesario */
    void Events();
    void Render();
    char Run();
};

} // namespace sdl
#endif // _SDL_CREDITS

Al igual que hicimos con los menús, crearemos una clase derivada de Credits en la que sobrescribiremos el método Render para añadir un marco decorativo.

Clase Button

Otra clase de objetos que nos interesa incorporar son los botones, que el usuario puede pulsar con el ratón para realizar distintas tareas.

Nuestros botones mostrarán un icono o imagen y un texto, rodeado de un marco. Podremos ocultarlos y deshabilitarlos de modo que solo sean accesibles cuando nos interese.

/*
 * Clases para manejar botones en SDL2
 */
#ifndef _SDL_BUTTON
#define _SDL_BUTTON

#include <string>
#include <SDL.h>
#include "sdl_renderer.h"
#include "sdl_texturearray.h"
#include "sdl_font.h"

namespace sdl {

class Button {
protected:
    Renderer& renderer;     /**< Contexto de renderización */
    TextureArray* texture;  /**< Textura con gráficos de botones */
    int index;              /**< Índice del gráfico dentro de la textura */
    Font& font;             /**< Fuente de caracteres */
    std::string line;       /**< Texto del botón */
    int textsize;           /**< Tamaño del texto */
    Color color;            /**< Color del texto */
    Rect rect;              /**< Área activa del botón */
    bool active;            /**< Indica si el botón es pulsable */
    bool visible;           /**< Indica si el botón debe ser mostrado */
    Point posTexture;       /**< Posición de la textura dentro del botón */
    Point posText;          /**< Posición del texto dentro del botón */
    int margen;             /**< Número de pixels que separan el texto y la imagen del borde */

public:
    Button(Renderer& r, Point pos, sdl::TextureArray* t, int i, std::string str, Font& f, int ts, Color c);
    void SetPos(Point &pos) { rect.X() = pos.X(); rect.Y() = pos.Y(); }
    void Render();
    void SetActive(bool a) { active=a; if(active) visible=true; }
    void SetVisible(bool v) { visible=v; }
    bool TestPoint(Point& p) { return SDL_PointInRect(p.Get(), rect.Get()); }
    bool Pushed(Point p);
};
} // namespace

#endif // _SDL_BUTTON

Clase Carta

Pasemos ahora a definir las clases que no forman parte del API, y son propias del juego.

Para empezar, necesitamos una clase que encapsule una carta.

#ifndef _SDL_CARTA
#define _SDL_CARTA

#include <string>
#include <SDL.h<

namespace mem {

class Carta {
private:
    sdl::TextureArray* texture; /**< Textura con los gráficos  */
    int index;                  /**< Índice del gráfico en la textura */
    bool mostrar;               /**< Indica si la carta está vuelta o no */
    bool bloqueada;             /**< Carta bloqueada  */
public:
    Carta() : texture(NULL), index(0), mostrar(false) {}
    Carta(sdl::TextureArray* text, int i) : texture(text), index(i), mostrar(false) {
    }
    void Show(sdl::Point& p);
    void Mostrar(bool m=true) { mostrar=m; }
    void Bloquear(bool b=true) { bloqueada=b; }
    bool Bloqueada() { return bloqueada; }
    bool operator==(Carta& c) { return index==c.index; }
};

void Carta::Show(sdl::Point& p) {
    if(mostrar) texture->Draw(p, index);
    else texture->Draw(p, 50);
}
}
#endif // _SDL_CARTA

Cada carta almacenará la información necesaria para mostrarla en pantalla, lo que incluye el gráfico, mediante un puntero a la textura y el índice dentro de esa textura, y dos valores booleanos. mostrar indicará si la carta se debe mostrar o no, en cuyo caso se mostrará un gráfico común que corresponde con la parte trasera de la carta. bloqueada indicará si la carta ya no puede ser volteada porque se ha descubierto previamente como parte de una pareja.

En cada "mazo" de cartas habrá dos de cada tipo, que formarán las parejas que el jugador debe descubrir. Sobrecargamos el operador de comparación para comparar si dos cartas son iguales, es decir, si tiene el mismo índice.

Clase Memory

La clase Memory es la que contiene el juego propiamente dicho.

El constructor se encarga de cargar los gráficos y las fuentes, así como de asignar valores iniciales a ciertos miembros y crear los botones que se usarán durante el juego.

Esta clase se deriva de Game, de modo que tendremos que redefinir algunos de sus métodos virtuales.

#ifndef _MEM_MEMORY
#define _MEM_MEMORY

#include <vector>
#include <cstdio>
#include <cstdlib>
#include <sdldll.h>
#include "mem_carta.h"

class Memory : public sdl::Game, public sdl::Mixer {
private:
    sdl::Font font;                 /**< Fuente para puntuaciones */
    sdl::Music music;               /**< Música */
    int dificultad;                 /**< Nivel de dificultad */
    int nParejas;                   /**< Número de parejas, en función de la dificultad */
    int nColumnas;                  /**< Número de columnas, en función de la dificultad */
    int nFilas;                     /**< Número de filas, en función de la dificultad */
    sdl::TextureArray *graficos;    /**< Gráficos */
    std::vector<mem::Carta> baraja; /**< Conjunto de parejas de cartas */
    sdl::Point inicio;              /**< Coordenadas de primera carta */
    int estado=0;                   /**< Estado del bucle del juego */
    bool musica;                    /**< Indica si la música se debe reproducir */
    int volumenMus;                 /**< Volumen de la música */
    int carta1, carta2;             /**< Índices de las cartas levantadas por el jugador */
    int intentos;                   /**< Número de intentos, parejas levantadas */
    int aciertos;                   /**< Número de aciertos, parejas levantadas coincidentes */
    sdl::Button *botonsalir;        /**< Botón de salir, activo cuando se completa el tablero */
    sdl::Button *botonabandonar;    /**< Botón de abandonar partida */
    int index;                      /**< Índice de la carta seleccionada por el usuario */

public:
    Memory(sdl::Window& window, sdl::Renderer& renderer);
    ~Memory();
    void SetDificultad(int d) { dificultad = d; }
    int GetDificultad() { return dificultad; }
    void CargaMusica(const std::string file) { music.Load(file.c_str()); music.Play(-1); if(musica) music.Resume(); else music.Pause();}
    void VolumenMas() { volumenMus+=5; if(volumenMus>128) volumenMus=128; music.Volume(volumenMus); }
    void VolumenMenos() { volumenMus-=5; if(volumenMus<0) volumenMus=0; music.Volume(volumenMus); }
    void Init();
    void Events();
    void Update();
    void Render();
    char Run();
    bool SwitchMusica() { musica = !musica; if(musica) music.Resume(); else music.Pause(); return musica; }
    bool Musica() { return musica; }
    char Menu1(int s=-1);
    char Menu2(int s=-1);
    char Menu3(int s=-1);
    char Menu4(int s=-1);
    void Creditos();
};

class MemMenu : public sdl::Menu {
public:
    MemMenu(sdl::Window& win, sdl::Renderer& ren, const std::string& font) : sdl::Menu(win, ren, font) {}
    void Render();
};

class MemCreditos : public sdl::Credits {
public:
    MemCreditos(sdl::Window& win, sdl::Renderer& ren, const std::string& font) : sdl::Credits(win, ren, font) {}
    void Render();
};
#endif // MEM_MEMORY

Init

El método Init establece los valores iniciales de una partida.

Aunque en los niveles fácil y medio no se usen todas las cartas, interesa que no se elijan siempre las mismas. La primera tarea de este método consiste en seleccionar las cartas que se usarán en esta partida.

Para ello creamos un vector de índices de cartas, que desordenamos, de modo que las primeras nParejas se eligen aleatoriamente. Evidentemente, en el nivel difícil se usan todas las cartas.

A continuación se establecen el número de parejas, columnas y filas, en función de la dificultad seleccionada.

Por último creamos la baraja de cartas, y la mezclamos.

void Memory::Init() {
    std::vector<int> orden = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24};
    int n, k;
    // Desordenar, para no elegir siempre las mismas cartas en niveles fácil y medio:
    for(int i=0; i<25; i++) {
        n = sdl::random(25);
        k = orden[n];
        orden[n] = orden[i];
        orden[i] = k;
    }
    estado=0;
    quit = false;
    baraja.clear();
    carta1=carta2=-1;
    intentos=aciertos=0;
    botonsalir->SetActive(false);
    botonabandonar->SetActive(true);
    switch(dificultad) {
        case 0:
            nParejas=10; nColumnas=5;
            break;
        case 1:
            nParejas=24; nColumnas=8;
            break;
        case 2:
            nParejas=50; nColumnas=10;
            break;
    }
    nFilas=(nParejas*2)/nColumnas;
    inicio.X() = (800-50*nColumnas)/2;
    inicio.Y() = (600-50*nFilas)/2;

    // Crear baraja
    for(int i=0; i<nParejas; i++) {
        baraja.push_back(mem::Carta(graficos,orden[i]));
        baraja.push_back(mem::Carta(graficos,orden[i]));
    }
    // Mezclar
    int x;
    mem::Carta t;
    for(int i=0; i<nParejas*2; i++) {
        x=sdl::random(nParejas*2);
        t = baraja[x];
        baraja[x] = baraja[i];
        baraja[i] = t;
    }
}

Events

El juego necesita procesar pulsaciones del ratón para comprobar si se ha pulsado alguno de los botones o sobre alguna de las cartas.

Cuando se detecte un clic sobre una carta se actualiza el valor de index con el índice de la carta correspondiente.

void Memory::Events() {
    int fil, col;

    index=-1;
    while(event.Poll()) {
        switch(event.Type()) {
        case SDL_MOUSEBUTTONDOWN:
            // Verficar si se ha pulsado un botón
            if(botonsalir->Pushed(sdl::Point(event.Button().x, event.Button().y)) ||
               botonabandonar->Pushed(sdl::Point(event.Button().x, event.Button().y))) {
                quit = true;
            } else
            if(event.Button().x > inicio.X() && event.Button().y > inicio.Y() &&
               event.Button().x < inicio.X()+nColumnas*50 && event.Button().y < inicio.Y()+nFilas*50) {
                fil = (event.Button().y - inicio.Y())/50;
                col = (event.Button().x - inicio.X())/50;
                index = fil*nColumnas+col;
            }
            break;
        }
    }
}

Update

El juego pasa por un determinado número de estados, dependiendo de las acciones del usuario:

  1. El jugador no ha levantado ninguna carta. Si levanta una carta no bloqueada será la carta1, se muestra y se pasa al estado 1, en caso contrario, permanece en este estado, 0.
  2. El jugador ha levantado la primera carta. Si levanta otra no bloqueada, que será la carta2 y la muestra. Si es igual a la anterior, es decir, si se ha descubierto una pareja, se incrementa el número de aciertos. Si el número de aciertos es igual al número de parejas, la partida ha terminado y pasamos al estado 3, si no se vuelve al estado 0. Si son diferentes se pasa al estado 2. En cualquier caso se incrementa el número de intentos.
  3. El jugador ha pulsado sobre una carta. Las cartas previamente levantadas son diferentes, ya que no existe otro modo de llegar a este estado, de modo que se vuelven boca abajo. Si la carta no está bloqueada será la carta1, se muestra y pasaremos al estado 1, si está bloqueada pasamos al estado 0.
void Memory::Update() {
    if(index==-1) return;

    switch(estado) {
    case 0:
        if(!baraja[index].Bloqueada()) {
            estado=1;
            carta1 = index;
            baraja[carta1].Mostrar();
        }
        break;
    case 1:
        if(carta1 != index && !baraja[index].Bloqueada()) {
            carta2 = index;
            baraja[carta2].Mostrar();
            if(baraja[carta1] == baraja[carta2]) {
                baraja[carta1].Bloquear();
                baraja[carta2].Bloquear();
                aciertos++;
                if(aciertos == nParejas) {
                    botonsalir->SetActive(true);
                    botonabandonar->SetActive(false);
                }
                estado = 0;
            } else {
                estado = 2;
            }
            intentos++;
        }
        break;
    case 2:
        baraja[carta1].Mostrar(false);
        baraja[carta2].Mostrar(false);
        if(!baraja[index].Bloqueada()) {
            carta1=index;
            baraja[carta1].Mostrar();
            estado = 1;
        } else {
            estado = 0;
        }
        break;
    }
}

Render

Dibuja las cartas, los botones y las puntuaciones.

void Memory::Render() {
    sdl::Point coord;
    std::string line;
    sdl::Color negro("#000000ff");
    sdl::Color fondo("#5a5a78ff");
    sdl::Color blanco("#ffffffff");

    // Renderizado:
    renderer.SetDrawColor(fondo);
    renderer.Clear();
    renderer.SetDrawColor(blanco);
    for(int i=0; i<nParejas*2; i++) {
        coord = sdl::Point(inicio.X()+50*(i%nColumnas), inicio.Y()+50*(i/nColumnas));
        baraja[i].Show(coord);
    }
    // Mostrar puntuaciones:
    coord = sdl::Point(60, 2);;
    line = "Intentos " + std::to_string(intentos) + " Aciertos " + std::to_string(aciertos);
    font.SetOutline(0);
    font.SetSize(45);
    font.RenderUTF8_Blended(line, coord, negro);
    botonsalir->Render();
    botonabandonar->Render();
    renderer.Present();
}

Run

Invoca al método Run de la clase Game.

char Memory::Run() {
    return Game::Run();
}

Los métodos Menu1, Menu2, Menu3 y Menu4 crean cada uno de los menús y los ejecutan.

El método Creditos hace lo mismo con los créditos del juego.

Función main

Finalmente, la función main inicializa las librerías, crea la ventana, el contexto de renderizado y un objeto Memory.

A continuación procesa los menús:

int main( int argc, char * argv[] ) {
    int opt=0, opt2, opt3;

    sdl::SDL2 sdl(SDL_INIT_EVERYTHING, true);
    sdl::Window window ("Memory", 800, 600, (SDL_WindowFlags)(SDL_WINDOW_SHOWN | SDL_WINDOW_BORDERLESS));
    sdl::Renderer renderer(window, SDL_RENDERER_ACCELERATED);

    Memory memory(window, renderer);

    do {
        opt = memory.Menu1();
        switch(opt) {
            case '1':
                memory.SetDificultad(memory.Menu2(memory.GetDificultad())-'1');
                break;
            case '2':
                do {
                    opt2 = memory.Menu3();
                    switch(opt2) {
                    case '1': // Elegir musica
                        opt3 = memory.Menu4();
                        switch(opt3) {
                        case '1':
                            memory.CargaMusica("./music/infinte-melodic-rap-beat-instrumental-205870.mp3");
                            break;
                        case '2':
                            memory.CargaMusica("./music/melodic-techno-03-extended-version-moogify-9867.mp3");
                            break;
                        case '3':
                            memory.CargaMusica("./music/watr-fluid-10149.mp3");
                            break;
                        }
                        break;
                    case '2': // +Volumen
                        memory.VolumenMas();
                        break;
                    case '3': // -Volumen
                        memory.VolumenMenos();
                        break;
                    case '4':
                        memory.SwitchMusica();
                        break;
                    }
                } while(opt2 != '5');
                break;
            case '3':
                memory.Run();
                break;
            case '4': // Créditos
                memory.Creditos();
                break;
        }
    } while(opt!='5');
    return 0;
}

Usar una DLL

Ha llegado el momento en que nuestras clases para encapsular SDL2 empiezan a usar demasiados ficheros y es engorroso incluirlos en cada nuevo ejemplo o juego, de modo que será interesante crear un proyecto separado para nuestra librería de clases de modo que el código del juego quede mucho más ligero.

Por supuesto, esta librería no es definitiva, aún está en fase de desarrollo, algunas clases seguirán cambiando a lo largo del curso, otras se reescribirán completas y se crearán otras nuevas.

Además, con la librería se ha incluido una documentación generada con Doxygen, a modo de ilustración.

El programa del juego asume que el fichero de cabecera "sdldll.h", que se necesita para usar nuestra DLL, se encuentra en una carpeta llamada "sdl2dll" situada en la misma ruta que el proyecto del juego. Hay que tener esto en cuenta si se descomprimen estos ficheros en carpetas con diferentes nombres.

Se puede modificar esa ubicación en las opciones de proyecto "Build options", en la pestaña "Search directories"->"Compiler", modificando la línea "..\sdl2dll", por la correspondiente en tu caso.

No basta con copiar el fichero "sdldll.h" en la carpeta del proyecto del juego, ya que este fichero de cabecera incluye a su vez varios ficheros.

Ejemplo 14

Nombre Fichero Fecha Tamaño Contador Descarga
Juego Memory sdl_memory.zip 2024-10-24 23020474 bytes 38

SDL2 DLL

Nombre Fichero Fecha Tamaño Contador Descarga
Wrapper DLL para SDL sdl2dll.zip 2024-10-24 629670 bytes 38