Bienvenidos sean a este post, hoy veremos el concepto de la fabrica de objetos.
En realidad, la fabrica de objetos es una clase a la cual se le encomienda la creacion de nuestros objetos; le pedimos que fabrique nuestro objeto, genere una nueva instancia de este objeto y lo devuelva. Veamos un ejemplo de implementacion muy rudimentario:
ObjetoJuego* FabricaObjetoJuego::crearObjetoJuego(ID id)
{
switch(id)
{
case "JUGADOR":
return new Jugador();
break;
case "ENEMIGO":
return new Enemigo();
break;
// muchos mas tipos de objetos...
}
}
Como pueden ver en este ejemplo muy simple, recibiremos un dato que sera el id para cada objeto y por medio de un switch generamos el objeto correspondiente. Si bien, esto no es la mejor solucion es una practica para demostrar el punto porque en este caso debemos un tener case para cada posible objeto y tendremos que modificar el codigo cada vez que surja un nuevo elemento en nuestro juego. Esto puede convertir en algo tedioso y poco practico pero la idea sera crear una fabrica que no se preocupe por cada tipo de objeto que podamos tener.
Una solucion para trabajar con objetos de manera dinamica es el uso de fabricas distribuidas, para ello debemos hacer una fabrica de objetos genericos la cual nos permitira crear de manera dinamica los objetos de nuestra fabrica en lugar de hacerlo por una funcion como vimos en el ejemplo. Para poder implementarlo vamos a enfocarnos en un par de puntos; el primero va a ser la utilizacion de un map que tendra un valor que sera de id e ira relacionado a un objeto creado por una clase base. Este mapa lo tendremos actualizado por medio de una funcion que recibira ambos datos. Vamos a utilizar el proyecto que estuvimos trabajando 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.
Primero crearemos la nueva clase que sera la fabrica de nuestros objetos, la llamaremos FabricaObjetosJuego, una vez creada iremos a nuestro archivo de encabezado y agregaremos el siguiente codigo:
FabricaObjetosJuego.h
#pragma once
#ifndef __FabricaObjetosJuego__
#define __FabricaObjetosJuego__
#include <string>
#include <map>
#include "ObjetoJuego.h"
class CreadorBase
{
public:
virtual ObjetoJuego* crearObjetoJuego() const = 0;
virtual ~CreadorBase();
};
class FabricaObjetosJuego
{
public:
bool registrarTipo(std::string, CreadorBase*);
ObjetoJuego* crear(std::string);
private:
std::map<std::string, CreadorBase*> m_creadores;
};
#endif // !__FabricaObjetosJuego__
Este tiene una particularidad como es la clase base, CreadorBase. En ella tenemos al destructor y una funcion abstracta llamada crearObjetoJuego, esta sera la que redefiniremos en cada uno de los objetos. La siguiente clase sera la fabrica en si, hablemos de su parte privada. En ella, tenemos un mapa para almacenar el id de cada objeto y el objeto en si. En cambio, en la parte publica tenemos las funciones que registran al tipo y otra para crear un objeto nuevo. Con esto comentado, pasemos a FabricaObjetosJuego.cpp y agreguemos el siguiente codigo:
#include "FabricaObjetosJuego.h"
#include <Windows.h>
CreadorBase::~CreadorBase() {}
bool FabricaObjetosJuego::registrarTipo(std::string id, CreadorBase* pCreador) {
std::map<std::string, CreadorBase*>::iterator it = m_creadores.find(id);
if (it != m_creadores.end()) {
delete pCreador;
return false;
}
m_creadores[id] = pCreador;
return true;
}
ObjetoJuego* FabricaObjetosJuego::crear(std::string id) {
std::map<std::string, CreadorBase*>::iterator it = m_creadores.find(id);
if (it == m_creadores.end()) {
char bufmsj[200] = "";
sprintf_s(bufmsj, "No se encontro el tipo: %d\n", id);
OutputDebugStringA(bufmsj);
return NULL;
}
CreadorBase* pCreador = (*it).second;
return pCreador->crearObjetoJuego();
}
Aqui definimos todas las funciones que lo necesitan, y la primera es el destructor de la clase base porque esta no es completamente abstracta sino solamente virtual. Veamos como es la definicion de la funcion registrarTipo:
bool FabricaObjetosJuego::registrarTipo(std::string id, CreadorBase* pCreador) {
std::map<std::string, CreadorBase*>::iterator it = m_creadores.find(id);
if (it != m_creadores.end()) {
delete pCreador;
return false;
}
m_creadores[id] = pCreador;
return true;
}
Esta sera la funcion encargada de registrar los distintos tipos de objetos, para ello recibe dos valores los cuales usaremos para el id del objeto y el objeto que crearemos. Observen lo primero que hacemos, definir un objeto tipo iterador de mapa para el encargado de almacenar la relacion entre el tipo de elemento y su creador. En esta variable, almacenaremos el resultado devuelto por la busqueda de find. Este busca el id que le pasamos como argumento. Con nuestro valor almacenado, podemos pasar al siguiente paso.
Lo siguiente es un condicional donde verifica que el valor almacenado en it (el iterador anterior) es distinto de la posicion final del mismo. Si la condicion es verdadera, elimina el valor en pCreador y devuelve un false saliendo de la funcion. En caso contrario, esto significa que no existe, lo cual nos perrmite poder asignar el puntero del creador al tipo o clave. Con esto, ya tenemos nuestro tipo registrado y devolvemos un true para salir correctamente de la funcion. Con esto realizado podemos pasar a la siguiente:
ObjetoJuego* FabricaObjetosJuego::crear(std::string id) {
std::map<std::string, CreadorBase*>::iterator it = m_creadores.find(id);
if (it == m_creadores.end()) {
char bufmsj[200] = "";
sprintf_s(bufmsj, "No se encontro el tipo: %s\n", id);
OutputDebugStringA(bufmsj);
return NULL;
}
CreadorBase* pCreador = (*it).second;
return pCreador->crearObjetoJuego();
}
Esta sera la funcion encargada de crear el nuevo objeto, por ello solamente recibe el id del tipo que deseamos crear. Volvemos a generar un iterador similar al anterior para buscar el id dentro de nuestro mapa. Lo siguiente es un condicional donde verifica si it esta en m_creadores, si llega al final significa que no lo encontro y por esto, mostraremos un mensaje para notificar que no se encontro el tipo. Por ultimo, enviamos a este mensaje en la salida de depuracion para devolver un valor NULL. En caso de no cumplirse la condicion, crearemos un objeto de tipo CreadorBase al cual le asignaremos un puntero del objeto asignado en el iterador (it) creando nuestro objeto. Antes de terminar, vamos a convertir esta fabrica en un singleton.
El procedimiento va a ser similar a como trabajamos hasta ahora, primero iremos a nuestro archivo de encabezado y en la parte privada agregaremos las siguientes tres lineas:
FabricaObjetosJuego();
~FabricaObjetosJuego();
static FabricaObjetosJuego* e_pInstancia;
Estas tres lineas no son otras que nuestro constructor, destructor y la variable que usaremos para verificar si esta creado o no nuestro objeto de fabrica. Los dos primeros son los prototipos que definiremos en un momento. Nuestro siguiente paso sera agregar el siguiente prototipo en la parte publica:
static FabricaObjetosJuego* instanciar();
Esta sera la funcion para crear el singleton, pasemos ahora a definir todos los prototipos anteriores. Para ello, iremos a FabricaObjetosJuego.cpp y agregaremos el siguiente codigo:
FabricaObjetosJuego* FabricaObjetosJuego::e_pInstancia = 0;
FabricaObjetosJuego::FabricaObjetosJuego() {}
FabricaObjetosJuego::~FabricaObjetosJuego() {}
FabricaObjetosJuego* FabricaObjetosJuego:: instanciar() {
if (e_pInstancia == 0)
e_pInstancia = new FabricaObjetosJuego();
return e_pInstancia;
}
La primera linea es para asignarle un valor a la variable estatica, porque es obligatorio que este iniciada, y luego tenemos la definicion del constructor y destructor pero por el momento los dejaremos en blanco. La ultima funcion es la encargada del singleton. Este verifica si el puntero de la instancia esta vacio, y en caso de ser verdadero procede a crear un objeto de la clase y lo asigna. En caso contrario, lo omitira y procede a devolver el objeto contenido en puntero, y esto nos asegura de que no existan duplicados de un objeto.
Por ultimo, debemos volver al archivo de encabezado y agregaremos un typedef para poder invocarlo de otra manera, tal como hicimos en otros casos. Para ello, agregamos la siguiente linea al final del archivo de encabezado por fuera del condicional #ifndef:
typedef FabricaObjetosJuego Elfabricante;
Ahora podemos comenzar a modificar nuestra clase ObjetoJuego pero antes debemos asegurarnos que cada elemento tenga un Creador. Para ello, debemos agregar una clase credora en cada uno de los elementos que tenemos, vamos a ver un ejemplo con la clase Jugador. Para ello, iremos al archivo de encabezado y primero agregaremo esta linea:
#include "FabricaObjetosJuego.h"
Esto sera para incluir la libreria que creamos anteriormente, a continuacion agregaremos el siguiente codigo:
class CreadorJugador : public CreadorBase
{
public:
ObjetoJuego* crearObjetoJuego() const;
};
Esta sera la nueva clase que usaremos para crear al jugador. Podemos ver que es heredera de la clase base que definimos en la fabrica. Esta solo posee una funcion y solo es un prototipo. Para definirlo, debemos ir a Jugador.cpp y agregaremos el siguiente codigo:
ObjetoJuego* CreadorJugador::crearObjetoJuego() const {
return new Jugador();
}
Basicamente lo que hara es generar un nuevo objeto de tipo Jugador, va a quedar con un error porque usamos un constructor que no esta definida en la clase base de Jugador pero en un momento lo solucionaremos.
Nuestra siguiente modificacion es en la clase ObjetoJuego donde debemos agregar la funcion encargada de cargar, para ello agregaremos la siguiente linea dentro de la parte publica:
virtual void cargar(const CargadorParams*) = 0;
Esto evitara que debamos pasar los parametros a la fabrica misma sino que sera a traves de cada objeto porque ahora todos los objetos herederos de esta deberan definir la clase cargar y enviar todos los parametros. Tambien debemos modificar su parte protegida actual:
protected:
ObjetoJuego(const CargadorParams*);
virtual ~ObjetoJuego();
De la siguiente manera:
protected:
ObjetoJuego(const CargadorParams*);
ObjetoJuego();
virtual ~ObjetoJuego();
Agregamos un nuevo constructor porque ahora no pasaremos los datos al constructor sino desde otra funcion. Aunque vamos a mantener a ambos constructores por un simple tema de compatibilidad por el momento. Nuestro siguiente paso sera ir a ObjetoJuego.cpp y agregaremos la definicion del nuevo constructor:
ObjetoJuego::ObjetoJuego() {}
Con todo esto realizado, nuestro siguiente paso sera implementar a la nueva funcion en las clases herederas. Para ello vamos a ir a SDLObjetoJuego, y en nuestro archivo de encabezado agregaremos la siguiente linea en la parte publica:
virtual void cargar(const CargadorParams*);
Esta sera la sobrecarga para el prototipo abstracto que agregamos anteriormente, y en esta misma seccion agregaremos al constructor predeterminado:
SDLObjetoJuego();
Lo agregamos porque como mencionamos anteriormente ya no va a ser necesario recibir dichos parametros. Pero al igual que en el caso anterior, los mantendremos a los dos por un tema de compatibilidad hasta migrar todo. Nuestra ultima modificacion en este archivo es ir a la parte protegida y agregaremos la siguiente linea:
int m_numFrames;
Esta sera una variable para indicar el numero de frames que utilizaremos. Con esto aclarado, pasemos a modificar el archivo .cpp donde debemos agregar la definicion del constructor a su nuevo formato y la definicion de la clase cargar:
SDLObjetoJuego::SDLObjetoJuego() : ObjetoJuego(){}
void SDLObjetoJuego::cargar(const CargadorParams* pParams) {
m_posicion = Vector2D(pParams->getX(), pParams->getY());
m_velocidad = Vector2D(0, 0);
m_aceleracion = Vector2D(0, 0);
m_ancho = pParams->getAncho();
m_alto = pParams->getAlto();
m_idTextura = pParams->getIdTextura();
m_frameActual = 1;
m_filaActual = 1;
m_numFrames = pParams->getNumFrames();
}
Este nuevo constructor no recibe ningun parametro para poder usar a la nueva funcion, y solamente llama al constructor de ObjetoJuego. En la funcion cargar si recibimos todos los parametros y mediante las funciones iremos iniciando todos las variables de nuestra clase. Con esto terminamos con las modificaciones en este clase.
Nos vamos a quedar un error en el nuevo constructor, para solucionarlo debemos ir a Vector2D.h y en la parte publica agregaremos la siguiente linea:
Vector2D();
Y en la definiremos en Vector2D.cpp de la siguiente manera:
Vector2D::Vector2D() {}
Como se habran dado cuenta, solamente agregamos la definicion del constructor predeterminado. Todavia no entendi muy bien la razon, pero fue la unica forma de solucionar a este error.
Con todo esto realizado, debemos volver a la clase Jugador para poder implementar estas modificaciones realizadas. Primero vamos a Jugador.h y debemos agregar el constructor predeterminado:
Jugador();
Y luego agregaremos el prototipo de nuestra funcion cargar:
void cargar(const CargadorParams*);
Nuestro siguiente paso sera ir a Jugador.cpp y agregaremos la definicion del constructor y la funcion cargar:
Jugador::Jugador() : SDLObjetoJuego() {}
void Jugador::cargar(const CargadorParams* pParams) {
SDLObjetoJuego::cargar(pParams);
}
El nuevo constructor, tal como mencionos anteriormente es para tener la opcion de crear los objetos sin parametros porque de esto se encargara cargar. Aunque seguimos teniendo a los dos por un tema de compatibilidad temporal. Lo siguiente es la definicion de la funcion cargar que recibe los parametros mediante la clase CargadorParams. Despues, los enviamos a la funcion cargar pero de la clase SDLObjetoJuego para que los procese finalmente.
Esto que estamos haciendo parece un sin sentido pero en realidad nos da la posibilidad de poder descentralizar la recepcion de los parametros a nuestra funcion de creacion en la fabrica. Esto sera mas evidente cuando trabajemos con los archivos.
Nuestra ultima modificacion sera con el cargador de parametros porque esta al necesitar trabajar con los menues no contempla la recepcion de los atributos para el callback y la velocidad de animacion para los menues y los graficos animados respectivamente. Para ello, debemos ir a nuestra clase CargadorParams y en su archivo de encabezado modificaremos el prototipo de la siguiente manera:
CargadorParams(float, float, float, float, std::string, int = 6, int = 0, int = 0);
Si observan lo unico que hicimos fue agregar dos nuevos parametros para manejar el id que usaremos para identificar a nuestra funcion de llamada para los punteros de funciones y otro para indicar la velocidad de animacion para los graficos animados como vimos en el game over en este post. Estos tendran un valor predeterminado para que no sea obligatorio pasarles un valor. Nuestro siguiente paso sera agregar las siguientes lineas en la parte privada:
int m_idCallback;
int m_velocAnim;
Son simplemente la declaracion de las variables que usaremos para almacenar el id del callback y la velocidad de animacion. Solo nos resta un tema mas, la definicion del constructor en el archivo .cpp:
CargadorParams::CargadorParams(float x, float y,
float ancho, float alto, std::string id, int numFrames,
int idCallback, int velocAnim) :
m_x(x), m_y(y), m_ancho(ancho), m_alto(alto),
m_idTextura(id), m_numFrames(numFrames),
m_idCallback(idCallback),m_velocAnim(velocAnim) {
}
Agregamos los dos nuevos argumentos para recibirlos y los usamos para iniciar a las nuevas variables. El resto sigue siendo el mismo codigo y por el momento no hace otra tarea mas. Solo nos resta unas pequeñas modificaciones mas. Debemos modificar a Enemigo y BotonMenu de la misma manera que en Jugador. Pasemos primero a la clase Enemigo y agreguemos la clase creadora pero antes debemos agregar al inicio esta linea:
#include "FabricaObjetosJuego.h"
Simplemente la inclusion de la libreria para poder usar a la clase CreadorBase. Agreguemos la clase creadora:
class CreadorEnemigo : public CreadorBase
{
public:
ObjetoJuego* crearObjetoJuego() const;
};
Al igual que en Jugador este es el prototipo de la nueva clase encargada de crear a los enemigos. Esta debe ir despues de la otra clase pero dentro del condicional #ifndef. Siguiendo en el mismo archivo, agreguemos al constructor predeterminado:
Enemigo();
Al igual que en Jugador, es solamente para la futura implementacion de los datos desde el archivo. Pasemos a agregar el siguiente prototipo en la parte publica:
void cargar(const CargadorParams*);
Esta funcion sera la encargada de cargar los parametros en lugar del constructor. Pasemos a definir todas estas funciones y para ello, iremos a Enemigo.cpp y agregaremos el siguiente codigo:
ObjetoJuego* CreadorEnemigo::crearObjetoJuego() const {
return new Enemigo();
}
Enemigo::Enemigo() : SDLObjetoJuego() {}
void Enemigo::cargar(const CargadorParams* pParams) {
SDLObjetoJuego::cargar(pParams);
}
La primera sera para crear el nuevo objeto, en otro post veremos como se aplica. La siguiente es la definicion del constructor predeterminado. Y la ultima sera para enviar los parametros a la funcion cargar pero de SDLObjetoJuego en lugar del constructor.
Esto mismo debemos repetirlo en BotonMenu. Vayamos a su archivo de encabezado y agreguemos la linea de implementacion de la fabrica:
#include "FabricaObjetosJuego.h"
Pasemos a agregar la clase creadora:
class CreadorBotonMenu : public CreadorBase
{
public:
ObjetoJuego* crearObjetoJuego() const;
};
Agreguemos al constructor predeterminado:
BotonMenu();
Y finalmente, agreguemos al siguiente prototipo en la parte publica:
void cargar(const CargadorParams*);
Pasemos a definirlas en el archivo .cpp, y para ello primero agregaremos el siguiente codigo:
ObjetoJuego* CreadorBotonMenu::crearObjetoJuego() const {
return new BotonMenu();
}
BotonMenu::BotonMenu() : SDLObjetoJuego() {}
void BotonMenu::cargar(const CargadorParams* pParams) {
SDLObjetoJuego::cargar(pParams);
}
Estas son las definiciones de la nueva funcion, el constructor y la nueva clase, con las mismas conductas. Con esto, ya tenemos implementada nuestra fabrica para manejar la informacion mediante archivos pero de esto comenzaremos a hablar en el siguiente post.
En resumen, hoy hemos visto como implementar la fabrica, desde sus conceptos basicos, como es una clase creadora para una de las clases que implementaremos, hemos creado una funcion encargada de cargar los parametros para centralizarlos en una sola clase y esta nos permite pasar la informacion a las distintas clases que manejan la fabrica, tambien hemos visto las ultimas modificaciones sobre el cargador de parametros. Espero les haya sido de utilidad y les dejo un link a GitHub donde estan los codigos creados hoy:
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





