Analizando las minas explosivas (continuación)

En el post anterior hablé sobre una posible estructura de datos para representar el tablero de este juego. Ahora veamos como hacerle una interfaz gráfica.

La interfaz de la que pienso hablar es de la que yo implementé con la biblioteca de Qt4 para C++.

Lo primero que vamos a crear es un panel de juego, el panel de juego es la ventana en donde queremos dibujar el tablero de juego y programar las interacciones sobre el mismo.

Para crear el panel nos valemos de la clase QWidget definida en Qt que representa una ventana básica la cual puede ser colocada dentro de otra ventana. Para esto entonces creamos una clase a la que llamaremos Game_Panel que deriva de QWidget:

class Game_Panel : public QWidget
{
  Matrix * matrix;
  bool finished;
  //...Operaciones públicas de la clase Game_Panel
};

Como se puede observar la clase contiene como atributos un puntero a un objeto de la clase Matrix (la que definimos en el post anterior) y una variable booleana que permite saber si el juego ya finalizó o no.

En este panel se manejan 3 señales, las señales son un mecanismo que tiene Qt que permite “lanzar avisos al aire”, es decir, se puede emitir cuando ocurra algún evento que quizás interese ser capturado (que otros objetos se enteren que ocurrió) para efectuar alguna acción al respecto desde otra parte.

las señales que se definen acá son:

signals:
  win();
  lost();
  flags_changed();

Entre las operaciones tenemos la de inicialización del panel la cual recibe como parámetros las dimensiones del tablero y la cantidad de minas que tendrá, el prototipo es el que sigue:

Game_Panel(const size_t & w, const size_t & h, const size_t & mines);

Y la implementación da memoria a un objeto de tipo Matrix con los atributos pasados como parámetro y también da un tamaño al panel de las dimensiones de la matriz multiplicado por una escala que viene dada por el tamaño al que se quiera dibujar cada casilla del tablero (en nuestro caso el valor es 20 definido en la macro SCALE).

La segunda operación que vamos a definir es la que dibuja el tablero en el panel, para esto sobreescribimos un método virtual que está en la clase QWidgwet que tiene el siguiente prototipo:

void paintEvent(QPointEvent *);

Éste es un método que es invocado automáticamente al abrir el panel y al hacer un llamado explícito al método repaint() de la ventana se activa una bandera para que Qt sepa que debe mandar a redibujar el panel.n Para la implementación del método de dibujo contamos con un directorio (dentro de nuestro directorio de proyecto) llamado “images” el cual, como se puede intuir, contiene las imágenes que utilizamos en el juego. Para el tablero están las imágenes de cada número mostrado al destapar casillas nombradas como su número con formato png (ej: 1.png contiene el valor de casilla 1), la que al destapar se muestra vacía se llama 0.png, la que representa una casilla tapada se llama Cuadro.png, la que representa una casilla tapada con bandera se llama Bandera.png, entonces la implementación del método de dibujo es como sigue:

void paintEvent(QPaintEvent *) // Como no usare el objeto QPaintEvent dentro lo dejo sin nombre para que el compilador no arroje advertencias de variable nunca usada
{
  QPainter painter(this);
  // Iterara sobre la matriz
    // Por cada paso de iteración
    QPixmap image;
    //Verificamos el estado de la casilla actual, s = 'c' en images cargamos la imagen Cuadro.png, si es bandera cargamos la imagen Bandera.png
    //Si no es ninguno de los casos anteriores, entonces significa que la casilla está destapada, así que verificamos el caracter y cargamos la imagen correspondiente al valor de la casilla (del 0 al 9 o mina en caso de ser *)
    //Luego mandar a pintar la imagen en la posición (i, j) actual de la matriz multiplicada por la escala para que se desplace el dibujo y no se monten las imágenes encima de las otras
  painter.drawPixmap(j * SCALE, i * SCALE, SCALE, SCALE, image);
  //Fin de la iteración sobre la matriz
}

Se instancia un objeto de tipo QPainter que es la clase que se encarga de hacer dibujos en el panel (más detalles en otros post futuros). Luego usamos un objeto de tipo QPixmap (usamos este tipo porque es al que yo puedo mandar a dibujar del tamaño que yo quiera en el momento de dibujarlo). El método que me permite cargar una imagen en el objeto QPixmap es load (ej: image.load(“images/1.png”)) El cual recibe como parámetro la ruta de la imagen. El método drawPixmap de la clase QPainter recibe como parámetros la posición de la imagen (x, y), las dimensiones de la imagen (w, h) y el objeto de tipo QPixmap a dibujar, la razón por la que se multiplica por la escala es porque la imagen se va a dibujar de tamaño 20 x 20 y los valores (i, j) representan posiciones en la matriz por lo que para i = 0 y j = 0, la imagen se va a dibujar en la posición (0, 0) del panel y para la posición (1, 0) de la matriz se va a dibujar en la posición (0, 20) del panel para respetar el ancho de tamaño 20 de la imagen anterior).

