Mostrando entradas con la etiqueta PROGRAMACIÓN. Mostrar todas las entradas
Mostrando entradas con la etiqueta PROGRAMACIÓN. Mostrar todas las entradas

miércoles, 22 de enero de 2014

Game Developer Magazine

music: GOTHIC SEX playing "El Templo del Amor (Temple of Love)"

Conocía esta revista desde hace unos pocos años, aunque me hubiera gustado haberlo hecho mucho antes.  Cuando ví, en verano del año pasado, la portada del que fue el último número (toda negra, con el mensaje "GAME OVER" en el centro), me quedé con esta cara -->     : $

Lo que no sabía era que la habían hecho pública; ahora, para deleite de todos los que la seguíamos con mayor o menor asiduidad, y para aquellos que aún lo la conocían, puede descargarse desde GDC Vault.


Me están encantando las primeras, publicadas en los años 1994/95. Hubiera sido la caña haber podido tener algo así aquí en aquella época.

R.I.P. Game Developer Magazine. :'(

Hasta otra. :P

martes, 28 de agosto de 2012

Bye, bye, warning C4921...!!!. :D

music: MASTODON -"Seabeast"-

La perseverancia a veces da sus frutos, ¡pardiez...!.

De nuevo, otro tema ha sido arreglado. No era un problema grave pero era algo que me estaba tocando la moral desde el principio (y de hecho, todavía me cuesta hablar en términos de pasado, como algo que ha desaparecido, que ya se ha solucionado). :P

Se trataba de una serie de warnings que aparecían durante la compilación del juego, cuyo código era C4291. Se limitaban a "aparecer", a estar ahí, dando un mensaje que hasta ahora me resultaba ilegible, cada vez que reservabas dinámicamente memoria para crear un objeto teniendo en funcionamiento el sistema de control de memory leaks. Y varias veces a lo largo de todo este tiempo me había puesto a intentar ver por qué se producían y qué hacer para que no salieran... sin éxito. Hasta ahora. : )

Y no, la solución no ha venido añadiendo al código  #pragma warning ( disable : 4291 )  :P

El asunto es: cuando activamos el sistema de control de memory leaks en el engine se realiza una sobrecarga de los operadores new y new[ ]. En concreto la sobrecarga 'Placement Form' de éstos, adoptando una cabecera así:

void* operator new      ( uint uSize, char* pszFile, uint uLine );
void* operator new[]    ( uint uSize, char* pszFile, uint uLine );

Pues bien, dado que estos son los operadores new y new[ ] que se sobrecargan, hay que hacer también las sobrecargas de delete y delete[ ] con esta misma estructura, es decir, así:

void  operator delete   ( void* p, char* pszFile, uint uLine );
void  operator delete[] ( void* p, char* pszFile, uint uLine );

La razón argumentada es que, si se crea dinámicamente un objeto y en el constructor de éste salta una excepción, se pueda liberar la memoria que había sido reservada para el objeto empleando dichas sobrecargas de delete y delete[]. Pero las únicas sobrecargas que había definidas hasta ahora eran:

void  operator delete   ( void* p );
void  operator delete[] ( void* p );

Pues... por eso daba los cansinos warnings. :P Al añadirle las otras sobrecargas de delete y delete[ ] a éstas últimas al fín han desaparecido, ¡joas, joas...!. :D

Mas información aquí.

En mi caso lo anterior nunca se va a producir pues, dada la arquitectura del engine, en los constructores de las clases no se hace nada "raro" que pueda provocar el lanzamiento de una excepción, no se hace nada más allá que inicializar a NULL, false ó 0 algunos atributos de la clase... dejando la "auténtica" inicialización del objeto en el método Init, llamado inmediatamente tras la creación del objeto, que todas las clases poseen. Por ello las nuevas sobrecargas quedan como una mera formalidad, pues no son empleadas dentro del engine (no se necesitan, y se siguen empleando las que ya había). Pero como digo siempre: El problema es no saber, y ahora que sé por qué se producía el tema, no me apetece quitarlas para no olvidarlo. XD

Hasta otra. :P

miércoles, 16 de mayo de 2012

Terrenos y Mapas de Alturas

music: L7 -"Pretend We're Dead"-

( NOTA: es probable que cambie las partes en las que digo píxel por téxel, dado que, aunque lo que voy a llamar Mapa de Alturas no es una textura propiamente dicha -no se mapea en ningún modelo 3D-, el concepto de píxel parece estar semánticamente más asociado con el monitor, mientras que el de téxel lo está más con una unidad atómica, indivisible, de una imagen. Aún no estoy seguro, de momento lo dejo como TO-DO ).

En los últimos días he estado desempolvando uno de los que fueron mis programas favoritos del Máster. Con él se genera la representación visual de un terreno a partir de un Mapa de Alturas, esto es, una imagen de escala de grises (o como es mi caso, una imagen RGB a la que hemos "desatudado" el color) cuyos valores de gris (color) equivalen a una determinada "altura" en un mundo 3D; esto es, a un determinado valor positivo a la coordenada Y de cada uno de los vértices que conforman el terreno.

Así, como valores extremos del rango:

-Un pixel en el mapa de alturas que tenga un color Negro (R:0, G:0, B:0) otorgará un 0.0 como valor a la coordenada Y del vértice correspondiente a ese píxel en el terreno, vamos, en el mundo 3D.

-Y un pixel en el mapa de alturas que tenga un color Blanco (R:255, G:255, B:255) otorgará un valor máximo como valor a la coordenada Y del vértice correspondiente.

Para preparar el Mapa de Alturas se parte de una imagen a la que, mediante Photoshop , hemos quitado el color, retocado a nuestro gusto y por último escalado a un tamaño de 128x128 píxeles. La guardaremos empleando el formato Photoshop RAW (*.RAW), que no debe confundirse con imágenes RAW de cámara digital (so pena de perder un montón de días haciéndote el lío padre como me pasó a mí). :P A todo esto, debo comprobar si el nuevo GIMP 2.8 también tiene soporte para abrir imágenes Photoshop RAW.

No necesitaremos que el fichero posea una cabecera que contenga el ancho y alto de la imagen en píxeles, pero justo por ello sí tendremos que ser nosotros los que tengamos que indicar al "Objeto Terreno" el tamaño del Mapa de Alturas que le pasamos  (que como ya hemos dicho, será 128 píxeles de ancho y de largo).

