Fundamentos de Qt: Implementación de un Juego de la Serpiente

  1. Conexión de Señales y Slots

En Qt, la comunicación entre objetos se realiza mediante señales y slots. Existen varias formas de establecer estas conexiones:

Convención de Nombres Automática

Qt Creator puede conectar automáticamente un widget a un slot si se sigue la nomenclatura on_<nombreObjeto>_<señal>.

void MainWindow::on_btnLimpiar_clicked() {
    // Lógica personalizada al hacer clic
}

Editor de Qt Creator

Se puede utilizar la interfaz gráfica de Qt Creator, navegando a la pestaña de Señales/Slots para vincular visualmente el emisor y el receptor.

Sintaxis Moderna de Conexión

La forma recomendada en C++11 y posteriores utiliza punteros a miembros, lo que permite verificación en tiempo de compilación.

QPushButton *boton = new QPushButton("Acción", this);
boton->resize(100, 50);
boton->move(50, 50);

// Conexión usando sintaxis de puntero a miembro
connect(boton, &QPushButton::clicked, this, &MainWindow::ejecutarAccion);

// Para desconectar:
disconnect(boton, &QPushButton::clicked, this, &MainWindow::ejecutarAccion);

Uso de Expresiones Lambda

Para lógica corta, se puedeen usar lambdas directamente en la conexión.

connect(boton, &QPushButton::clicked, this, [this]() {
    // Código a ejecutar
});
  1. Menejo de Temporizadores (QTimer)

QTimer emite la señal timeout() a intervalos regulares.

Reloj en Tiempo Real

QTimer *relojTimer = new QTimer(this);
connect(relojTimer, &QTimer::timeout, this, &MainWindow::actualizarHora);
relojTimer->start(1000);

void MainWindow::actualizarHora() {
    QDateTime ahora = QDateTime::currentDateTime();
    ui->txtHora->setText(ahora.toString("dd/MM/yyyy HH:mm:ss"));
}

Cronómetro Interactivo

QTimer *cronoTimer = new QTimer(this);
connect(cronoTimer, &QTimer::timeout, this, &MainWindow::incrementarCrono);
cronoTimer->setInterval(1000);

void MainWindow::incrementarCrono() {
    int valor = ui->lcdNumeros->value();
    ui->lcdNumeros->display(valor + 1);
}

void MainWindow::on_btnControl_clicked() {
    if (!cronoTimer->isActive()) {
        cronoTimer->start();
        ui->btnControl->setText("Pausar");
    } else {
        cronoTimer->stop();
        ui->btnControl->setText("Reanudar");
    }
}
  1. Eventos de Teclado y Ratón

Para capturar entradas, se deben sobrescribir los métodos de eventos en la ventana principal.

// Evento de presión de tecla
void MainWindow::keyPressEvent(QKeyEvent *evento) {
    if (evento->key() == Qt::Key_W) {
        moverArriba();
    }
}

void MainWindow::moverArriba() {
    if (ui->widgetJugador->y() > 0) {
        ui->widgetJugador->move(ui->widgetJugador->x(), ui->widgetJugador->y() - 20);
    }
}

// Evento de clic del ratón
void MainWindow::mousePressEvent(QMouseEvent *evento) {
    if (evento->button() == Qt::LeftButton) {
        // Acción para clic izquierdo
    }
}
  1. Diálogos con QMessageBox

Qt proporciona diálogos estándar para interactuar con el usuario. Se puede usar <br> en el texto para saltos de línea.

// Diálogo de información
QMessageBox::information(this, "Título", "Mensaje de texto");

// Diálogo de pregunta con captura de respuesta
QMessageBox::StandardButton respuesta = QMessageBox::question(
    this, "Confirmación", "¿Desea continuar?", 
    QMessageBox::Yes | QMessageBox::No
);

if (respuesta == QMessageBox::Yes) {
    // El usuario presionó Sí
}
  1. Renderizado Gráfico con QPainter

Para dibujar imágenes o formas, se utiliza QPainter dentro del evento paintEvent. Es necesario añadir las imágenes al archivo de recursos (.qrc).

void MainWindow::paintEvent(QPaintEvent *evento) {
    Q_UNUSED(evento);
    QPainter pincel(this);
    
    QPixmap imagenFondo;
    imagenFondo.load(":/imagenes/fondo.png");
    pincel.drawPixmap(0, 0, width(), height(), imagenFondo);
    
    update(); // Solicitar redibujado si es necesario
}
  1. Generación de Números Aleatorios

