genbetadev / Genbeta-Dev-Engine

Desarrollo de un Game Engine básico sobre C++ y SFML 2.1
MIT License
63 stars 32 forks source link

Gestor de recursos #4

Open adrigm opened 10 years ago

adrigm commented 10 years ago

Es necesario un gestor de recursos para facilitar la carga de los mismos de una manera eficiente y sin desperdicio de memoria.

Los recursos de la primera versión serán:

ArnauPrat commented 10 years ago

Hola adri,

Tienes en mente algún tipo de estructura o funcionalidad especial? Es que yo tengo uno hecho (habría que adaptar algunas cosillas) de un proyecto similar a este en el que estoy trabajando cuando puedo desde hace mas o menos un año. Básicamente mi gestor es una clase templatizada, cuyo parámetro de template es el tipo del recurso a cargar. Recurso es una clase base con un método virtual puro Load. Cada implementación de recurso implementa la carga. Luego el gestor es el que se encarga de gestionar el que no se cargue el mismo recurso mas de una vez. Un recurso se identifica con el nombre del archivo. El gestor devuelve una estructura que hace la función de handler del recurso, también templatizada para aprovechar el tipado estático. A esta estructura (es muy liviana, entre 8 y 12 bytes, se podría comprimir mas), se le puede pedir el recurso. De esta manera, nos ahorramos el tener stale pointers, y si por algún caso la memoria destinada a recursos fuera limitada, la recarga del recurso seria transparente.

No se si tenias en mente algo así, o algo más simple o algo totalmente distinto y mas complejo.

Gracias,

Arnau.

adrigm commented 10 years ago

@ArnauPrat Por ahí, van los tiros. Solo que hay que tener en cuenta varias cosas:

Pon un ejemplo de uso del sistema que tienes pensado, como se usaría en un juego para ver la idea.

DavidBM commented 10 years ago

Sobre el identificador, la propia clase puede crear uno cuando lo necesite, no? Usar una cadena de texto para identificar me parece muy poco óptimo. Aún más cuando se tendrá que crear un algoritmo de busqueda con cadenas en el gestor. Creo que se puede ahorrar si el identificador lo da el mismo gestor con un tipo propio.

adrigm commented 10 years ago

@DavidBM la cuestión es que como lo haces? Date cuenta que el usuario haría algo como esto

assetManager<sf::Texture>.get("Data/images/player.png")

Por lo que el identificador debe ser la cadena

Se podría hacer algo como

assetManager<sf::Texture>.load("Data/images/player.png")

que lo cargara y devolviera un tipo de dato expecífico, pero no creo que sea lo más optimo.

danigomez commented 10 years ago

A mi se me ocurre usar internamente en Texture el hash del string como identificador, y se vuelve a hashear para saber a que recurso te estás refiriendo cuando indicás el path del archivo, assetManagersf::Texture.get("Data/images/player.png") -> internamente Hash("Data/images/player.png") como identificador Y así la búsqueda se hace con enteros.

texter commented 10 years ago

Y no se puede utilizar el identificador del fichero que da el S.O.?

adrigm commented 10 years ago

@LeonardoJegigzem usar hash de cadenas es una solución, pero tendríamos que implementar algo simple. También está el problema de que no siempre es posible obtener un hash único, pero son casos remotos que se podrían asumir.

@texter Para un proyecto multiplataforma como este no lo veo.

DavidBM commented 10 years ago

El identificador de fichero es viable para varias plataformas? (Linux, Windows, Mac)

DavidBM commented 10 years ago

De todas formas, el usuario se guardará su referencia al fichero en una variable, no? O vamos a hacer que cada vez que quiera acceder al fichero tenga que pasarle el String?

e-osuna-g commented 10 years ago

Creo que el dice para hacer búsquedas, osea que el gestor los tenga en un arreglo o algo, para no acceder a ellos con su string lo cual seria mas lento que un arreglo o vector, (al menos eso fue lo que creí el quiso dar a entender)

texter commented 10 years ago

@adrigm no? porqué? que yo sepa los tres sistemas usan sistema de ficheros con i-nodos y si no voy muy equivocado en el i-nodo se guarda un identificador del i-nodo (que es el identificador del fichero), que (si no voy muy mal) es un numero único.

adrigm commented 10 years ago