A partir del Mapa de Alturas tendremos en el mundo 3D una malla poligonal cuadrada de 128x128 = 16384 vértices, dividida en (128 - 1) * (128 - 1) = 16129 sectores cuadrados contiguos, como una rejilla. Pero dado que la unidad atómica que nos interesa para renderizar es el triángulo -y no el cuadrado-, dividimos cada sector en dos triángulos rectángulos que comparten la hipotenusa. El número total de triángulos será el doble que el de sectores, esto es, 32258. : )

Por último, se crea un Vertex Buffer y un Index Buffer, y las coordenadas X,Y y Z son calculadas para cada vértice e introducidas, con la ayuda del Index Buffer, en el Vertex Buffer.

Aquí es donde entra en juego al Mapa de Alturas. Como anteriormente se ha mencionado la coordenada Y del actual vértice (del aquel que hay que generar actualmente las coordenadas, vamos) se toma consultando el valor de color del píxel correspondiente en el Mapa de Alturas; en concreto, se consultará la componente de color G de dicho píxel. Las coordenadas X y Z se generan a partir de un valor de espaciado entre sectores (un valor fijo, actualmente es 16) y de unos Offset en X y en Z: el offset en X es la mitad del ancho del Mapa de Alturas, y el Z la mitad del alto (para el caso, ambos valdrán 64).

Así, si pensamos en una rejilla o matriz 2D a cuyas filas accediéramos mediantes en índice i y a cuyas columnas accediéramos mediante el índice j, las coordenadas X,Y,Z para cada vértice 3D [i][j] se calcularían así:

Vertice3DActual[i][j].X = (i * EspaciadoEntreSectores) - (Offset_X * EspaciadoEntreSectores)
Vertice3DActual[i][j].Y = MapaAlturas[i][j].Componente_G
Vertice3DActual[i][j].Z = (j * EspaciadoEntreSectores) - (Offset_Z * EspaciadoEntreSectores)

Tras haber calculado las coordenadas de todos lo vértices se calculan las normales para los tres vértices de cada triángulo, los índices para el Index Buffer, se sitúa una cámara libre dotada de un movimiento básico de 6 grados de libertad y... estos son los resultados.




La flecha roja indicaría a grandes rasgos la posición y orientación de la cámara. Efectivamente, la última escena es un poquito "marciana". ; )

Hasta otra. :P

viernes, 9 de septiembre de 2011

El problema "curioso" (y su resolución)

music: PJ HARVEY -"This Is Love"-

Durante las últimas semanas me he dedicado a solucionar el problema "curioso" del que ya hablaba en la entrada del día 12/05/2011.

Se hacía imprescindible poder dar una solución a esto si pretendía grabar el inplay del juego; como comentaba en la entrada anterior, era imposible hacer un vídeo que se viera decentemente debido al enorme lag que se producía.

Afortunadamente ya dejé hace tiempo las pistas necesarias por donde podía ponerme a empezar a trabajar para solventar esto... y los resultados han sido muy satisfactorios. : )

Al contrario de lo que pensaba el Componente LogicaPelota no era el problema... pero sí la puerta de entrada para ir, poco a poco, "buceando" en el código hasta llegar al núcleo principal de los problemas: los métodos IBounding::GetCollision e IBounding::GetPoints.

La optimización, realizada principalmente en dichos métodos, ha llevado a aumentar el framerate del juego (estando en modo Debug) hasta los... ¡¡¡147-150 FPS!!!. Teniendo en cuenta que, en estas mismas condiciones, las medidas marcaban 46-48 FPS ( hace tiempo hice algunas pequeñas optimizaciones que lo subieron hasta ahí desde la situación de (12/05/2011) ) el empuje ha sido espectacular. : )

Resumo las modificaciones que se han realizado para llevar a cabo la hazaña:

a) Dejar de devolver por valor un std::vector. Una de las cosas que he aprendido gracias a Quimera Engine es que, cuando devuelves por valor el resultado de un método, está sucediendo lo mismo que con los parámetros de entrada por valor: se está creando UNA COPIA del contenedor que alberga el resultado. Si dicho resultado es un std::vector, se tiene que crear UNA COPIA del vector... con lo que se estaba gastando inútilmente un montón de tiempo en cada iteración de GetPoints. Solución: poner un std::vector como un parámetro de salida en GetPoints, y por referencia.

b) Por lo que he podido ver, llamar al método std::vector.clear() en cada iteración de un método puede ser algo ASESINO. Hay que evitar a toda costa llamar a std::vector.clear() en un lugar que tiene que ejecutarse en cada iteración del juego, y con la mayor rapidez posible.

c) Asimismo, realizar múltiples llamadas a std::vector.push_back(elemento) en una misma iteración de un método (en concreto, unas 94 veces), también puede llegar a ser ASESINO. Hay que evitar realizar tantas llamadas a push_back en un lugar que tiene que ejecutarse en cada iteración del juego, y con la mayor rapidez posible.

d) La mezcla de b) y c) había dado un cóctel particularmente letal para el rendimiento. La solución ha sido sencilla:

d.1) Se fija un límite máximo de tamaño para los std::vector (en concreto, para los std::vector de puntos 3D) y se declaran ya con dicho tamaño. Así, ya no es necesario emplear ni clear para vaciarlos en cada iteración, ni push_back para añadir un elemento. Pueden ser los elementos introducidos directamente como si fueran arrays.

d.2) Al tener tamaño fijo ya no es posible emplear el método std::vector.size() para conocer cuántos elementos posee el vector. Por lo que se devolverá también, como parámetro de salida, una variable unsigned int que contendrá la cantidad de elementos que poseerá el vector. Dicho valor podrá ser empleado como centinela para recorrer el vector en un bucle for.

e) Por si fuera poco, había un fallo tonto en IBounding::Draw que estaba haciendo consumir inútilmente un montón de tiempo, y que ya ha sido subsanado.

Aparte... ciertos trucos de más bajo nivel, principalmente declaración de algunos grupos de variables locales a un método como static, que han hecho ganar algunos frames más... pero el gran avance en el rendimiento del juego ha sido gracias a lo explicado anteriormente.