Utilizando la clase QRandomGenerator (requiere incluir <QRandomGenerator>).

int numeroAleatorio = QRandomGenerator::global()->bounded(1, 100); // Entre 1 y 99
  1. Proyecto Práctico: Juego de la Serpiente

A continuación se presenta la implementación de un juego de la serpiente. El juego permite moverse, acelerar, pausar y cambiar el fondo. Al chocar con el propio cuerpo, se pregunta si se desea reiniciar.

Estructura de la Serpiente (SnakeEntity.h)

Se utiliza QPoint para manejar las coordenadas de manera más eficiente y nativa en Qt.

#ifndef SNAKEENTITY_H
#define SNAKEENTITY_H

#include <QPoint>
#include <QVector>

class SnakeEntity {
public:
    QPoint cabeza;
    QPoint cola;
    QVector<QPoint> cuerpo;
    char direccion;

    SnakeEntity();
    void reiniciar();
};

#endif // SNAKEENTITY_H

Implementación de la Serpiente (SnakeEntity.cpp)

#include "SnakeEntity.h"

SnakeEntity::SnakeEntity() {
    reiniciar();
}

void SnakeEntity::reiniciar() {
    cabeza = QPoint(400, 200);
    cuerpo.clear();
    cuerpo.append(QPoint(370, 200));
    cuerpo.append(QPoint(340, 200));
    cola = cuerpo.last();
    direccion = 'r';
}

Encabezado de la Ventana Principal (MainWindow.h)

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTimer>
#include <QPushButton>
#include <QPainter>
#include <QKeyEvent>
#include <QRandomGenerator>
#include <QMessageBox>
#include "SnakeEntity.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

protected:
    void paintEvent(QPaintEvent *event) override;
    void keyPressEvent(QKeyEvent *event) override;

private slots:
    void iniciarJuego();
    void cicloDeJuego();
    void configurarMenu();

private:
    Ui::MainWindow *ui;
    SnakeEntity serpiente;
    QTimer *motorTiempo;
    QPushButton *btnInicio;
    QPoint posComida;
    bool juegoActivo;
    bool modoRapido;
    int indiceFondo;

    void generarComida();
    void moverSerpiente();
    void detectarColisiones();
};

#endif // MAINWINDOW_H

Lógica Principal (MainWindow.cpp)

#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QMenuBar>
#include <QMenu>
#include <QAction>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow),
      motorTiempo(new QTimer(this)), btnInicio(nullptr),
      juegoActivo(false), modoRapido(false), indiceFondo(0) {
    ui->setupUi(this);
    setFixedSize(1000, 600);
    configurarMenu();

    btnInicio = new QPushButton("Iniciar Juego", this);
    btnInicio->setGeometry(400, 250, 200, 60);
    connect(btnInicio, &QPushButton::clicked, this, &MainWindow::iniciarJuego);

    connect(motorTiempo, &QTimer::timeout, this, &MainWindow::cicloDeJuego);
}

MainWindow::~MainWindow() {
    delete ui;
}

void MainWindow::configurarMenu() {
    QMenuBar *barra = menuBar();
    QMenu *menuOpciones = barra->addMenu("Opciones");

    QAction *actSalir = menuOpciones->addAction("Salir");
    connect(actSalir, &QAction::triggered, this, &QWidget::close);

    QAction *actFondo = menuOpciones->addAction("Cambiar Fondo");
    connect(actFondo, &QAction::triggered, this, [this]() {
        indiceFondo = (indiceFondo == 0) ? 1 : 0;
        update();
    });

    QAction *actAyuda = menuOpciones->addAction("Controles");
    connect(actAyuda, &QAction::triggered, this, [this]() {
        QMessageBox::information(this, "Controles", 
            "W/A/S/D: Mover<br>Espacio: Pausar<br>Shift: Acelerar");
    });
}

void MainWindow::paintEvent(QPaintEvent *event) {
    Q_UNUSED(event);
    QPainter pincel(this);
    
    QPixmap fondo;
    fondo.load(indiceFondo == 0 ? ":/res/fondo1.png" : ":/res/fondo2.png");
    pincel.drawPixmap(0, 0, width(), height(), fondo);

    if (juegoActivo) {
        QPixmap imgComida;
        imgComida.load(":/res/comida.png");
        pincel.drawPixmap(posComida.x(), posComida.y(), 30, 30, imgComida);

        QPixmap imgCabeza, imgCuerpo;
        imgCuerpo.load(":/res/cuerpo.png");
        
        QString direccionStr = QString(serpiente.direccion);
        imgCabeza.load(QString(":/res/cabeza_%1.png").arg(direccionStr));

        pincel.drawPixmap(serpiente.cabeza.x(), serpiente.cabeza.y(), 30, 30, imgCabeza);
        for (const QPoint &segmento : serpiente.cuerpo) {
            pincel.drawPixmap(segmento.x(), segmento.y(), 30, 30, imgCuerpo);
        }
    }
}