Ahora vamos a definir la operación de interacción del jugador con el tablero, para eso sobreescribimos un método virtual de la clase QWidget llamado mousePressEvent el cual es invocado automaticamente al recibir cualquier señal de click del mouse, el prototipo es como sigue:

void mousePressEvent(QMouseEvent *);

En este caso si nos interesa usar el objeto de tipo QMouseEvent, este objeto que lleva como parámetro contiene información como las coordenadas del panel en donde se hizo el click, el botón con el cual se hizo click, etc. y esto nos será realmente útil. La implementación del método es como sigue:

void mousePressevent(QMouseEvent * evt)
{
  // Si la variable finished esta en true retorno
  // Obtengo la posicion del panel en donde se hizo click
  QPoint p = evt->pos();
  // Convierto la posicion dada a posiciones en la matriz, si al dibujar multiplico por la escala, al dividir y truncar el resultado a un numero entero (OJO no es redondear) me lleva a la posicion en la matriz.
  const size_t j = size_t (p.x() / SCALE);
  const size_t i = size_t (p.y() / SCALE);
  // Pregunto si el boton fue el derecho
  if (evt->button() == Qt::RightButton)
    {
      //Hago el llamado de la operación flag de la clase Matrix en la posición (i, j)
      //Emito la señal de que las banderas cambiaron (emit flags_changed()).
    }
// Si no es el botón derecho, pregunto si es el botón izquierdo
  else if (evt->button() == Qt::LeftButton)
    {
      // Aqui hay dos casos
      // Si la pocisión al que le hace click el usuario tiene una mina y no tiene bandera entonces se invoca a la operacion discover_all_mines de la clase Matrix, coloca el juego como terminado (finished = true) y emite la señal de perdió (emit lost()).
      // En caso contrario mando a destapar la casilla invocando al método discover de la clase Matrix e igualo mi variable de finalización a lo que retorna la operación are_uncover_all de la clase Matrix, si queda en true entonces emito señal de ganar (emit win()) sino sigo mi juego normal
    }
}

La otra operación importante de mencionar es reinit la cual libera la matriz existente, pide memoria para una nueva, coloca a finished en false y llama al metodo repaint.

Como podemos haber visto en otras implementaciones de este juego, existe una cara arriba de nuestro panel la cual, clásicamente, tiene unos lentes de sol, cuando el jugador gana ella se pone feliz, cuando el jugador pierde ella llora o se pone triste, y al hacerle click el juego se reinicia. Veamos como se implementa.

Definiremos un QWidget para pintar la cara al que llamaremos Smile (lo siento, fue lo que se me ocurrió) que tiene como único atributo un objeto de tipo QPixmap.

class Smile : public QWidget
{
  QPixmap image;
  //...Operaciones públicas y privadas de la clase Smile
};

En esta clase tambien se define una se&ntildeal para emitir a la que llamaremos smile_clicked la cual vamos a emitir al momento de hacerle click. También se definen dos tipos de operaciones especiales llamadas slots, los slots son unos métodos especiales que pueden o no recibir parámetros pero siempre retornan void. El caso especial de estos métodos es que ellos pueden ser conectados con alguna señal para que el sistema lo invoque automáticamente una vez que una señal es emitida pero también pueden ser invocados como los métodos comunes (nombre_objeto.metodo_slot()).

Las señales y los slots son los siguientes:

signals:
  void smile_clicked();
public slots:
  void sad();
  void happy();

Aquí definimos una operacion privada llamda normal la cual internamente manda a cargar en image la imagen de la cara con lentes almacenada en el directorio images (image.load(“images/lentes.png)) y repinta el panel. En el constructor lo primero que hacemos es invocar a la funcion normal() para que se inicie con la imagen con lentes.

Análogamente los métodos de tipo slot sad y happy hacen lo mismo que normal pero con las imágenes triste.png y feliz.png respectivamente.

El médoto paintEvent manda a pintar la imagen en la posición (0, 0) con su tamaño (que en este caso es 40) y el método mousePressevent lo que hace es que invoca al método normal nuevamente y emite la señal smile_clicked().

Ahora si vamos a unir todo en un solo esqueleto de juego.

Para eso crearemos una clase llamada Game_Frame basada tambien en QWidget la cual tiene como atributos un Game_Panel, un Smile y una etiqueta que esta siempre mostrando cuantas banderas se han colocado sobre el maximo que se pueden colocar. En este ejemplo no hay un módulo que permita cambiar el tamaño del tablero y la cantidad de minas dinámicamente por lo que también son atributos de esta clase las dimensiones del tablero y la cantidad de minas que va a tener.

