Bienvenidos sean a este post, hoy veremos un concepto fundamental en programacion.
Hasta ahora fuimos viendo las distintas posibilidades de como aplicar los mecanismos de OOP en nuestro juego pero hoy veremos como aplicarlas todas juntas para crear clases reutilizables.
El proyecto que usaremos es el que estamos modificando post a post pero sino lo poseen les dejo un link para descargarlo:
Descarguen el archivo y extraigan el contenido en el PC. Lo unico que deben hacer es revincular a SDL y SDL_Image. Para ello, les recomiendo este post donde comento como hacerlo para SDL y este otro post donde hago lo mismo para SDL_Image. Una vez que este todo funcionando correctamente, podemos continuar. Nuestro primer paso sera crear una nueva clase llamada CargadorParams. Para ello, debemos hacer click con el boton derecho sobre el programa, elegir Agregar y luego Clase. Y a este le pasaremos el nombre anterior y le damos Ok para crearla. Con nuestra nueva clase creada, procedamos a modificar el codigo de CargadorParams.h de la siguiente manera:
CargadorParams.h
#pragma once
#ifndef __CargadorParams__
#define __CargadorParams__
#include <string>
class CargadorParams
{
public:
CargadorParams(float, float, float, float, std::string);
float getX() const;
float getY() const;
float getAncho() const;
float getAlto() const;
std::string getIdTextura() const;
private:
float m_x;
float m_y;
float m_ancho;
float m_alto;
std::string m_idTextura;
};
#endif // !__CargadorParams__
En esta clase, ahora tendremos las variables de la clase ObjetoJuego para ser manipuladas por esta nueva clase. Pero son solo declaraciones, pasemos a CargadorParams.cpp y modifiquemos el codigo existente con el siguiente:
CargadorParams.cpp
#include "CargadorParams.h"
CargadorParams::CargadorParams(float x, float y,
float ancho, float alto, std::string id):
m_x(x), m_y(y), m_ancho(ancho), m_alto(alto), m_idTextura(id) {}
float CargadorParams::getX() const { return m_x; }
float CargadorParams::getY() const { return m_y; }
float CargadorParams::getAncho() const { return m_ancho; }
float CargadorParams::getAlto() const { return m_alto; }
std::string CargadorParams::getIdTextura() const { return m_idTextura; }
El constructor se encargara de recibir e iniciar a las variables miembro por medio de los datos recibidos, luego por medio de funciones get obtendremos cada uno de sus valores asignados. Lo siguiente sera modificar a ObjetoJuego.h de la siguiente manera:
ObjetoJuego.h
#pragma once
#ifndef __ObjetoJuego__
#define __ObjetoJuego__
#include <SDL3/SDL.h>
#include <string>
#include "ManejarTexturas.h"
#include "CargadorParams.h"
class ObjetoJuego
{
public:
virtual void dibujar() = 0;
virtual void actualizar() = 0;
virtual void limpiar() = 0;
protected:
ObjetoJuego(const CargadorParams*);
virtual ~ObjetoJuego();
};
#endif //! defined(__ObjetoJuego__)
Lo primero que haremos sera agregar a la clase creada anteriormente. Seguimos manteriendo a los prototipos como abstractos pero eliminamos a la funcion cargar, esto es asi porque vamos a utilizar una clase para eso. Tambien hemos removido de dibujar al parametro de tipo SDL_Renderer porque vamos a hacerlo trabajar de forma distinta pero seguimos incluyendo la libreria de SDL porque mas adelante la necesitaremos. Luego tenemos el prototipo del constructor y el destructor en la parte protegida, y como tenemos a las variables en la otra clase procedemos a eliminarlas. Nuestro siguiente paso sera modificar a ObjetoJuego.cpp de la siguiente manera:
ObjetoJuego.cpp
#include "ObjetoJuego.h"
ObjetoJuego::ObjetoJuego(const CargadorParams* pParams){}
ObjetoJuego::~ObjetoJuego(){}
El constructor recibe un parametro de tipo CargadorParams, la clase anterior, ya que esta sera la encargada de cargar los parametros y reemplaza a cargar. El destructor por el momento solo sera en blanco. Nuestra siguiente modificacion sera transformar a la clase Juego en Singleton (tal como hicimos con el ManejarTexturas), para ello debemos crear una funcion de instanciar en la parte publica del archivo de encabezado (Juego.h):
static Juego* instanciar();
En este caso, solo agregamos un prototipo. Antes de pasar a la definicion de esta agregaremos estas lineas en la parte privada de la clase:
Juego();
static Juego* e_pInstanciar;
Agregamos el constructor en la parte privada y la variable que usaremos para manejar las instancias, tal como lo hicimos en ManejarTexturas. Con todo esto, pasemos a Juego.cpp y agregaremos las siguientes lineas:
Juego* Juego::instanciar() {
if (e_pInstanciar == 0) {
e_pInstanciar = new Juego();
return e_pInstanciar;
}
return e_pInstanciar;
}
Juego::Juego() {}
Juego* Juego::e_pInstanciar = 0;
Tal como mencionamos anteriormente, esto es muy similar a cuando trabajamos con ManejarTexturas, dado que verifica si la instancia es igual a cero, es decir no esta creada, y si esto es verdadero crea un nuevo objeto dentro de esta variable, y lo devuelve, en caso de no cumplirse significa que el objeto ya esta creado e instanciado por ende lo devuelve directamente. Luego tenemos la definicion de nuestro constructor, y como no necesitamos nada por el momento lo dejamos vacio. Por ultimo, iniciamos a la variable e_pInstanciar porque esto debe hacerse obligatoriamente al ser estatico. Antes de hacer los ultimos cambios debemos volver a Juego.h, aqui debemos eliminar o comentar el constructor predeterminado de la parte publica. Luego, definiremos un typedef por fuera de ifndef, tal como hicimos en este post. La linea que debemos agregar es la siguiente:
typedef Juego Eljuego;
Por ultimo, vamos a agregar una nueva funcion encargada de devolver la renderizacion y para ello agregaremos el siguiente prototipo en la parte publica:
SDL_Renderer* getRenderer() const;
Y volvamos a Juego.cpp donde definiremos esta nueva funcion:
SDL_Renderer* Juego::getRenderer() const { return m_pRenderer; }
Este simplemente nos devolvera el puntero utilizado para la renderizacion. Todavia nos faltan algunos cambios antes de probar nuestro juego, para ello debemos ir a main.cpp y modificaremos el codigo actual por el siguiente:
#include "Juego.h"
#include <iostream>
int main(int argc, char* argv[]) {
std::cout << "Iniciando el juego..." << std::endl;
if (Eljuego::instanciar()->iniciar("Capitulo 1", 640, 480, false)) {
std::cout << "Inicio Exitoso" << std::endl;
while (Eljuego::instanciar()->corriendo()) {
Eljuego::instanciar()->manejaEventos();
Eljuego::instanciar()->actualizar();
Eljuego::instanciar()->renderizar();
SDL_Delay(10);
}
}
else {
std::cout << "Fallo el inicio..." << std::endl;
std::cout << SDL_GetError() << std::endl;
return 1;
}
std::cout << "Cerrando el juego..." << std::endl;
Eljuego::instanciar()->limpiar();
return 0;
}
Como vemos se ha modificado de forma considerable pero no muy distinto de como vinimos trabajando hasta ahora. El primer cambio importante es el condicional que verifica si el instanciar de iniciar funciono correctamente. En caso de ser verdadero, procede a notificarlo y luego mediante un while que verifica que el estado sea corriendo, m_bCorriendo igual a true, y en el bloque llamaremos a las funciones para manejar los eventos sobre la ventana, sobre como actualizarlo y por ultimo el renderizado en la ventana. Luego tenemos el pequeño delay o retardo, pero en caso de no haber podido crear el juego nos notifica cual fue el error y devuelve el valor 1. Por fuera de esta condicional tenemos una accion que nos notifica que se cerro el juego y procede a salir, esto sera en el caso de que salgamos el bucle y por ultimo devolvemos 0 para indicar que todo salio bien.
Dado que al hacer abstracta a ObjetoJuego nos hemos quedado con una clase «vacia». Lo decimos asi porque si bien tiene muchas funciones abstractas pero algunas no lo son, como el constructor y destructor. Por lo que deben quedar en el archivo .cpp. Con esta clase base, lo siguiente sera crear una clase que nos permita trabajar con los objetos y que sea derivada de esta. Para ello debemos agregar una nueva clase a la cual llamaremos SDLObjetoJuego, una vez generada iremos a su archivo de encabezado y agregaremos el siguiente codigo:
SDLObjetoJuego.h
#pragma once
#ifndef __SDLObjetoJuego__
#define __SDLObjetoJuego__
#include <string>
#include "ObjetoJuego.h"
#include "CargadorParams.h"
#include "ManejarTexturas.h"
class SDLObjetoJuego : public ObjetoJuego
{
public:
SDLObjetoJuego(const CargadorParams*);
virtual void dibujar();
virtual void actualizar();
virtual void limpiar();
protected:
float m_x;
float m_y;
float m_ancho;
float m_alto;
int m_filaActual;
int m_frameActual;
std::string m_idTextura;
};
#endif // !__SDLObjetoJuego__
Nota:
Al ser una clase que haremos maestra debemos usar el chequeador de si esta definida o no para no tener duplicados en memoria.
En esta clase declaramos los prototipos en la parte publica, el constructor con el CargadorParams para recibir los valores de nuestros parametros. Tenemos los prototipos de las otras funciones que vinimos usando hasta ahora. En la parte protegida, tenemos las mismas variables que vinimos manejando hasta ahora inclusive las de animaciones. Esta va a ser la base de nuestra clase reutilizable pero todavia nos falta un par de detalles mas. Nuestro siguiente paso sera agregar las siguientes definiciones de nuestros prototipos en SDLObjetoJuego.cpp:
SDLObjetoJuego.cpp
#include "SDLObjetoJuego.h"
#include "Juego.h"
SDLObjetoJuego::SDLObjetoJuego(const CargadorParams* pParams) :
ObjetoJuego(pParams) {
m_x = pParams->getX();
m_y = pParams->getY();
m_ancho = pParams->getAncho();
m_alto = pParams->getAlto();
m_idTextura = pParams->getIdTextura();
m_frameActual = 1;
m_filaActual = 1;
}
void SDLObjetoJuego::dibujar() {
ManejarTexturas::instanciar()->dibujar_frame(m_idTextura,
m_x, m_y, m_ancho, m_alto, m_filaActual, m_frameActual,
Eljuego::instanciar()->getRenderer());
}
void SDLObjetoJuego::actualizar() {}
void SDLObjetoJuego::limpiar() {}
Pasemos a hablar sobre la definicion del primero de nuestros prototipos como es el constructor:
SDLObjetoJuego::SDLObjetoJuego(const CargadorParams* pParams):
ObjetoJuego(pParams) {
m_x = pParams->getX();
m_y = pParams->getY();
m_ancho = pParams->getAncho();
m_alto = pParams->getAlto();
m_idTextura = pParams->getIdTextura();
m_frameActual = 1;
m_filaActual = 1;
}
En esta primera definicion recibiremos un objeto de tipo CargadorParams y este lo pasaremos al constructor de ObjetoJuego. Lo siguiente sera definir cada una de las variables en la parte privada de la clase y para esto usamos a cada uno de los get que definimos en CargadorParams, salvo m_cuadroActual y m_filaActual que las definiremos nosotros, tal como hicimos hasta ahora. Pasemos a la siguiente funcion:
void SDLObjetoJuego::dibujar() {
ManejarTexturas::instanciar()->dibujar_frame(m_idTextura,
m_x, m_y, m_ancho, m_alto, m_filaActual, m_frameActual,
Eljuego::instanciar()->getRenderer());
}
Definimos a nuestra funcion dibujar, en la cual mediante instanciar de ManejarTexturas enviaremos todos los datos a la funcion dibujar_frame de esta clase. Y entre los datos que enviamos esta el renderer de la clase Juego, Como nuestra clase base es abstracta, debemos definir todas las funciones de la mismas y por este motivo solamente creamos dos funciones vacias. Con estas modificaciones realizadas podemos modificar a Jugador y Enemigo para que sean herederas de la nueva clase. Para ello, iremos a modificar a Jugador.h y cambiaremos su codigo de la siguiente manera:
Jugador.h
#pragma once
#ifndef __Jugador__
#define __Jugador__
#include "SDLObjetoJuego.h"
#include "CargadorParams.h"
class Jugador : public SDLObjetoJuego
{
public:
Jugador(const CargadorParams*);
void dibujar();
void actualizar();
void limpiar();
};
#endif /* defined(__Jugador__)*/
En realidad no solo cambiamos de quien es la clase base sino tambien a la funcion cargar que la quitamos y usamos un constructor donde pasamos los parametros de tipo CargadorParams, modfiquemos a Enemigo.h de la misma manera:
Enemigo.h
#pragma once
#ifndef __Enemigo__
#define __Enemigo__
#include "SDLObjetoJuego.h"
#include "CargadorParams.h"
class Enemigo : public SDLObjetoJuego
{
public:
Enemigo(const CargadorParams*);
void dibujar();
void actualizar();
void limpiar();
};
#endif // !__Enemigo__
Es exactamente lo mismo pero con su propio constructor, nuestro siguiente paso sera definir las funciones de las dos clases en sus respectivos archivos .cpp, primero vamos a hacerla para la clase Jugador:
Jugador.cpp
#include "Jugador.h"
Jugador::Jugador(const CargadorParams* pParams) :
SDLObjetoJuego(pParams) {}
void Jugador::dibujar() {
SDLObjetoJuego::dibujar();
}
void Jugador::actualizar() {
m_x -= 1;
m_frameActual = int(((SDL_GetTicks() / 100) % 6));
}
void Jugador::limpiar() {}
Lo primero que definimos es al constructor de Jugador y lo que hacemos es iniciar al constructor de SDLObjetoJuego por medio de los parametros recibidos. Despues tenemos a la funcion dibujar que utilizara a la funcion de dibujar de SDLObjetoJuego. El siguiente metodo es actualizar donde decrementaremos a m_x y cambiaremos la ubicacion del cuadro actual (m_frameActual). Por ultimo, definimos en blanco a limpiar. Ahora debemos hacer exactamente lo mismo para Enemigo, veamos el codigo:
Enemigo.cpp
#include "Enemigo.h"
Enemigo::Enemigo(const CargadorParams* pParams) :
SDLObjetoJuego(pParams) {}
void Enemigo::dibujar() {
SDLObjetoJuego::dibujar();
}
void Enemigo::actualizar() {
m_x += 1;
m_frameActual = int(((SDL_GetTicks() / 100) % 6));
}
void Enemigo::limpiar() {}
Este trabajara de la misma forma salvo por actualizar donde en lugar de decrementar el valor de m_x lo incrementaremos. El resto de las funciones son exactamente igual al de Jugador. Con esto ya tenemos todo listo para poder crear nuestros objetos en el juego pero deberemos hacer las ultimas modificaciones. La primera sera eliminar (o comentar) los objetos de Jugador y Enemigo de Juego.h:
Jugador* m_jugador;
Enemigo* m_enemigo;
Lo siguiente sera eliminar o comentar el siguiente bloque de la funcion iniciar de Juego.cpp:
m_jugador = new Jugador();
m_jugador->cargar(300, 300, 128, 82, "animado");
m_objetosJuego.push_back(m_jugador);
m_enemigo = new Enemigo();
m_enemigo->cargar(0, 0, 128, 82, "animado");
m_objetosJuego.push_back(m_enemigo);
Nuestra siguiente y ultima modificacion sera en la funcion iniciar donde agregaremos las siguiente dos lineas:
m_objetosJuego.push_back(new Jugador(
new CargadorParams(300, 100, 128, 82, "animado")));
m_objetosJuego.push_back(new Enemigo(
new CargadorParams(300, 300, 128, 82, "animado")));
Como al comienzo modificamos a nuestra clase ObjetoJuego, no solamente la hicimos completamente abstracta y eliminamos la carga sino que tambien modificamos a dibujar para que no cargue la renderizacion, por ende debemos ir a Juego.cpp y en la funcion renderizar cambiaremos esta linea:
m_objetosJuego[i]->dibujar(m_aRenderer);
De la siguiente manera:
m_objetosJuego[i]->dibujar();
Con esta ultima modificacion podemos compilar nuestro juego y ver que sucede, mediante el siguiente video
Como podemos ver en el video ahora tenemos todo en su lugar para poder reutilizar Juego y ObjetoJuego todas las veces que necesitemos.
En resumen, hoy hemos visto como implementar los tres pilares de la Programacion Orientada a Objetos (OOP), como la abstraccion y la herencia en conjuncion con el polimorfismo nos permite implementar nuevos elementos en nuestro codigo sin tener que hacer muchas modificaciones y usando un codigo que se puede reutilizar. Espero les haya sido de utilidad y les dejo un link a GitHub donde estan los codigos creados hoy:
Creando clases reusables / GitHub
Les dejo algunas de mis redes sociales para seguirme o recibir una notificacion cada vez que subo un nuevo post:


Donación
Es para mantenimento del sitio, gracias!
$1.50