@texter Explica un poco mejor como sería tu implementación.

danigomez commented 10 years ago

@texter Pero si no me equivoco, no existe una interfaz común en Linux, Mac y Windows para obtener el identificador del fichero, o si??, en todo caso habría que hacer un wrapper para que use la manera que corresponde según el SO, no??

adrigm commented 10 years ago

@LeonardoJegigzem, yo no lo he mirado, pero si existe está claro que no sería un estándard de C++ sino que haría que usar bibliotecas independientes de cada SSOO y hacer una interfaz común en nuestro proyecto. No creen que sería liar mucho la perdiz solo para obtener un IDE? Creo que antes mejor obtener un hash de la cedena de la ruta que también es única.

DavidBM commented 10 years ago

De todas formas, si usamos un identificador dado por el SO, que nos impide crear el identificador nosotros mismos?

danigomez commented 10 years ago

@DavidBM Es que si lo creamos nosotros, no tendriamos que controlar que id va a ser asignado??, es decir, cuales ya están asignados, liberar los id de los recursos que se hayan liberado, etc, ya habria que agregar toda una lógica sólo para asignar el id :/,

adrigm commented 10 years ago

Os dejo un artículo de mi compañero David Saltares, programador de IA en Crytek UK, sobre la comparación de cadenas mediantes ID: http://siondream.com/blog/projects/id-generator-avoiding-string-comparisons/

Quizás nos pueda servir algo así.

texter commented 10 years ago

En C está la función stat() (Si no recuerdo mal pertenece al estandar POSIX, pero nadie ha dicho que en todos los S.O. se utilicen los estandares POSIX) que te dice el estado de un fichero.

Dentro de los campos hay un ino_t que es el identificador del inodo. Se puede leer ese campo.

@adrigm no se hasta que punto en Windows y Mac se puede utilizar la misma función.

Y bueno yo lo he dicho como idea, se que es mas facil a nivel de programación el utilizar una función hash ya creada.

adrigm commented 10 years ago

@texter habría que mirarlo.

Sí, todos son ideas aquí estamos para eso para debatir, proponer y aprender maneras de hacer las cosas :)

DavidBM commented 10 years ago

A mi la idea de poner un algoritmo de busqueda en el main loop me parece un performance killer. Si es númerico aún puedes hacerlo de tiempo n. (si se optimiza bien con busqueda binaria se puede bajar el tiempo muchisimo) Si es con strings ya tienes que hacer un algoritmo de comparación de strings por cada elemento. Lo que multiplica el tiempo para acceder a un recurso. A parte de que al no ser numérico ya no puedes ordenar las id para optimizar el algoritmo.

Creo que algo óptimo y sencillo sería tener un vector indice con los punteros a los recursos y luego un LILO para apuntarse las IDs libres.

Y bueno, vamos a hacer un motor de videojuegos, no podemos simplificar taaaanto. Al fin y al cabo, el tamaño de algo así no es pequeño e intruducir soluciones con estructuras de datos estandar puede dar para un articulo donde la gente aprenda.

texter commented 10 years ago

Perfecto. :)

Está claro que la utilización de nuestro propio gestor de recursos creado especificamente para el motor es una manera de aprender a utilizar herramientas, pero creo que también es bueno recordar que no todo se tiene que inventar. Porque digo esto? Muy sencillo, como bien has dicho @adrigm estamos aquí para aprender, debatir etc, pero no olvidemos lo que hemos aprendido tampoco, si ya se pensó en identificadores únicos para los ficheros, no tenemos que volver a inventar los identificadores de los ficheros.

Mi idea de fondo es la de reducir los ciclos de CPU, si es un motor, a parte de aprender a crearlos, también es bueno que sea rápido eso pasa por reducir la carga de la CPU.

Si ya se que en este punto no se tiene que mirar, pero si tenemos las herramientas se puede probar a ver si es posible.

DavidBM commented 10 years ago

A mi personalmente, si la solución de @texter cumple con que sea multiplatafoma, me parece optima. No tanto a nivel didactico, pero si a nivel de rendimiento.

Lo único es que si luego queremos extender el sistema a uno que admita otros tipos de recursos que no sean unicos por fichero (por ejemplo, una sprite en la que se creen varias images independientes, o un fichero con varias animaciones) nos fallará.