La solución final es terminar de plantear e implementar el sistema de detección y respuesta de colisiones de una forma distinta a la actual, pero por el momento la optimización realizada ha sido suficiente. Al fín se ha podido grabar un vídeo. Ya veremos si continuo optimizando más o me voy a hacer otras cosas más urgentes (como introducir audio, por ejemplo).

Hasta otra. :P

sábado, 20 de agosto de 2011

Reduciendo iteraciones

music: AEROSMITH -"Beyond Beautiful"- / -"She's On Fire"-

Partamos de la siguiente base: el bloque de código más rápido es aquel que nunca se ejecuta. Con esta premisa es evidente que nos interesa lograr, de una forma u otra, reducir la cantidad de veces que ciertos bloques de código se ejecutan para mejorar el rendimiento, si ello es posible. Independientemente de lo mejor o peor implementados (y optimizados) que estén dichos bloques.

Una de las razones por las que no he podido llegar a mucho en términos de optimización es porque concebía la optimización exclusivamente como la "aceleración" de la ejecución de ciertas partes de código particularmente costosas computacionalmente, concentrándome demasiado en este enfoque. No se trata de olvidar esto ni mucho menos, sino de anteponer una política de reducción de iteraciones en ciertos puntos clave. Y en el caso de Super Pong, estos puntos clave son los Componentes.

Existen compomentes que no necesitan ser ejecutados todo el tiempo, en cada tick (la IA o la visión de la raqueta, por ejemplo). Desgraciadamente, el engine no estaba preparado para poder establecer una política así, en la que se establezca que un determinado componente no va a ejecutar su método Update en cada iteración, sino sólamente cada vez que haya transcurrido un determinado espacio de tiempo. Y eso es a lo que me he estado dedicando durante los últimos días.

El punto clave de todo esto es la clase IComponent, de la cual heredan TODAS las demás clases componente. El siguiente resúmen explica los puntos más importantes:

AÑADIDOS en IComponent:

Atributo m_fElapsedTime_Max: El Elapsed Time Establecido para el compoente (en ms, pero expresado en segundos). Representa el intervalo de tiempo que debe transcurrir para dar permiso al componente para ejecutar su método Update_Exec. Se asignará en TODOS los componentes un valor para m_fElapsedTime_Max en el método Init del componente. Asignando 0.0f vamos a hacer hacemos que el componente ejecute Update_Exece en cada tick.

Atributo m_fElapsedTime_Acum: El Elapsed Time (en ms, pero expresado en segundos) actualmente acumulado en el componente. Cuando se alcance el Elapsed Time Establecido, se ejecutará el método Update_Exec del componente.

Método virtual Update_Exec() vacío. Este será, como tal, el antiguo método Update. Todos los componentes heredarán este método vacío, sin código; se sobreescribirá en TODOS los componentes (salvo en aquellos que no tengan elementos que actualizar), y se ejecutará cuando se haya alcanzado el Elapsed Time Establecido para el componente.

MODIFICACIONES en IComponent:

IComponent::Update deja de ser un método virtual vació para pasar a ser no virtual y albergar el bloque de código que comprueba que, si se ha alcanzado el Elapsed Time establecido para ese componente, se ordene ejecutar sú método Update_Exec.

(** TO-DO: ya me acordaré algún día de cómo se tabulaba esto... **)

void IComponent::Update(const float& fDelta)
{
m_fElapsedTime_Acum += fDelta;
if ( m_fElapsedTime_Acum >= m_fElapsedTime_Max )
{
m_fElapsedTime_Acum = 0.0f;
this->Update_Exec(fDelta);
}
}

Este método es heredado por todos los componentes, y cuando la Entidad ordena a sus componentes que se actualicen (manda hacer Update a todos sus componentes, vamos), está llamando a este Update heredado de IComponent.

Tras esto, se puede comenzar a pensar un plan de "presupuestos" (Budget es la palabra) de tiempo para cada componente... y el engine está preparado. Por el momento, tengo establecido que los componentes Vista e IANPC ejecuten el método Update_Exec cada 500 ms (sus m_fElapsedTime_Max tendrán almacenado 0.5f, al estar expresado en segundos).

Hice una prueba loca y asigné 10 segundos a los componentes Vista e IANPC, y el framerate no aumentó respecto del caso anterior. Pero hice una segunda prueba loca asignando también 10 segundos al componente LógicaPelota... ¡¡¡y el framerate aumentó en casi VEINTE FRAMES!!!. El componente LogicaPelota, por lo tanto, está suponiendo un cuello de botella importante (como ya sospechaba). Habrá que optimizarlo acelerando su ejecución... y antes de ello reduciendo el número de iteraciones. ; )

Hasta otra. :P

viernes, 5 de agosto de 2011

Orientación de Entidad y Orientación Visual

music: VENDEMMIAN -"All Is Lost (All Is Gone)"-

Ayer logré otra de las cosas que el juego estaba pidiendo como agua de mayo: hacer que la pelota ruede sobre sí misma mientras se desplaza por el escenario. : )

Para ello se me ha ocurrido un pequeño truco: añadir a las entidades los Ángulos de Orientación Visual, que se unirían a los ángulos que las entidades ya poseían (pasándose éstos últimos a llamarse Ángulos de Orientación de Entidad). Y así, las entidades poseen ahora dos tipos de orientación:
  • DE ENTIDAD: Orientación lógica, real, de la entidad en el mundo.
  • VISUAL: Aquellas entidades visibles en el juego tendrán ahora una orientación de su representación visual en el mundo, la cual NO SIEMPRE coincidirá con la Orientación de Entidad; esto dependerá, en última instancia, del tipo de entidad.
Se asignará el valor de los Ángulos de Orientación de Entidad a los de Orientación Visual al comienzo de la ejecución del método Update de las entidades; de esta forma se asegura que, por defecto, el valor de ambos tipos de ángulos será el mismo al comienzo del método Update.

Y así, dejamos que sea alguno de los componentes de la Entidad quien, en última instancia, determine el valor definitivo de los Ángulos de Orientación Visual por el mero hecho de poseer ó no la Entidad algún componente que los calcule y asigne en la Entidad.

Por otro lado se modifica la llamada al método IRenderContext::DrawMesh (dentro del método Draw del componente Visualization3D), pasando ahora como ángulos de orientación del mesh los Ángulos de Orientación Visual.