class Game_Frame : public QWidget
{
  Game_Panel panel;
  Smile smile;
  QLabel lbl_flags;

    void init_gui();

    size_t h;
    size_t w;
    size_t m;
};

Vamos a tener un a operación privada llamada init_gui() que es la que se va a encargar de armar el esqueleto del juego organizadamente y se implementa como sigue:

void init_gui()
{
  QVBoxLayout * vlayout = new QVBoxLayout;
  QHBoxLayout * hlayout1 = new QHBoxLayout;
  hlayout1->addWidget(&smile);
  vlayout->addLayout(hlayout1);
  QHBoxLayout * hlayout2 = new QHBoxLayout;
  hlayout2->addWidget(&panel);
  vlayout->addLayout(hlayout2, 1);
  lbl_flags.setFixedSize(100, 20);
  QHBoxLayout * hlayout3 = new QHBoxLayout;
  hlayout3->addWidget(&lbl_flags, 1);
  vlayout->addLayout(hlayout3, Qt::AlignCenter);
  setLayout(vlayout);
}

Ahora veamos los detalles.

Un Layout es un tipo que modela marcos que permiten la organización de Widgets (en general) dentro de otro Widget (los botones, radiobuttons, checkboxes, etc. son basados en QWidget). Aquí utilizaremos dos tipos, horizontales y verticales, ellos permiten añadirle widgets y otros layouts y se añadiran en el orden en que se programen. Ejemplo:

QLabel * label1 = new QLabel("label 1");
QLabel * label2 = new QLabel("label 2");
QLabel * label3 = new QLabel("label 3");

Si los anadimos en un QHBoxLayout (layout horizontal) seria algo como esto:

QHBoxLayout layout;

layout.addWidget(label1);
layout.addWidget(label2);
layout.addWidget(label3);

El resultado es este:

+-------+
|label 1|
|label 2|
|label 3|
+-------+

Si por el contrario es vertical sería así:

QVBoxLayout layout;

layout.addWidget(label1);
layout.addWidget(label2);
layout.addWidget(label3);

El resultado es este:

+-----------------------+
|label 1 label 2 label 3|
+-----------------------+

Nota: Los marcos realmente no se ven, los dibujé como referencia solamente para que se entienda.

Entonces en nuestro caso se instancia uno vertical que es el principal y un horizontal por cada elemento del juego, al primer horizontal le añadimos la cara y este es añadido en el vertical, al segundo horizontal le añadimos el panel y este es añadido al vertical y al tercer horizontal le añadimos la etiqueta y este es añadido al vertical.

En esta clase definimos dos slots, uno para reiniciar el juego cuando el objeto smile emita su señal de click y otro de cambiar la información de las minas en la etiqueta cuando el panel emita la señal flags_changed.

public slots:
  void reinit_game();
  void change_mines_info();

Las implementaciones son así:

void change_mines_info()
{
  //Asignar a la etiqueta lbl_flags el texto "Minas: " y concatenerle la cantidad de banderas que tiene le matriz actualmente / la cantidad de minas totales.
}

void reinit_game()
{
  // Invocar al metodo reinit del panel con parametros w, h, m e invocar a change_mines_info para que se reinicie la etiqueta.
}

El constructor se encarga entonces de invocar al metoro init_gui() y de establecer las conexiones entre signals y slots, la implementación es la siguiente:

Game_Frame()
{
  init_gui();
  connect(&panel, SIGNAL(win()), &smile, SLOT(happy()));
  connect(&panel, SIGNAL(lost()), &smile, SLOT(sad()));
  connect(&smile, SIGNAL(smile_clicked()), this, SLOT(reinit_game()));
  connect(&panel, SIGNAL(flags_changed()), this, SLOT(change_mines_info()));
}

connect es la función que permite conectar un signal con un slot, una vez que se establece dicha conexión, al emitirse una señal conectada, se ejecutará la acción programada en el slot que se le conectó a dicha señal. los parámetros que recibe connect son: puntero al objeto que emite la señal, la macro SIGNAL con el nombre se la señal que se quiere, puntero al objeto que esta interesado en captar la señal, la macro SLOT con el nombre del slot que nos interesa ejecutar al ocurrir la señal.

Ya teniendo esto, tenemos el juego funcional.

P.S.: Las implementaciones de los códigos mostradas acá son básicas, más que todo para entendimiento. Los detalles de la implementación están en el código fuente disponible para revisión y/o descarga en: https://github.com/R3mmurd/Mines

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Posted in C++, Programación, Qt, Videojuegos Tagged with: , , ,

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*