ArnauPrat commented 10 years ago

Bueno bueno bueno, veo que mi mensaje ha desatado un Tsunami de dudas. Intentaré explicar mejor mi propuesta, con el fin de aclarar todas las dudas. Primero de todo, es un error estar pensando en rendimiento a estas alturas. Bajo mi humilde opinión, y creo que Adrián estará de acuerdo conmigo, hay que plantear las cosas de una manera simple y funcional, ya llegará el dia de optimizar las cosas y si hay que reescribir algunas partes, se hace. Ya veréis, mas adelante, que en cuanto a rendimiento, habrá otras partes del motor mucho mas críticas. Si llegamos al punto en que la carga de recursos es un problema, o bién estamos haciendo un sandbox con el motor a lo Skyrim, o bien realmente lo hemos hecho muy pero que muy mal con la carga (que lo dudo), o bien somos unos cracks y el resto del motor va a la velocidad de la luz.

Dicho esto, voy al grano. Mi idea es relativamente simple. Se le piden los recursos a un ResourceManager, mediante una función LoadResource. Algo así: ResourceManager textureManager; ResourceHandler textureHandler = textureManager.LoadResource("data/player/weapon.bmp");

Texture será una implementación de la interfície Resource, al igual que cualquier otro tipo de recurso. El uso de templates nos permite, por un lado, reusar código de una manera más eficiente, y por el otro y muy importante, permite aprovecharnos de las comprobaciones de tipado estático. Así cuando hagamos:

Texture* tex = textureHandler.GetResource(); // o bien usando textureHandler-> (sobrecargando el operador ->)

estaremos forzando que GetResource() nos devuelva algo del tipo Texture*.

Evidentemente, el ResourceHandler se encargará de proporcionar el recurso, ya cargado en memoria por el ResourceManager, de manera eficiente. El handler se podrá compartir entre objetos.

Respecto al dilema de los strings, keep it simple. Yo propongo, inicialmente, usar strings directamente. Si en algún momento supone un problema, haremos lo siguiente. Al pedir un recurso a partir de la ruta relativa, aplicaremos una función de hash a la string, lo que nos devolverá un entero. Usaremos este entero para compararlo con los recursos ya cargados. Si hay un hit, comprobaremos que la string original sea la misma que la del recurso. SI es el caso, y alo tenemos. Si es diferente, habremos detectado un conflicto. en tal caso, se reportará al sistema de log o lo que tengamos y habrá que cambiar el nombre de uno de los recursos para que el hash de un numero distinto. Asumiendo una función de hash y strings distribuidas uniformemente, la probabilidad de conflicto es de 1 entre 2^32. En la practica es un poco mayor y se puede dar el caso de conflicto, pero realmente es un mal menor porque los conflictos se detectan en fase de desarrollo.

Un saludo,

Arnau

DavidBM commented 10 years ago

Vale, ya veo mi confusión. Creia que el usuario pedia cada vez el recursos usando el string. De ahí que hayan saltado mis alarmas. Sorry. Ahora ya lo entiendo, me parece perfecto ese sistema :)

De hecho, dejaría la comprobación por strings, que para el asunto, va bien.

adrigm commented 10 years ago

Yo estoy de acuerdo con @ArnauPrat de mantenerlo simple, al meno al principio, pensemos que los recursos se suelen cargar al principio de una pantalla (típica pantalla de carga) y no estamos accediendo a cargar recursos continuamente, pero si llegado el momento la comprobación de cadenas fuera un cuello de botella siempre podemos optimizar.

Respecto el método de @ArnauPrat, no me queda claro el uso de textureHandler, lo que hacemos es tener un objeto que contiene información de la textura cargada, ¿Pero que conseguimos con eso?

Mi propuesta es algo así:

AssetManager am;
sf::Texture* miTextura;
am.get(miTextura, "Data/images/texture.png");

Donde el primer parámetro que recibe el método get es el tipo T y el segundo el string de la ruta. Con esto busca en un vector o Lista si el recurso existe. Si no existe lo carga y lo devuelve y si existe devuelve el puntero (o la referencia).

Para evitar la comparación de cadenas internamente Get puede obtener el hash de la ruta pasada y almacenar eso en su vector o lista de recursos.

El simple y quizás no lo más óptimo, pero efectivo.