Y aquí está la clave: que la pelota (es decir, la Entidad Esfera3D) que va a tener un componente que va a calcular definitivamente los Ángulos de Orientación Visual (y establecerlos en dicha Entidad): el componente LógicaPelota, el cual ya existía (gracias a él la pelota se desplaza y ordena realizar la Collision Detection/Response de ésta)... convenientemente modificado para ir calculando en cada Update unos Ángulos de Orientación Visual que hagan el efecto de rodar sobre sí misma y cambiar el sentido de giro cuando choque contra una raqueta. Simple. :P

Todo esto está muy bién, pero... ¿y por qué no cuelgo un vídeo?. Pues porque estoy desde ayer intentando hacer uno con este programa pero, por mucho tutorial y mucha configuración, me salen con un lag que parece que estuvieran reproduciéndose con un gnomo dando pedales. Así que si logro hacer uno decente ya lo colgaré (** EDIT: ir a entrada de 10/09/2011 **).

Hasta otra. :P

martes, 26 de julio de 2011

Carga Multietapa del Nivel y Movimientos de Cámara

music: ALICE IN CHAINS -"Sea Of Sorrow / Bleed The Freak"-

Estos últimos dos fines de semana han sido de lo más fructíferos. : )


Carga Multietapa del Nivel:

Por una parte, he perfeccionado el sistema de carga de un nivel:

-Sacando la orden de carga fuera del Bucle de Mensajes de la Ventana (la carga comenzará ahora habiendo salido de la iteración actual del Bucle de Mensajes).

-Haciendo que los diversos elementos del nivel se carguen en varias iteraciones o etapas de carga (actualmente, cuatro) para que el programa salga de la sección Update del Bucle Principal del juego y entre en la sección Render de dicho Bucle.

-Estableciendo un nuevo estado (M_LOADING_END), el cual será alcanzado al finalizar toda la carga del nivel; el Bucle Principal detectará que se ha alcanzado dicho estado y entonces se alcanzará el estado "Modo Juego" (M_JUEGO);

CONCLUSIÓN: ya no hay que establecer un tiempo concertado de sincronización como comentada en la anterior entrada, para esperar a que el flujo de programa cargue el nivel y abandone el Bucle de Mensajes de la Ventana... sino que los elementos se sincronizarán con el flujo de programa gracias a los estados M_LOADING y M_LOADING_END.


Movimientos de Cámara:

Y por otra... esto.

Lo había intentado antes varias veces, pero esta vez me dije que del pasado fin de semana no pasaba. Y así, tras un exhaustivo análisis del código y los apuntes, al fin ha logrado comprender el modo de funcionamiento de uno de los puntos negros del engine: la cámara.

Ello me ha permitido hacer los añadidos y modificaciones oportunas al código para poder hacer que, al desplazarse la raqueta, se produzca una leve inclinación horizontal y/o vertical de la escena; más pronunciada ó menos cuanto más se aleja la raqueta de su particular posición de comienzo, al inicio de la partida.

Pongo unos screenshots para mostrar cómo queda el invento a partir de ahora.











Hasta otra. :P

miércoles, 20 de julio de 2011

Loading screen

music: GARY NUMAN -"Cars"-

A veces, la mente funciona de un modo extraño. El caso es que el sábado pasado me levanté inspirado y me vino a la cabeza una idea para solucionar una de las cosas que me parecían más feas del juego.

Es algo que sólo sucedía al comienzo de la primera partida. Cuando seleccionas Jugar, mientras se van creando los elementos del nivel (el mundo, las entidades, los componentes asociados a dichas entidades, etc) se producía una especie de disincronía que hacía que, cuando aparecía la imagen del juego, la pelota ya hubiera avanzado un buen trecho. Resultado: la raqueta controlada por el ordenador no podía alcanzarla y se producía un tanto seguro.

Para solucionarlo, he añadido al motor un nuevo estado de "carga de datos" (M_LOADING) que implica que está cargando datos y, durante este estado, el fDelta que se envía al método principal Update de la aplicación es cero. Al seleccionar la opción Jugar pasamos directamente a este estado y, una vez se hayan cargado los datos y haya pasado un determinado tiempo concertado de sincronización, se pasa automáticamente a "Modo Juego" (M_JUEGO).

Mientras se cargan los datos aparece una pantalla de carga; este es el resultado:


¿Funciona?. Pues sí, funciona. Al entrar en "Modo Juego" todo queda sincronizado y la pelota permanece en el centro de la acción, tal y como puede verse en la captura. ¡¡¡Al fin!!!. :D

Pues nada, veamos si la Musa de la Inspiración me visita en otras ocasiones, ahora que llevaba tiempo dándole un respiro a esto. Si hasta estoy pensando en borrar lo que puse el día 1 y todo. XD

Hasta otra. :P

miércoles, 27 de octubre de 2010

Con dos... monitores. :P

music: FIVE HORSE JOHNSON -"Ten Cent Dynamite"-

Es la 01:00 a.m. y en este momento la inspiración para escribir se ha ido ya a la cama, cosa que debería hacer yo también. He logrado terminar el prototipo en el que estaba trabajando en estos días para probar el funcionamiento del juego con dos monitores.


Básicamente lo he realizado así:
  • Se crean dos ventanas que se mostrarán En Modo Ventana (no fullscreen como hasta ahora) y MAXIMIZADAS, consiguiendo así que los elementos 2D queden automáticamente escalados al tamaño adecuado y posicionados en el lugar correcto.
  • Se captura la resolución del Escritorio (del primer monitor) y, dado que al final se especifica a las ventanas que se maximicen, se logra así tener una resolución independiente en cada monitor.
  • Sólo se crea un único Render Device (no hay replicación de vertex buffers ni doble carga de recursos).
  • Existirán dos Swap Chains. La primera es creada automáticamente al crear el Render Device, y la segunda se crea mediante el método GetAdditionalSwapChain del Render Device. A cada una de ellas se le especificará un Render Target distinto.
  • Se ha creado una cámara adicional que sigue a la raqueta controlada por el ordenador, y cuya escena es mandada a pintar a la segunda Swap Chain.
  • Hay dos pasadas de Render, una para cada Adaptador.

Como prototipo que es no he prestado mucha atención a la calidad de la implementación y ciertos detalles son realmente mejorables. En especial me preocupa el hecho de abandonar el modo fullscreen y el hecho de llamar a los métodos BeginScene / EndScene dos veces, pues es un hecho que va bastante lento. Si a alguien se le ocurre o sabe una forma mejor de hacerlo que lo comente por aquí, soy todo ojos. ; )

