
Hace bastante tiempo que no escribo por estos lares. Pero eso no significa que no le haya dado caña, ni mucho menos, al desarrollo del motor. En estas semanas he tenido bastantes problemas de tiempo, pero aun así he sacado lo suficiente para seguir avanzando sustancialmente. Por otro lado, he encontrado problemas grandes, sobre todo con el tema del renderizado, y me he decidido finalmente a hacer una refactorización final, con los errores aprendidos y asumidos, del rendering pipeline. De este modo CTexture y CTextureManager desaparecer. Además, a partir de ahora SDL, en lo que a gráficos respecta, sólo será un intermedio entre el motor y OpenGL.
OpenGL >>>>> SDL_Surface
Uno de los objetivos del proyecto es conseguir que el motor sea multiplataforma, es decir, compile tanto en Windows, como en Linux y Mac OSX. Este último SO lo dejo para el final, pero Linux y Windows, especialmente Windows, requieren de muchas optimizaciones, no sólo en código, sino a la hora de compilar. Siendo un desarrollo inicial en Linux, el motor funciona bastante bien en esta plataforma. Sin embargo, al compilar en Windows, además de tener que hacer modificaciones en el código (el compilador de C++ de Windows es menos permisivo que gcc), hay que elegir muy bien el tipo de compilación que queremos y sus parámetros. En Visual Studio 2010 mayormente se trabaja en modo Debug, pero este entorno no tiene apenas optimizaciones de compilación, linkeo, etc. y trabajar así es muy lento. No obstante, compilar bajo Release, con todas las optimizaciones activas, etc. da unos resultados aceptables.
La decisión crítica en este proceso fué dejar de usar SDL_Surface, la unidad básica de gráficos de SDL. Puesto que SDL es mucho más lento que OpenGL, decidí dejar de usar los SDL_Surfaces como elemento básico de renderizado en el motor. Sin embargo, usaré los SDL_Surface como el paso intermedio para cargar gráficos en la memoria. Puesto que la interfaz de uso de SDL es más sencilla que OpenGL, la usaré para hacer una carga inicial de los recursos (además, los recursos serán imágenes de formatos normales, jpg, png, etc. es decir, no habrá formato propio por el momento); una vez hecha la carga del recurso, transferirla a OpenGL para que éste gestione su renderizado. De este modo uso lo mejor de ambos sistemas. La penalización es la sobrecarga a la hora de cargar recursos, penalización aceptable si tenemos en cuenta que el sistema optimiza (a través de varios flags) la carga dinámica de los recursos:
-
Mantener en memoria lo que ha cargado hasta que haya una señal específica de limpieza, por ejemplo al final de un State o de un World.
-
Hacer la carga en memoria sólo de aquellos elementos que se está visualizando en memoria: muy óptimo en cuanto a memoria, pero bastante lento si tenemos muchos elementos cambiantes en el juego.
-
Mi solución preferida, un híbrido entre ambos: eliminar aquellos elementos que no se están visualizando, pero no tocar aquellos a los que previamente hemos asignado una flag indicando que vamos a usarlo a menudo en el juego. Por ejemplo, en un juego de disparos, añadiríamos a la lista de recursos itinerantes las balas, los efectos, etc. que si bien no se están mostrando continuamente, si que es susceptible de aparecer a menudo. Por otro lado, elementos circunstanciales (un jefe final, por ejemplo), podemos asignarles memoria sólo cuando estén presentes en la pantalla.
De este modo me ahorro el poco intuitivo sistema de carga de recursos en OpenGL (http://www.nullterminator.net/gltexture.html), lo cual añade facilidad al desarrollo. Para el futuro se queda el eliminar esa dependencia de SDL, y añadir soporte, si bien ya tiene para gráficos comunes, para formatos específicos definidos por el usuario.
Compilar en Windows se convirtió en toda una odisea
Modelo de Entidades
Por otro lado, el sistema de entidades ha quedado más o menos diseñado y resuelto, a falta de añadir soporte para cámaras y demás, y hay ejemplos de código en el repositorio. Todo lo que se mostrará, exceptuando mapas y texturas superpuestas (UI, menúes, etc.), serán entidades. Es decir, un objeto usable, el personaje principal, los enemigos, las partículas, efectos, etc. estarán resueltos sobre el modelo base de entidad en el motor. De este modo, y como ya expliqué en la entrada anterior, podremos hacer uso intensivo de la herencia para crear entidades comunes con pequeños cambios, etc. todo esto forma parte del usuario. El modelo base de entidades resuelve lo siguiente:
-
Proporciona interfaces de métodos comunes a todas las entidades: cargar un recurso, mostrar/ocultar, mover de posición, asignar una cámara, comprobar colisiones, etc.
-
Se garantiza que ciertos métodos (OnRegister, OnCameraChange, OnDelete, OnEvent) serán llamados cuando se produzca, lo cual nos permite definir distintas acciones para las entidades que hereden de aquí.
-
La carga/descarga de recursos se realiza de forma transparente, y el motor es el que se encarga de asignar/liberar memoria de una forma óptima.
Podéis ver el código de este modelo base en CEntity y CEntitymanager.
CResourceManager
Una parte esencial de los motores gráficos es la gestión de los recursos de manera eficiente. Esto se divido en 2 grandes bloques: cargar en el programa la lista de todos los recursos que serán necesarios a lo largo de la ejecución del juego, y cargar en memoria / borrar aquellos elementos que se estén usando en el momento de forma dinámica y eficiente. En el primer bloque, necesitamos primeramente definir la manera en que proporcionaremos estos recursos al motor. En el caso de SeventhEngine, se usará un archivo XML con todos aquellos recursos definidos por el usuario. Tendrá el siguiente formato:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?xml version="1.0" encoding="utf-8"?> <!-- Resources XML file. Everything used within the game will be loaded from here --> <resources> <!-- Single textures, they can be used to show directly in the screen or to source a animation --> <textures> <texture name="ryu" src="./resources/textures/ryu.png" format="png" /> <texture name="ken" src="./resources/textures/ken.png" format="png" /> <texture name="tile" src="./resources/textures/tiletest.png" format="png" /> </textures> <!-- Tilesets, used to group graphic assets in a single file. Tilesets will be divided in tiles and later can be used to display in the screen or to source a animation --> <tilesets> <tileset name="maintile" src="./resources/textures/tile.png" format="png"> <!-- Every tile from the tileset, with specific width/height and coords --> <tile name="main_player_frame01" x="100" y="100" width="100" height="100" /> </tileset> <tileset name="enemy_knight_01" src="./resources/tilesets/enemy_knight_01.png" format="png" /> <tileset name="ui" src="./resources/tilesets/mainui.png" format="png" /> </tilesets> <!-- Animations, to be attached to a entity and displayed in the screen (like a effect, player or background) 'framerate' property is the default one, but can be changed at runtime --> <animations> <!-- Animation type - texture, loads frames from a specific texture --> <animation type="texture" name="clouds_level1" framerate="500"> <!-- each frame is a textured defined previously --> <frame texture="zelda_overall_01" /> <frame texture="zelda_overall_02" /> </animation> <!-- Animation type - tile, each frame is a specific tile from a tileset --> <animation type="tile" name="main_player_walk_right" framerate="500"> <frame tileset="main_player" tile="main_player_frame01" /> <frame tileset="main_player" tile="main_player_frame02" /> <frame tileset="main_player" tile="main_player_frame03" /> <frame tileset="main_player" tile="main_player_frame04" /> <frame tileset="main_player" tile="main_player_frame05" /> </animation> </animations> <!-- Maps, loaded from an external .xml file in the Tiled XML format --> <maps> <map name="map_level1_intro" src="./resources/maps/level1/intro.xml" format="xml" /> <map name="map_level1_dungeon" src="./resources/maps/level1/dungeon.xml" format="xml" /> </maps> </resources> |
Como véis, es bastante intuitivo y fácil de usar. Con esta información, cargada al inicio del motor, podremos usarla desde todos los módulos del motor para cargar nuestros recursos cuando los necesitemos. Por ejemplo, si tenemos una entidad, que en principio sólo usará una textura como elemento gráfico, desde el ResourceManager tendremos que hacer:
|
1 |
ResourceManager->LoadTexture("nombre_de_la_textura"); |
Si a lo largo del juego, necesitamos asignar un animación a la misma entidad, haríamos:
|
1 |
ResourceManager->LoadAnimation("nombre_de_la_animación"); |
La interfaz es completamente transparente. El gestor de recursos se encargaría de enviar una señal al rendering pipeline de qué elementos tiene que renderizar, de si se ha dejado de usar una textura (y en este caso, comprobar si hay más elementos con la misma textura usándose, etc.), y varias opciones más, de manera que la carga / descarga de recursos se hace de manera transparente. Esto es importante, porque, además de que el usuario no tiene porqué saber nada de como se gestionan los recursos (excepto en casos especiales en dónde una acción del usuario es necesaria para que el motor sepa qué optimizar o no y que dependen del tipo de juego que se tenga que desarrollar), porque ocultar esa información nos permite realizar optimizaciones internas, o incluso cambiar el sistema por completo. Este es un concepto muy importante del desarrollo OOP, Information Hiding (http://en.wikipedia.org/wiki/Information_hiding).
Hay varios casos especiales, como el de los mapas, que usarán un sistema distinto, y que explicaré más detalladamente en otras entradas.
Eso es todo por hoy. Espero volver a coger soltura en las entradas a partir de la semana que viene, con el objetivo de 1 entrada semanal. Además, la primera demo del motor deberá estar disponible para principios de año, y el juego elegido será un Pong. Si tenéis alguna duda, ya sabéis, a los comentarios