DavidBM commented 10 years ago

A mi la forma de @adrigm me parece buena. Además es simple, facil de enteder y sin complicaciones.

adrigm commented 10 years ago

Por cierto con mi forma se puede aprovechar para devolver true o false si el recurso ha sido cargado o no.

ArnauPrat commented 10 years ago

Me parece bien tu propuesta @adrigm, es simple. Aún así te aclaro las dudas. Entiendo que en tu propuesta, el uso seria el siguiente:

//En la fase de carga cargaríamos la textura.
AssetManager am;
sf::Texture* miTextura;
am.get(miTextura, "Data/images/texture.png");
...
//Luego, en algún lugar del bucle principal. Haríamos algo así.
Draw(miTextura, x, y);

Correcto? Simple y efectivo. El único inconveniente es que, si tu te guardas directamente un puntero a tu textura, estás obligado a que la textura esté siempre en la misma posición de memoria, a menos que en cada iteración del bucle principal, se la pidas al gestor. En el tipo de proyecto en el que estamos, esto no va a suponer ningún problema en un principio. El handler permite abstraerte del lugar exacto en donde reside tu textura. Esto permite al gestor de recursos, si lo requieres, reorganizar internamente como tiene guardados los recursos. Para qué querría el gestor hacer esto? Por ejemplo, porque estás en un dispositivo móvil con una cantidad de memoria disponible para recursos limitada, y en cierto punto, necesitas borrar algún recurso de memoria que actualmente no usas para hacer espacio a uno que necesitas. Al cabo de 10 frames, cuando tengas que volver a usar el recurso que has borrado, el handler transparentemente le pedirá a tu gestor que lo recarge, mientras que si te has guardado solamente el puntero al recurso como tu propuesta, estarás apuntando a basura o a otro recurso. Otro ejemplo, es que el gestor quiera reorganizar como tiene alocatados los recursos internamente para o bien defragmentar la memoria y liberar espacio, o bien porqué ha analizado los patrones de acceso a los recursos y quiere mejorar la localida en el acceso a ellos.

La utilización del handler es sencilla

//En la fase de carga cargaríamos la textura.
AssetManager am;
Handler < sf:Texture >  miTextura = am.get("Data/images/texture.png");
...
//Luego, en algún lugar del bucle principal. Haríamos algo así.
Draw(miTextura.GetResource(), x, y);

El handler puede tener un método isValid() para comprobar si la carga fue satisfactoria. La implementación de GetResource es muy simple. Puede ser, inicialmente directamente un puntero a la textura, o el handlier puede guardar un índice a un vector y un identificador único para comprobar que el elemento del vector sea el requerido etc.. En cualquier caso, obtener la dirección del recurso, por experiencia, es barato. Otra ventaja, es que si el motor no ha podido cargar la textura, podría devolver un puntero a una textura de debug, así el juego puede seguir funcionando pero en vez de la textura inicia, se ve el típico cuadrado rosa nuclear.

Espero haber aclarado las dudas. Me sigue pareciendo bien tu propuesta si no quieres rizar demasiado el rizo.

PD: Como se hace para formatear bien el código aquí en los comentarios? Gracias

DavidBM commented 10 years ago