Hasta otra. :P

miércoles, 25 de agosto de 2010

Objetos Producer-per-Platform

music: THE PRETTY RECKLESS -"Make Me Wanna Die"-

Durante todo este tiempo que he estado sin actualizar he estado realizando unos cambios a nivel de estructura interna del engine e incoporádole algunas cosillas. Una de ellas es la inclusión de una nueva clase de la que heredarán sucesivas clases hijas con las que se crearán un nuevo tipo de objetos: los Producer-per-Platform.

Un Producer-per-Platform es un objeto servidor, encargado de crear y entregar al objeto cliente objetos dependientes de plataforma. Entiéndase por plataforma una parte del sistema o el engine, normalmente situada en lo que llamaríamos "bajo nivel", que puede variar según nuestras preferencias o disponibilidad (S.O., Bibliotecas de creación de ventanas, de audio, gráficas, etc.). Según la clase a la que pertenezca el objeto Producer-per-Platform, éste creará unos u otros objetos "Product" (ventanas, render devices, audio devices, etc.). Y la clase concreta del Product que creará se decidirá, por directivas de preprocesador, en tiempo de compilación.

Debe aclararse, por tanto, que los objetos Producer-per-Platform (desde ahora, Producer) no son los dependientes de plataforma, sino los objetos product que crearán (desde ahora, Product).

La relación entre el Producer y el objeto cliente (desde ahora, Cliente) será de USO, y normalmente adoptará la siguiente forma:

0) El Cliente poseerá, como atributo privado, un puntero polimórfico de la clase de Product que el Producer debe crear.

1) La generación del Product se producirá dentro de un método del Cliente; en la implementación de dicho método el cliente poseerá, como objeto estático local, el Producer necesario.

2) El producer creará el product dependiendo de la directiva de precompilador actualmente establecida para ese Product (estará establecida en la propia implementación del Producer) y entregará el Product en el puntero polimórfico del cliente como valor de retorno.

3) La implementación del método finalizará, y el Producer quedará destruído. El Cliente tiene comunicación directa con el Product a través del interfaz de éste último.

4) La responsabilidad de ordenar la inicialización del estado del Product recae en el Cliente; éste será quien llamará al método Init del Product.

5) Asimismo, la responsabilidad de finalizar la vida del Product recae también en el cliente; éste será el encargado de destruir el Product, liberando la memoria que ocupaba.

Como puede intuirse un Producer NO ES UN MANAGER; es un concepto mucho más sencillo que el de un objeto Manager que carga recursos en memoria, lleva la cuenta de la cantidad de instancias (referencias) de recurso que hay empleadas en el sistema y, cuando el número de referencias de un determinado recurso llega a 0, elimina realmente dicho recurso de la memoria.

a) El Producer surge de la necesidad de crear objetos (Product) dependientes de plataforma. SOLAMENTE en este contexto tiene sentido el Producer. De saber a ciencia cierta que va a emplearse únicamente una plataforma el Cliente generaría el Product de forma directa, sin Producer.

b) Gracias a los Producer tendremos un mayor control sobre el engine sabiendo dónde hay establecidas directivas de preprocesador que determinan compilación para una u otra plataforma: en los módulos del los Producer.

He creado ya un Producer de ventanas que devuelve al Cliente (que en este caso es el Objeto Aplicación) una ventana creada mediante la biblioteca Win32. Ahora podría crearse una clase ventana creada mediante, por ejemplo, la biblioteca GLUT y el mismo Producer de ventanas creará y devolverá a la Aplicación una ventana creada mediante GLUT con sólo cambiar la directiva de precompilación en el Producer de ventanas (de crear ventanas Win32 a crear ventanas GLUT).

El siguiente paso en el que emplear esta técnica es en el render device. A ello voy.

Hasta otra. :P

martes, 25 de mayo de 2010

Probando nVidia PerfHUD

music: BLUE ÖYSTER CULT -"See You In Black"-

En estos días he estado probando nVidia PerfHUD (versión 6.62), un programa tipo profiler que puede ayudar a detectar cuellos de botella en el pipeline gráfico. Quiero ver si la caída en el framerate que comenté en la anterior entrada tuviera también algo que ver con un mal uso de Direct3D.

De momento no he tenido mucho éxito. Durante la instalación, se me indica que me van a ser actualizados los drivers de la tarjeta gráfica... el problema es que mi tarjeta está ahora mismo un poco anticuada y no pasa la prueba del logotipo de Windows con esos últimos drivers con lo que el asunto, como puede esperarse, no pinta muy bien. Y así ha sido: el programa se cuelga cada dos por tres, y el ordenador se ha quedado algo inestable hasta que le he vuelto a reinstalar los drivers anteriormente instalados.

Total, que voy a probar con una versión más antigua, a ver si así hay más suerte.

Hasta otra. :P

(** EDIT 31/05/2010 **) Ídem de ídem con la versión 5.2, y ya no se ven versiones más antiguas del entorno. Así que este asunto, al menos por el momento, así se va a quedar... :(

miércoles, 12 de mayo de 2010

Super Pong y el Modo Debug

music: WENDY O'WILLIAMS singing -"It's My Life"- / KISS -"Rock and Roll Hell"-

De vuelta al trabajo. Ya estoy dándole duro otra vez al Super Pong; de ahora en adelante voy a intentar actualizar más a menudo, como muy tarde cada dos semanas; aunque será como todo, dependerá de los progresos y cómo me cunda el asunto. : )

Durante la semana anterior he estado arreglando algunas cosas y haciendo que la información de los AABBs quede guarde como un centro y unos "radios" o Halfwidths en X,Y y Z; de esta manera, se guarda la mitad de la longitud (y no la longitud entera) en X,Y y Z del AABB, lo que ahorra tener que dividir entre 2 (o multiplicar por 0.5) en un montón de sitios dentro del juego. : )

He estado marcando objetivos y, entre ellos, está el hecho de crear algún tipo de sombra o proyección de la pelota contra las paredes del recinto, para que el jugador pueda hacerse una mejor idea de lo cerca que está la pelota de la raqueta suya. También voy a meterle algo de musica, aunque aún no he logrado crear el efecto cross-fading que me había planteado.

