Programar como dios manda
Hola a todos, aqui estamos de nuevo. Ahora con una serie de tutoriales sobre como programar bien. :) el titulo es un poco cachondo, ya lo se, pero es la verdad... hoy en dia casi todo el mundo usa C++ para la programacion profesional, este lenguaje se convierte en el ideal tambien para programar juegos, no solo porque hereda toda la funcionalidad de C sino que ademas nos permite OOP (programacion orientada al objeto). A estas alturas ya deberias saber de que va la OOP (clases, objetos, herencia, encapsulacion, etc...) En estos tutoriales vamos a mejorar nuestra OOP, y sobretodo como usarlo para lo que nosotros queremos, esto es, programar videojuegos. No es lo mismo programar el HalfLife que ContaPlus, por ello las tecnicas que se usan para uno y otro deberian ser bastante diferentes.
Es importante ademas no solo utilizar estas nuevas tecnicas que vamos a ver, sino ademas hacerlo con "estilo". El estilo es importante porque nos va a ayudar a escribir codigo consistente y porque no decirlo, bonito. :)
Lo primero que debemos hacer es adoptar la notacion hungara. Es muy posible que ya sepas de que se trata, pero si no te cuento rapidamente de que va: Consiste unicamente en predecir los nombres de variables con una notacion que nos proporcione informacion acerca de la variable. Por ejemplo, si una variable se llama PuntosVida, ahora deberia llamarse iPuntosVida (lo que nos indica que es de tipo integer). O por ejemplo el puntero a la pantalla Device3D deberia llamarse pDevice3D para indicar que es un puntero, y asi sucesivamente. Podeis encontrar una lista completa del estandar para la notacion hungara en cualquier sitio de internet, aqui vamos a utilizar las notaciones mas comunes y extendidas, aunque tu puedes crear las tuyas propias.
Tambiene debemos nombrar a las variables segun otro tipo de caracteristicas, por ejemplo usar "m_" para nombrar a las variables miembro de una clase. Por ejemplo tenemos la clase Jugador, pues los miembros serian m_iPuntosVida, m_pDibujoCara, etc... asi tambien tenemos "g_" para las variables globales, "s_" para las estaticas y asi sucesivamente.
Por ultimo, la notacion hunara tambien afecta a las clases,
asi que vamos a nombrar empezando con "C" todas las clases. Por ejemplo
CJugador.
Cuando se trate de una clase Interfaz usaremos la letra "I". Por ejemplo
IDirectDraw. Tambien se suele comenzar con "UI" las clases que tratan
sobre el interfaz del usuario.
Diseño de clases
Seguro que os habeis dado cuenta a la hora de diseñar clases que hay una serie de funciones miembro que se suelen repetir, pero que solemos llamar de diferente forma segun el proyecto. Exceptuando el constructor y el destructor, que han de llamarse igual que la clase, el resto de funciones suelen incluir alguna como "Limpiar", "Acctualizar", ... que afectan a los valores miembro, asi como un pseudo-constructor/destructror ("crear","destruir") que se suelen usar en caso de que no queramos activar la clase en el momento de crearla por ejemplo. Asi mismo la clase "destruir" es importante para limpiar las variables en el momento que nosotros queramos, esto es muy importante como veras mas adelante.
Una ventaja añadida de usar el par Construir/Destruir en vez de el constructor/destructor por defecto es que podemos crear y destruir el objeto dinamicamente tantas veces como queramos reusando la misma memoria del objeto.
Este seria nuestro modelo de clase a seguir:
class Ejemplo { public: Ejemplo() { Clear(); }
~Ejemplo() { Destroy(); }
void Clear();
bool Create();
void Update();
void Destroy();
}
Diseño de Jerarquias
Existen dos tipos de jerarquizar: La primera consiste en derivar una clase de otra, es el caso mas habitual y se llama "Herencia". La otra consiste en encapsular un objeto como miembro de otro, a esta se le llama "Encapsulamiento".
La unica regla que tenemos que tener en mente es: Si un objeto se relaciona con otro de la forma "es un" hay que usar la herencia publica. Si por el contrario la relacion se describe mejor de la forma "tiene un" hay que usar el encapsulamiento.
Una forma sencilla de ver la diferencia entre los dos tipos es con este ejemplo:
La clase CMercedes "es un" tipo de la clase CCar
La clase CMercedes "tiene un" tipo de case CVolante
Diseño de patrones
Muy bien, vamos adetrandonos en la miga de asunto...jejeje
Los patrones nos ayudan a resolver una cantidad de problemas de una manera similar, esto quiere decir que programamos para resolver problemas generales.
Existen cantidad de patrones comunmente conocidos entre los programadores para segun que tipo de problema queramos resolver, nosotros vamos a ver los que afectan al desarrollo de videojuegos:
El patron "Singleton"
Se usa cuando un objeto global ha de ser accedido por un gran numero de clases o modulos. Si creamos un objeto estatico no local serviria para nuestro proposito, no? pero esto nos produce a la larga otro tipo de problemas y por lo tanto no nos conviene. El patron nos ofrece muchas mas ventajas, un ejemplo sencillo de singleton seria este:
class Singleton
{
public:
Singleton1& Instance()
{
static Singleton Obj;
return Obj;
}
private:
Singleton1();
}
Sim embargo, si queremos poder derivar de nuestra clase singleton haran falta algunos cambios...
class SingletonBase
{
public:
SingletonBase()
{ cout << "Se ha creado!" << endl; }
virtual ~SingletonBase()
{ cout << "Se ha destruido!" << endl; }
virtual void Access()
{ cout << "Se ha accedido!" << endl; }
static SingletonBase* GetObj()
{ return m_pObj; }
static void SetObj(SignletonBase* pObj)
{ m_pObj=pObj}
protected:
static Singleton* m_pObj;
};
SignletonBase* SingletonBase::m_pObj;
inline SingletonBase* Base()
{
assert(SignletonBase::GetObj());
return SingletonBase::GetObj();
} //Crear una clase devirada del tipo singleton
cass SingletonDerived:public SingletonBase
{
public:
SingletonDerived()
{ cout << "SingletonDerived creado!" << endl; }
virtual ~SingletonDerived()
{ cout << "SingletonDerived destruido!" << endl; }
virtual void Access()
{ cout << "SingletonBase accedido!" << endl; }
protected:
}; inline SingletonDerived* Derived()
{
assert(SingletonDerived::GetObj());
return(SingletonDerived*)SingletonDerived:::GetObj();
}
Usando el codigo...
//El singleton complejo requiere algo mas de trabajo, pero es mas flexible.
//Proporciona mas control sobre la creaccion del objeto
SingletonDerived:SetObj(new SingletonDerived);
//Se ha sobreescrito la funcion, incluso accediendo a traves del metodo original
Base()->Access();
Derived()->Access();
//Este singleton que hemos diseñado requiere por desgracia tambien la destruccion
delete SingletonDerived::GetObj();
>Vale tio, muy impresionante, pero esto de que narices
me sirve al fin y al cabo.
Esto mismo me preguntaba yo al principio. Los patrones singleton se usan normalmente cuando piensas usar un objeto global o puntero para hacer referencia a una sola instancia de una clase. Por ejemplo, una clase manager-type, donde una solo se requiere una sola instancia (esto es, el nombre del patron). Las clases que manejan el sonido de un juego, la interfaz del usuario, o hasta el propio juego son del tipo singleton :)
>mmm, ok! pero sigo sin entender porque no usar un simple objeto global, o un puntero.
Primero, si piensas crear objetos globales, acceder a un objeto a traves de una sola funcion es mas facil que tener que hacer "extern" un objeto global en todos los ficheros. Ademas, de esta forma controlas cuando el objeto se crea a lo largo de todo el juego.
Segundo, si usas un puntero en vez de un objeto, tomas el control cada vez que el objeto es accedido
Por ultimo, si creas un singleton que se pueda derivar usando la tecnica anterior, pueden ampliar tu clase manteniendo compatibilidad con la clase base. Veamos como funciona porque no me he explicado bien creo :)
Imaginemos que la libreria A utiliza una case singleton, derivada como se visto antes. La libreria B debe usar A, asi que debemos incuir los archivos de cabecera y y demas porque B necesita de A. La libreria C hace uso de las dos anteriores, pero necesita realizar cambios en la libreria A por circunstancias de juego. En vez de crear una nueva versionn, podemos simplemente derivarla. (Clase D). Asi entonces si el juego es el encargado de instanciar el objeto, podemos sustituir la clase D por la A. Creando una nueva funcion de acceso que devuelve un puntero a D en vez de A.#, Podemos acceder a todas las funciones nuevas de D. Sin embargo, la clase B continuara utilizando el antigo acceso como hacia antes.
Las funciones virtuales pueden ser sobreescritas, pero debes asegurarte de que la nueva funcionalidada es compatible con la anterior para garantizar la compatibilidad.
Mas adelante veremos otro ejemplo mas completo sobre la
creacion de singletons
El patron Façade
Este patron se utiliza cuando queremos diseñar una clase manager. Es una clase que proporciona una unica interfaz para una gran cantidad de clases relacionadas, generalmente algun tipo de susbsistema podriamos llamarlas. Estas clases seran habitualmente del tipo "singleton". Un ejemplo de clase manager es la que se encarga de manejar el acceso a los subsistemas de sonido y graficos en la interfaz de usuario.
Este patron nos permite rebajar al minimo las interdependencias entre clases. Pongamos un ejemplo y lo veras mas claro:
En el peor de los casos tendremos dos subsistemas en los cuales todas sus clases necesitaran acceder a las otras, esto nos lleva a tener un gran numero de interdependencias: (n-1)2 donde n es el numero de clases en el proyecto. El problema entonces viene cuando queremos modificar un subsistema, porque tendremos que modificar tambien seriamente el otro.
La regla general que debemos seguir para evitar esto es: Siempre que sea posible evitar exponer las clases de un subsistema fuera del mismo
El patron State
Al programar un juego nos tenemos que enfrentar al manejo de los "estados" del juego, esto es en que parte estamos, en que situacion se encuentra tal cosa, etc... La primera solucion que nos viene a la cabeza es utilizar una enumeracion, y posteriormemnte un switch .. case para cada comportamiento. Los problemas llegan cuando tenemos un gran numero de estados, y debemos controlar estos estados en diferentes partes de nuestro programa... ya te puedes imaginar el "copiar-pegar" que se crea :)
Una manera eficaz de resolver esta situacion es utilizar objetos para representar los diferentes estados de juego. Estos estados comparter codigo entre ellos mediante la herencia de sus clases base, ademas podemos derivar nuevos estados a su vez.
Cuando implementemos esto es nuestro codigo, se traduce en dejar la transicion de las clases a una clase manager. De esta manera eliminamos las dependencias entre objetos, dejando a la clase manager la tarea de "saber sobre" los objetos-estado. Los estados se pueden crear usando un objeto Factory (objeto-fabrica lo podriamos llamar) que lo explicare enseguida.
Este patron State no solo sirve para enumerar los estados del juego, tambien se usa en sistemas de Inteligencia Artificial o para representar diferentes modos de juego en el mismo programa.
El patron Factory
Este patron trata sobre como organizarnos a la hora de
crear objetos. Debemos crear clases interfaces abstractas para especificar cuando
crear implementaciones de clases derivadas. Si, ya se que todo esto suena muy
pedante, pero es la mejor explicacion posible. Esto en la practica se traduce
que vamos a tener un solo objeto capaz de crear cantidad de objetos diferentes,
generalmente relacionados entre si por una clase base comun. Esta clase comun
tiene un unico metodo que acepta un tipo de ID de clase y devuelve el objeto
creado.
>Pues no entiendo para que tanto rollo, que ventajas tiene esto?
Pues son varias:
1. Si centralizamos la creacion de objetos podremos controlar mejor como se colocan estos en memoria. Es mejor evitar la asignacion de memoriai automatica, y asignar nosotros la memoria exacta a los objetos.
2. La inicializacion de los objetos que pertenecen a un mismo grupo, dentro de una jerarquia de clases, se hace mas facil si todos ellos heredan algun tipo de metodo inicializador, por ejemplo insertarlos dentro del manejador de recursos o lo que sea.
3. Este patron nos permite poder extender nuestra coleccion de objetos facilmente, añadiendo nuevas ID de clase a las existentes. Incluso podemos realizar esto mediante datos sin modificar el codigo de programa.
Mas o menos la cosa quedaria asi:
BaseClass* ClassFactory::CreateObject( int id) { BaseClass* pClass = 0; switch( id ) { case 1: pClass = new Class1; break; case 2: pClass = new Class2; break; case 3: pClass = new Class3; break; default: assert("Error! Se ha pasado una ID incorrecta a la clase factory!"); }; //aqui podemos poner algo de codigo comun para la inicializacion pClass -> Init(); return pClass; }
Utilizaremos esta estructura para crear gran cantidad de elementos en nuestros juegos: Inteligencia Artificial, Recursos, Texturas, Sonidos, Estados, etc...
Y por el momento ya vale de rollo, que me duele la cabeza un poco :) a ti no???
salu2,