void MainWindow::iniciarJuego() {
    if (btnInicio) {
        delete btnInicio;
        btnInicio = nullptr;
    }
    serpiente.reiniciar();
    generarComida();
    juegoActivo = true;
    motorTiempo->start(200);
    update();
}

void MainWindow::generarComida() {
    bool posicionValida = false;
    while (!posicionValida) {
        int x = QRandomGenerator::global()->bounded(0, 970);
        int y = QRandomGenerator::global()->bounded(0, 570);
        
        x = (x / 30) * 30; 
        y = (y / 30) * 30;
        
        QPoint candidata(x, y);
        if (candidata == serpiente.cabeza || serpiente.cuerpo.contains(candidata)) {
            continue;
        }
        posComida = candidata;
        posicionValida = true;
    }
}

void MainWindow::cicloDeJuego() {
    if (!juegoActivo) return;
    moverSerpiente();
    detectarColisiones();
    update();
}

void MainWindow::moverSerpiente() {
    serpiente.cuerpo.prepend(serpiente.cabeza);
    
    int paso = 30;
    if (serpiente.direccion == 'r') serpiente.cabeza.rx() += paso;
    else if (serpiente.direccion == 'l') serpiente.cabeza.rx() -= paso;
    else if (serpiente.direccion == 'u') serpiente.cabeza.ry() -= paso;
    else if (serpiente.direccion == 'd') serpiente.cabeza.ry() += paso;

    // Efecto túnel (cruzar los bordes)
    if (serpiente.cabeza.x() < 0) serpiente.cabeza.rx() = 970;
    if (serpiente.cabeza.x() > 970) serpiente.cabeza.rx() = 0;
    if (serpiente.cabeza.y() < 0) serpiente.cabeza.ry() = 570;
    if (serpiente.cabeza.y() > 570) serpiente.cabeza.ry() = 0;

    // Verificar si consumió la comida
    if (serpiente.cabeza == posComida) {
        generarComida();
    } else {
        serpiente.cuerpo.removeLast();
    }
    serpiente.cola = serpiente.cuerpo.last();
}

void MainWindow::detectarColisiones() {
    if (serpiente.cuerpo.contains(serpiente.cabeza)) {
        juegoActivo = false;
        motorTiempo->stop();
        
        QMessageBox::StandardButton resp = QMessageBox::question(
            this, "Game Over", "¡Te has chocado! ¿Reiniciar?", 
            QMessageBox::Yes | QMessageBox::No);
            
        if (resp == QMessageBox::Yes) {
            iniciarJuego();
        } else {
            QApplication::quit();
        }
    }
}

void MainWindow::keyPressEvent(QKeyEvent *event) {
    if (event->key() == Qt::Key_Space) {
        if (juegoActivo) {
            juegoActivo = false;
            motorTiempo->stop();
        } else if (btnInicio == nullptr) {
            juegoActivo = true;
            motorTiempo->start(modoRapido ? 80 : 200);
        }
        return;
    }

    if (event->key() == Qt::Key_Shift) {
        modoRapido = !modoRapido;
        motorTiempo->setInterval(modoRapido ? 80 : 200);
        return;
    }

    if (!juegoActivo) return;

    char nuevaDir = serpiente.direccion;
    if (event->key() == Qt::Key_W && serpiente.direccion != 'd') nuevaDir = 'u';
    else if (event->key() == Qt::Key_S && serpiente.direccion != 'u') nuevaDir = 'd';
    else if (event->key() == Qt::Key_A && serpiente.direccion != 'r') nuevaDir = 'l';
    else if (event->key() == Qt::Key_D && serpiente.direccion != 'l') nuevaDir = 'r';
    
    serpiente.direccion = nuevaDir;
}

Punto de Entrada (main.cpp)

#include "MainWindow.h"
#include <QApplication>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    MainWindow ventana;
    ventana.show();
    return app.exec();
}

Etiquetas: Qt C++ QTimer QPainter QRandomGenerator

Publicado el 6-25 17:50