Sin embargo, sigo arrastrando todavía un problema "curioso": la diferencia brutal de rendimiento entre crear un ejecutable en Release y ejecutarlo separadamente del entorno de programación en Modo Pantalla Completa... y ejecutar el juego en Debug, dentro del entorno de programación(sea en Pantalla Completa ó en modo Ventana). Sigue sucediendo lo mismo: en modo Debug el rendimiento SE VA A PIQUE. Tal es la caida que no se puede llevar una partida con normalidad, lo cual hace muy difícil la depuración del código.
No sería tan importante si, estando en Debug, mantuviera un rendimiento aceptable en Pantalla Completa pues el asunto se resolvería mediante dos monitores (como en el trabajo: uno para tener a la vista el código, otro para ver la ejecución del juego). Pero no, es algo que he estado probando este fin de semana y se ralentiza igual.

El cuello de botella parece producirse en los componentes IA_NPC y LogicaPelota, posiblemente por las llamadas al componente Colision3D; es por ello que deseo modificar el sistema de Collision Detection and Response y hacer algo que sea más eficiente (aparte: solucionar los eventuales problemas con el tunnelling).

En fín, iré compaginando esto con las otras cosas que quiero hacer y veamos en qué acaba el asunto en las próximas semanas. Esta imagen lleva congelada demasiado tiempo.


Hasta otra. :P

jueves, 29 de abril de 2010

¿Seguridad en el cálculo ó Fallo en la programación?.

music: MOBY -"Natural Blues"-

Bueno, pues al fín terminé de vér cómo funciona el sistema de Collision Detection. Pero hay algo que no termino de enterarme, y no sé si lo están haciendo por seguridad ó es un bug de la demo. Anoto aquí la duda sobre el asunto, el cual explico resumidamente:

1-Por una parte tenemos 8 vértices, y tenemos que comprobar que alguno de ellos, está ó no dentro de un espacio cuadrado delimitado por cuatro vértices que están dentro de un plano. Para ello vamos a tener un bucle for que lo que va a ir haciendo es coger cada uno de dichos vértices (de entre los 8 a comprobar), hallar la proyección ortogonal de ellos sobre el plano, y comprobar si dicha proyección está ó no dentro del espacio cuadrado del plano. OK

2-En cuanto se demuestra que la proyección ortogonal sobre el plano de uno de estos 8 vértices está, además, dentro del espacio cuadrado, se procede a calcular la distancia existente entre el vértice y su proyección; si es aproximadamente 0, se considera que el vértice mismo está colisionando contra ese plano. Y además, se rompe el bucle for y no se siguen comprobando el resto de los vértices. También OK.

3-Hasta aquí, todo bien. El asunto es que al citado bucle for de los 8 vértices lo engloba un bucle infinito; si se cumple todo lo del paso 2, se incrementará un contador... ¡PERO PERMANECEREMOS DENTRO DEL BUCLE INFINITO!. En la siguiente iteración de dicho bucle infinito NO se pasa a evaluar otro grupo de 8 vértices, sino los mismos; no se cambia de plano (ni de su correspondiente espacio cuadrado, sigue siendo el mismo). Con lo cual volverá a evaluarse y volverá a salir, digo yo, EL MISMO VÉRTICE; se volverá a incrementar el contador, se rompe el bucle for de los 8 vértices, volvemos a entrar en el bucle infinito...

4-...y así, hasta que el valor almacenado contador sea mayor que 20, y ahora sí, salimos del bucle infinito. Momento en que, si bien los 8 vértices van a ser los mismos, se toma un nuevo plano (el cual tendrá su correspondiente espacio cuadrado definido por cuatro vértices que están dentro de él) y se pasa al punto 1.

OK, pero... ahí está la duda: hasta llegar al punto 4 ,hemos realizado el proceso explicado en el punto 3... ¡¡¡20 VECES!!!. Y lo que no llego a ver es si esto algo que se desea realizar de esa manera por alguna razón (quizás por seguridad, para evitar posibles errores de punto flotante, o lo que sea)... o es directamente un fallo en la programación.

Pero bueno, ahí se va a quedar la cosa; ya me enteraré otro año.

Hasta otra. :P

viernes, 23 de abril de 2010

No eran proyecciones, sino ecuaciones de planos...

music: COPTIC RAIN -"Barefoot / Perfect Lie"-

Estos días los he pasado averiguando cómo funcionaba el sistema de C0llision Detection de la famosa demo de los Donuts, algo en lo que me había quedado encasquillado en otra ocasión... pero esta vez he tenido más éxito.

El problema radicaba, principalmente, en el enfoque. Mientras veía el funcionamiento del método encargado de realizar la Colission Detection estaba entendiéndolo como una serie de proyecciones de cada uno de los vértices del Bounding Box del primer objeto (aquel que realiza la Collision Detection con el resto de objetos) sobre las normales de las caras del Bounding Box del "segundo objeto (en cada caso, éste último será el objeto digno del análisis de Collision Detection por parte del primer objeto; cabe destacar que Objeto 1 siempre será distinto de Objeto 2).

Bajo esta perspectiva la explicación "casi funcionaba", pero me fallaba algo: no comprendía cómo con una simple resta del vértice actual del Objeto 1 sobre lo que yo consideraba "la proyección de él mismo" sobre la normal de la cara actual del Objeto 2 hallábamos ya la proyección de dicho vértice actual sobre la cara actual del Objeto 2.

La solución ha venido de la mano del libro "Real-Time Collision Detection", del cual ya puedo decir que ha merecido la pena el dinero invertido aunque sólo sea por esto (no obstante, espero tener más razones aún en el futuro). :P

Como preliminares:

1) Volví a repasar algo el tema de vectores, recordando un elemento clave: un vector representa una magnitud, dirección y sentido... pero NO posee dirección.

2) Volví a ver las ecuaciones del plano, y las propiedades asociadas a dichas ecuaciones.

A partir de aquí, lo ví claro: lo que verdaderamente se estaba haciendo NO era proyectar sobre las normales... sino hallar el punto más cercano de cada vértice del Bounding Box del Objeto 1 a cada una de las caras del Bounding Box del Objeto 2, tratando dichas caras como planos mediante la ecuación del plano:

n · X - d = 0, siendo:

--> · la operación Producto Escalar,
--> n, la normal de la cara actual del Objeto 2,
--> d = n · P, tal que P sea un punto perteneciente a la cara actual del Objeto 2.

Dicho punto más cercano, si prefiere verse así, puede verse directamente como la proyección del
vértice actual del Bounding Box del Objeto 1 sobre la cara actual del Bounding Box del Objeto 2.

Desde aquí:

-Si a dicho punto más cercano lo llamamos R y al vértice actual del Bounding Box del Objeto 1 lo llamamos Q,

-Y si tenemos en cuenta que las normales de los Boundings van a estar normalizadas...

... la cosa queda así: R = Q - [(n · Q) - d]n <-- Esta es la citada resta, ahora sí teniendo sentido, que comentaba más arriba; esto es lo que verdaderamente se hace en la demo.

Y ahora sí: una vez hallado ese punto más cercano a Q, R (o esa proyección de Q, R, si prefieres verlo así), se comprueba que se encuentra dentro de los límites de la cara actual del Bounding Box del Objeto 2; si lo está, se puede decir "con la boca pequeña" que ambos objetos están colisionando; si no, debe proseguirse con el análisis.

Pues ya quedan poquitas cosas de las que quería ver de esta demo. Tengo que ponerme a realizar un recuento de objetivos a realizar para Super Pong.

Por cierto, todo esto me ha dado una idea acerca de algo que tenía ganas de hacerle: poder proyectar puntos, líneas o algo así desde la posición de la pelota hacía las paredes del recinto para que el jugador pueda hacerse una mejor idea sobre la distancia a la que se encuentra la pelota de la raqueta, que dada la cámara a veces no sabes muy bién dónde está.

Hasta otra. :P

miércoles, 24 de marzo de 2010

Se me enfrian los Donuts... :P

music: none

¡Bueno, al fin!. Ya he terminado de ver los artículos de física que estaba viendo. La verdad es que el esfuerzo ha merecido la pena pues gracias a ellos me he enterado de un montón de cosas pero para el lector no familiarizado con la física (o que ya no lo esté, como es mi caso) resultan bastante duros.

En cualquier caso, he repasado/entendido cosas como el Método de Euler, la fórmula de Rotación 3D de Ollinde Rodrigues, las Fuerzas y los Pares de Torsión ("Torques" en ingles), el Momento de Inercia, la descomposición de la velocidad que lleva un rigid body que se traslada y rota alrededor de su Centro de Masas por el Teorema de Chasles... un par de "misterios sin resolver":

-Similarity Transform: Un proceso que permite transformar la orientación de un body en coordenadas de mundo. Se transforman los vectores de la orientación mediante una matriz que es producto de tres matrices:
1)Matriz traspuesta de la "matriz de paso de coornadas locales a coordenadas de mundo" --> Al estar hablando de un sistema ortonormal, la matriz traspuesta será la inversa la matriz anteriormente mencionada, con lo que estamos hablando de la "matriz de paso de coordenadas de mundo a coordenadas locales".
2)Matriz de transformación --> Debido al paso 1) la orientación ya está en coordenadas locales; así que transformamos la orientación en coordenadas locales.
3)Matriz de paso de coordenadas locales a coordenadas de mundo --> la transformación vuelve a pasarse a coordenadas de mundo.

-Matriz Inertia Tensor; algo que ya había visto rondando por algunas demos , y que viene a ser la representación en 3D del Momento de Inercia en 2D.

En fin, digamos que en este momento me siento preparado para entender la parte de simulación física de la demo de los Donuts, y que al comienzo parecía una hardcorada total. Me pondré con el asunto en estos días, pero eso sí... voy a tener que recalentar un poco los Donuts. :P

Hasta otra. :P


P.D. Y mañana sale el Runaway a la venta...

lunes, 8 de marzo de 2010

The Black Art Of 3D Game Programming

music: FATBOY SLIM -"Right Here, Right Now"-

Albert Einstein: "No te metas en la cabeza aquello que puedas meterte en el bolsillo".
Anderson_JAG: "Sr. Einstein... ¡¡¡¿ha visto usted el tamaño de este libro?!!!". :P


Ya tocaba hablar de uno de mis libros favoritos, "The Black of 3D Game Programming" de A. LaMothe. Gracias a este libro, el cual llevo estudiando desde hace aproximadamente un año, he podido llegar a enterarme de un montón de cosas de las que, de otra forma, hubiera sido imposible haberme formado una idea acerca de cómo se producían, de su funcionamiento.

A nivel general, he de decir que cuando quiero estudiar o informarme sobre un tema relacionado con programación de videojuegos... donde me gusta mirar es en los libros. Sean mejores, sean peores... no importa. Siempre le daré, al menos, una oportunidad a un libro.

Así que, en estos años de andadura en el mundillo de la programación de videojuegos, me he encontrado principalmente con tres tipos de libros:

TIPO 1: Aquellos que te sueltan el rollo macabeo, te plantan los cuatro pseudocódigos generalistas y/o formulones, y... ¡ahí lo llevas, majo!.

TIPO 2: Ahora nos vamos al otro lado. Libros muy orientados a realizar algo muy concreto, empleando una tecnología muy concreta, pero que se quedan cortos no ya a la hora de profundizar en la materia, sino de explicar la materia misma. Qué se esta haciendo en esta o aquella parte del código, porqué llamar a estas fórmulas de Direct3D, OpenGL o lo que sea, y no a estas otras; porqué llamarlas en este orden. Porqué los parámetros empleados, y no otros. Etc.

TIPO 3: El tipo más minoritario. Hay una explicación más o menos extensa y difícil pero para nada farragosa o intragable; y si aún así, por mucho que te hayas esforzado, no te enteras de nada... no te preocupes: porque inmediatamente detrás va a venir siempre una pequeña demo, lista para ser compilada y ejecutada, con la que vas poder entender qué narices estaba diciendo el autor en este o en este otro lugar realizando un tracing de su funcionamiento.

Es fácil adivinar que enmarco a este libro dentro del Tipo 3. Cuando hablo de demos, no me refiero a una demo de final de capítulo... ¡sino a VARIAS demos dentro de un mismo capítulo!. Con lo cual puedes volver atrás en la teoría y decir: "¡Claro, ahora lo entiendo...!".