Al menos en la wiki se usa MarkDown. (http://es.wikipedia.org/wiki/Markdown)

Prueba a ver si funciona MarkDown, si esto sale en negrita, funciona

Lo que comenta @ArnauPrat me parece muy cierto, un gestor de recursos ya planteado paara que maneje la memoria me parece util.

adrigm commented 10 years ago

@ArnauPrat, lo que dices es muy cierto, pero no me queda claro una cosa. Si el usuario una vez cargada la textura en el handler debe usar este para el manejo de la textura, ¿No tendría que ser creado siempre un nuevo Handler en cada ámbito local?

Me explico, tu estás en la clase SceneHome que es una escena (Pantalla) de tu juego ahí hace algo como esto

Handler < sf:Texture >  miTextura = am.get("Data/images/texture.png");

Esa variable miTextura estaría como mucho a nivel de clase en SceneHome, si yo ahora cambio de escena por ejemplo a SceneGame que es manejada por otra clase, debería crear un nuevo Handler.

Es decir el Handler solo existe a nivel local en la clase o método que se cree.

danigomez commented 10 years ago

Igualmente las texturas van a estar asociadas a las Entidades que puedan ser dibujadas, por lo que el problema de ámbito creo que se resuelve dentro del contexto o mundo del juego, es decir, algún tipo de variable global que contenga los elementos del juego que están actualmente en uso, de modo que puedan ser usados de un modo cross en todo el engine, yo he resuelto un problema similar de ese modo. El 07/11/2013 06:41, "Adrián" notifications@github.com escribió:

@ArnauPrat https://github.com/ArnauPrat, lo que dices es muy cierto, pero no me queda claro una cosa. Si el usuario una vez cargada la textura en el handler debe usar este para el manejo de la textura, ¿No tendría que ser creado siempre un nuevo Handler en cada ámbito local?

Me explico, tu estás en la clase SceneHome que es una escena (Pantalla) de tu juego ahí hace algo como esto

Handler < sf:Texture > miTextura = am.get("Data/images/texture.png");

Esa variable miTextura estaría como mucho a nivel de clase en SceneHome, si yo ahora cambio de escena por ejemplo a SceneGame que es manejada por otra clase, debería crear un nuevo Handler.

Es decir el Handler solo existe a nivel local en la clase o método que se cree.

— Reply to this email directly or view it on GitHubhttps://github.com/genbetadev/Genbeta-Dev-Engine/issues/4#issuecomment-27949112 .

ArnauPrat commented 10 years ago

Tienes que ver el handler de la misma manera que ves un puntero: una variable que te permite acceder al recurso. En el caso del puntero, esta variable contiene una dirección de memoria. En el caso del handler, esta variable (estructura) contiene la informacion necesaria para proporcionarte la direccion. Si estas en otro contexto y le pides al gestor la misma textura, este te devolvera un handler que hace referencia a la textura en questión. La manera mas simple de implementarlo seria así:

class Handler{
public:
   Resource* GetResource() {
     return manager->GetResource(mUniqueId);
}
private:
   short int mUniqueId;
   ResourceManager* manager;
};

Hay 1000 maneres de implementar un handler, mas o menos eficientes en funcion de la potencia. Para empezar, por ejemplo, el handler podria simplemente tener un puntero al recurso. El handler lo puedes copiar, compartir entre objetos o volver a pedirselo al gestor si te hace falta. Es simplemente un identificador de recurso. La clave es que te permite referenciar el recurso independientemente de donde está en la memória, lo que lo hace más seguro.

ArnauPrat commented 10 years ago

Hola Chicos, os parece bien que implemente una propuesta de la infraestructura este fin de semana? Una pegunta: Lo pongo dentro de Core o lo meto en una carpeta a parte? Un saludo

RdlP commented 10 years ago

A mi me parece bien que se empiece a implementar ya, así tenemos una base sobre la que empezar a trabajar en el caso de que haya que modificarlo, en cuanto al lugar, creo que al ser una feature esencial debería ir dentro de Core

dsocolobsky commented 10 years ago

No estaría mal utilizar un smart pointer como std::unique_ptr para ahorrarnos el trabajo de liberar la memoria manualmente.

También seria buena idea tener todos los recursos en un std::map, relacionando el ID (Ya sea numerico o string) con el puntero. std::map<std::string, std::unique_ptr<Resource>> resources;

Yo por ahora voto por usar Strings, ya veremos si es necesario cambiar a IDs numericos, aunque un punto a favor de los numericos es poder "cachear" los resources por mas usados, es decir, si usamos mucho la textura "Player" seria interesante reacomodarla al principio de un mapa/array ordenado, con el fin de acceder a ella rapidamente.

Por cierto, esto esta interesante: https://github.com/LaurentGomila/SFML/wiki/Tutorial:-Image-Manager

adrigm commented 10 years ago

@ArnauPrat Dale, siempre hay tiempo de cambiar y modificar código. Va dentro del Core, será uno de los aspectos esenciales del Engine.

@dysoco Más o menos la idea de lo que expone ese tutorial es la que yo tenía.

ArnauPrat commented 10 years ago

Ok, me miraré el tutorial para coger ideas.

ArnauPrat commented 10 years ago

Hola Chicos,

@adrigm , como lo hago para subir los cambios que he realizado?

Gracias,

adrigm commented 10 years ago

Haz commit en tu repositorio y luego solicita pull request