Y gracias a ello, he podido llegar a comprender (y más aún, VER) cosas como pintado de líneas, relleno de polígonos mediante scan conversion, shading Flat y Goraud, Z-Buffering y BSP partitioning. De acuerdo, existirán técnicas mejores que las explicadas en este libro para realizar las citadas tareas. De acuerdo, fastidia mucho que haya temas que LaMothe comenta explicitamente que no se van a tratar en el libro; de acuerdo, la demo final del tema de Voxel Graphics dista mucho de ser... como se dice ahora, "resultona". Pero no menos cierto es que si te encuentras atorado, si por alguna razón no logras comprender determinados aspectos relacionados con álgebra, con gráficos 3D, con estructuras de representación de un entorno 3D, o inlcuso con recursividad... te puede venir muy bien. De este libro se puede hablar muy bien ó muy mal. Depende del prisma. Pero dado que a mí me está sirviendo, aunque no sea de una forma directa... seré de los primeros.

CONCLUSIÓN:

Si no te apetece "perder el tiempo con algo viejo y obsoleto" porque estás buscando explicaciones sobre tecnología next-gen, o un manual para hacerte un engine que supere a Unreal Engine... tienes toda la razón, "no pierdas el tiempo".

Si consideras que los engines no son como las hortalizas (que las plantas, y crecen) sino que debe haber una base "soterrada" bajo capas y capas de tecnología de rimbombantes nombres y te apetece conocer un poco qué hay bajo todo ello y formarte una idea al respecto, aunque no pueda ser una idea exacta... este libro te puede interesar.

E insisto: si te encuentras atorado, si por alguna razón las transformaciones 3D, el pipeline gráfico, Direct3D u OpenGL "no te entran", si notas que te falta perspectiva de base... este libro te puede ayudar mucho si le derivas parte de tu tiempo; leete el Cap.1, de ahí salta al 10 y... ¡a trabajar!.

Hasta otra. :P


(** EDIT:  10/16/2013 **)

Thanks for signing me the book, Mr. LaMothe. : )


miércoles, 17 de febrero de 2010

Physics, Collision Response and Runaway

music: AMERICAN DOG -"Dog Will Hunt"-

Veamos... estas últimas semanas han sido muy productivas a nivel de trabajo. He estado analizando el funcionamiento de la demo de los Domuts, especialmente el sistema de detección de colisiones, y la verdad es que me he enterado de bastantes cosas; pero tocar la parte relacionada con la física y el sistema de collision response aún me he parecido delicado con lo que, a riesgo de perderme sin entender ni papa, me he metido primeramente a ver unos artículos sobre Rigid Body Dynamics escritos por C. Hecker (link al website personal) que me están resultando muy, muy útiles y didácticos. Este señor ha logrado resumir en cuatro artículos... diría que entre 6 y 7 temas de física (Cinemática y Dinámica) de un libro de Bachillerato; y con la ayuda de las demos que acompaña, puedes ver perfectamente donde y cómo se emplea cada elemento, cada fórmula... en definitiva, cómo encaja todo. Hoy he terminado de ver los relacionados con 2D y he empezado a ver el de 3D (¡menos mal que avisa que la mayoría de fórmulas de cinemática ya vistas valen también para 3D, jejejeje...!). ¡¡¡RECOMENDADOS!!!.

Respecto de Runaway: A Twist of Fate... he estado bastante tiempo sin comentar nada porque, aparte de estar muy ocupado con estos y otros asuntos, era un tema que estaba copando toda la atención del blog, y tampoco quería eso. Me hice este blog basicamente como un "Dev Blog", para ir comentando mis progresos en programación, en mis proyectos personales, señalar artículos y cosas interesantes relacionadas con el mundo de la programación de videojuegos que fuera utilizando, recomendar libros del tema, etc. Por eso he dejado al Runaway más de lado, aunque han ido apareciendo noticias importantes sobre él (tan importantes como que será publicado en España el próximo 25 de marzo); podeis echar un vistazo a la página de Runaway: A Twist of Fate en español y también a la nueva página de Pendulo Studios, pero vamos, si habeis estado atentos seguro que todo esto ya lo habeis visto.

Hasta otra. :P

domingo, 31 de enero de 2010

DONUTS IV: REVENGE OF THE SPACE TORUS

music: W.A.S.P. -"The Heretic (The Lost Child)"-

Durante estos días me he puesto a mirar una demo a la que llevaba tiempo queriendo echarle un ojo: "Donuts IV: Revenge of the Space Torus".

Se hallaba entre los samples de DirectX SDK Summer 2003 e, inexplicablemente, fue eliminada en versiones posteriores. Me estoy centrando, sobre todo,en determinar el funcionamiento del Sistema de Collision Detection and Response, el principal elemento de Super Pong que quiero modificar, y que se me atraganta desde hace muuuuucho tiempo. Las explicaciones que he encontrado en Metanet Software , en Gamasutra y en los libros "Real-Time Collision Detection" y "3D Math Primer for Graphics and Game Development" estan muy bien... pero sigo sin saber muy bien qué hacer con todo ello, sin ver cómo encaja todo ello... sin ver "the big picture", vamos.

También he estado entretenido "rescatando" la posibilidad de pintar, estando en Modo Debug, los Bounding Box de los enemigos y los donuts que lanzas como proyectil (digo rescatando porque pareciera que dicha opción hubiera quedado "enterrada" en el código, sin posibilidad de dar una orden de activación de dicha característica desde el exterior). Así que la he dejado que puedas activarla/desativarla pulsando una tecla estando en Modo Debug. Por aquí he dejado un par de capturillas.


Gracias a "The Black Art of 3D Game Programming" de A. LaMothe (un libro al que le debo mucho y del que ya hablaré en una entrada próxima) he podido enterarme sin mucha dificultad de cómo se están calculando las normales de las seis caras de los Bounding. Por otra parte, parece que los únicos vértices del bounding que emplea para comprobar son los ocho de las esquinas (en ese sentido probaré a añadirle los seis puntos céntricos de cada cara, para refinar las comprobaciones).


Lo dicho, espero poder enterarme de más cosas y poder algún día resolver el problema de las colisiones y el "tunnelling" de Super Pong (mención aparte, el framerate en el portátil se me va a pique). :P

Me marcho otro rato a la Biblioteca, que es temporada de estudiantes y también abre los domingos.

Hasta otra. :P