Experimento con Game Maker [gestionar ficheros grandes]
Llevo días dándole vueltas aun tema relacionado con Game Maker Studio. En la mayoría de programas, y videojuegos también, cuando se quiere almacenar y leer gran cantidad de datos se recurre a bases de datos relacionales.
Existen un montón de motores de bases de datos, se pueden gestionar desde un servidor, como Sql Server, Oracle, o la más popular para uso en internet MySQL. También tenemos bases de datos populares que se pueden ejecutar en local, en Android se suele usar SQLite, pero también existe MS Access o Firebird (no se suele usar mucho, pero la he probado y da buenos resultados). Y también existen bases de datos que no son relacionales, como puede ser MongoDB.
- 1 Acceder a una base de datos desde Game Maker
- 2 Un ejemplo de fichero de datos
- 3 Leer como fichero de texto
- 4 Leer como fichero ini (1 seccion)
- 5 Fichero ini con muchas secciones
- 6 Crear y leer una pila
- 7 Crear y leer una cola
- 8 Crear y gestionar una lista
- 9 Crear y leer una cola con prioridad
- 10 Crear y acceder a un mapa
- 11 Conclusión final
Acceder a una base de datos desde Game Maker
Después de ver todo el surtido que hay en el mercado, viene el kit de la cuestión: Game Maker Studio no tiene absolutamente nada para acceder a bases de datos. Existen extensiones en el MarketPlace para acceder a algunas bases de datos, pero la pregunta es:
¿Es necesario acceder a una base de datos en Game Maker Studio?
Situaciones dónde necesitemos grandes volúmenes de datos
Podemos imaginarnos situaciones dónde necesitemos almacenar muchos datos. No estoy hablando de un mega juego dónde hay música, sonidos, un montón de sprites. etc. Todo esto serían recursos que ponemos en nuestro proyecto, me refiero a datos que hay que almacenar.
Un ejemplo sería una aventura gráfica, dónde hay cantidad de texto en forma de diálogos. Otro ejemplo sería si queremos guardar estadísticas para acceder a ellas, por ejemplo por dónde ha pasado un personaje, que se ha encontrado, etc. Y un último ejemplo sería si almacenamos mucho texto, como puede ser un libro interactivo.
Así que aquí viene la misma pregunta: ¿necesitamos una base de datos? Con Game Maker podemos guardar datos de varias maneras:
Así que vamos a probar como funcionan todas estas opciones en ficheros de texto accediendo a una gran cantidad de datos. ¡Empieza el experimento!
Un ejemplo de fichero de datos
Si nos vamos al Instituo Nacional de Estadística (de España), podemos descargarnos un fichero con una lista de nombres más usados en nuestro país (los que tienen más de 20 personas en toda España con ese nombre). Una muestra de ese fichero es ésta, con fecha 01/01/2015:
Orden | Nombre | Frecuencia | Edad Media |
1 | ANTONIO | 715.215 | 54,6 |
2 | JOSE | 641.525 | 60,0 |
3 | MANUEL | 618.891 | 54,0 |
4 | FRANCISCO | 530.309 | 56,0 |
5 | JUAN | 367.726 | 54,9 |
6 | DAVID | 359.930 | 28,3 |
7 | JOSE ANTONIO | 316.275 | 47,3 |
8 | JAVIER | 303.247 | 30,9 |
9 | JOSE LUIS | 301.752 | 51,0 |
10 | FRANCISCO JAVIER | 289.325 | 42,0 |
Orden | Nombre | Frecuencia | Edad Media |
1 | MARIA CARMEN | 668.639 | 54,5 |
2 | MARIA | 633.600 | 48,9 |
3 | CARMEN | 415.535 | 60,2 |
4 | JOSEFA | 298.346 | 66,4 |
5 | ISABEL | 279.932 | 56,2 |
6 | ANA MARIA | 277.090 | 48,8 |
7 | MARIA PILAR | 267.272 | 54,5 |
8 | MARIA DOLORES | 265.341 | 54,3 |
9 | MARIA TERESA | 256.586 | 54,6 |
10 | ANA | 255.010 | 43,1 |
¿Curioso, verdad? Al ser un fichero Excel, voy a pasarlo a formato csv para tratarlo como un fichero de texto. Por si a alguien le interesa, os dejo los ficheros generados.
Los ficheros no son muy grandes, ocupan casi 650 kb cada uno, pero tienen ¡más de 24000 líneas cada uno! Así que de partida ya tenemos una buena base para empezar a probar. También nos servirá para poner varios ejemplos de como funcionan las estructuras de datos.
Leer como fichero de texto
Aprovechando que es un formato CSV, vamos a acceder a él como si fuese un fichero de texto, leyendo línea a línea. Creamos un proyecto nuevo y añadimos los ficheros como Included Files. Añadimos un objeto nuevo, llamado obj_fichero_texto. Voy a crearle un sprite sencillito para hacer que funcione como un botón, así podemos escribir el código en el evento Mouse – Left Released . Será el siguiente:
///leer fichero texto var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); var linea = ""; var total = 0; while (!file_text_eof(file)) { linea = file_text_readln(file); total++; } file_text_close(file); show_debug_message("Tiempo fichero texto: " + string(current_time - ahora) + " milisegundos");
Usamos variables de hora para saber cuanto tarda, y leemos el fichero de tiempo. Al final hacemos la resta del current_time
del principio y del final.
**********************************. Entering main loop. **********************************. Tiempo fichero texto: 50 milisegundos
¡Más de 24.000 líneas en 50 milisegundos! No esta nada mal, ¿verdad?
Seguimos con el experimento
Leer como fichero ini (1 seccion)
Imaginemos que estos datos estuviesen en un fichero ini. Voy a aprovechar el código anterior, que lee los datos de un fichero, y me creo un fichero ini con una única sección que se llame [nombres], y la clave un número consecutivo. El formato sería así:
[nombres] 1="ANTONIO" 2="JOSE" 3="MANUEL" 4="FRANCISCO" 5="JUAN" ... 24584="ZIRUI"
Después de crear el fichero, accederé aleatoriamente a uno de los valores, a ver si el tiempo de respuesta es bueno. Este es el código
///crear ini 1 seccion var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); ini_open("hombres1seccion.ini"); var linea = ""; var total=0; while (!file_text_eof(file)) { linea = file_text_readln(file); total++; orden = string_copy(linea,0, string_pos(";",linea)-1); nombre = string_letters(linea); ini_write_string("nombres",orden,nombre); } file_text_close(file); ini_close(); show_debug_message("Tiempo ini 1 seccion: " + string(current_time - ahora) + " milisegundos"); //lectura ini random ahora = current_time; ini_open("hombres1seccion.ini"); nombre = ini_read_string("nombres",string(irandom(total)), ""); ini_close(); show_debug_message(nombre); show_debug_message("Tiempo lectura ini 1 seccion: " + string(current_time - ahora) + " milisegundos");
Veamos el resultado:
**********************************. Entering main loop. **********************************. Tiempo ini 1 seccion: 1162 milisegundos ALEC Tiempo lectura ini 1 seccion: 13 milisegundos
Hemos tardado más de un segundo en crear un ficheri ini de más de 24000 líneas, pero el acceso es bastante rápido también. ¿Probamos con otro formato de ini?
Fichero ini con muchas secciones
Ahora el formato del fichero ini es diferente, siendo así:
[24584] nombre="ZIRUI" [24583] nombre="ZIJIE" [24582] nombre="ZIDAN" [24581] nombre="ZHENHAO" [24580] nombre="ZAKARYAE" [24579] nombre="ZAIM"
Curiosamente, la creación del ini coloca las líneas leídas al principio del fichero. Este es el código de creación
///crear ini muchas secciones var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); ini_open("hombressecciones.ini"); var linea = ""; var total=0; while (!file_text_eof(file)) { linea = file_text_readln(file); total++; orden = string_copy(linea,0, string_pos(";",linea)-1); nombre = string_letters(linea); ini_write_string(orden,"nombre",nombre); } file_text_close(file); ini_close(); show_debug_message("Tiempo ini 1muchas secciones: " + string(current_time - ahora) + " milisegundos"); //lectura ini random ahora = current_time; ini_open("hombressecciones.ini"); nombre = ini_read_string(string(irandom(total)),"nombre", ""); ini_close(); show_debug_message(nombre); show_debug_message("Tiempo lectura ini muchas secciones: " + string(current_time - ahora) + " milisegundos");
Y los tiempos son:
**********************************. Entering main loop. **********************************. Tiempo ini 1muchas secciones: 1507 milisegundos ALEC Tiempo lectura ini muchas secciones: 21 milisegundos
Hemos visto que la gestión de ficheros es muy rápida, ¿que pasará con las estructuras de datos?
Crear y leer una pila
Cuando hablamos de pilas, comentamos que añadimos una a una y sacamos la última añadida. Vamos a ver que pasa al llenar una pila con todos los nombres y cuanto tarda en sacarlos. El código es el siguiente:
///pila var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); pila = ds_stack_create(); var linea = ""; //var total=0; while (!file_text_eof(file)) { linea = file_text_readln(file); // total++; orden = string_copy(linea,0, string_pos(";",linea)-1); nombre = string_letters(linea); ds_stack_push(pila,nombre); } file_text_close(file); show_debug_message("Tiempo llenar pila: " + string(current_time - ahora) + " milisegundos"); show_debug_message("Total pila: " + string(ds_stack_size(pila))); //lectura pila ahora = current_time; while(!ds_stack_empty(pila)){ nombre = ds_stack_pop(pila); //show_debug_message(nombre); } show_debug_message("Tiempo vaciar pila: " + string(current_time - ahora) + " milisegundos");
Y el tiempo que tarda es:
**********************************. Entering main loop. **********************************. Tiempo llenar pila: 197 milisegundos Total pila: 24584 Tiempo vaciar pila: 6 milisegundos
Llenar una pila de +24500 elementos y vaciarla no llega ni a un segundo. La verdad que aquí flipé un poco de lo bien optimizado que está. ¡Probamos con una cola!
Crear y leer una cola
Las colas, a diferencia de las pilas, el elemento que sale es el primero que ha entrado. El código es muy parecido al anterior
///cola var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); cola = ds_queue_create(); var linea = ""; //var total=0; while (!file_text_eof(file)) { linea = file_text_readln(file); // total++; orden = string_copy(linea,0, string_pos(";",linea)-1); nombre = string_letters(linea); ds_queue_enqueue(cola, nombre); } file_text_close(file); show_debug_message("Tiempo llenar cola: " + string(current_time - ahora) + " milisegundos"); show_debug_message("Total cola: " + string(ds_queue_size(cola))); //lectura cola ahora = current_time; while(!ds_queue_empty(cola)){ nombre = ds_queue_dequeue(cola); //show_debug_message(nombre); } show_debug_message("Tiempo vaciar cola: " + string(current_time - ahora) + " milisegundos");
Y los tiempos también
**********************************. Entering main loop. **********************************. Tiempo llenar cola: 161 milisegundos Total cola: 24584 Tiempo vaciar cola: 7 milisegundos
Usaremos ahora una lista.
Crear y gestionar una lista
Con las listas podemos hacer varias acciones interesantes: leer en cualquier posición, ordenarla y mezclarla (como barajar cartas). También la ordenaremos después de mezclar. Este es el código de creación de lista y todas las acciones descritas:
///lista var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); lista = ds_list_create(); var linea = ""; var total=0; while (!file_text_eof(file)) { linea = file_text_readln(file); total++; orden = string_copy(linea,0, string_pos(";",linea)-1); nombre = string_letters(linea); ds_list_add(lista, nombre); } file_text_close(file); show_debug_message("Tiempo llenar lista: " + string(current_time - ahora) + " milisegundos"); show_debug_message("Total lista: " + string(ds_list_size(lista))); //lectura lista ahora = current_time; nombre = ds_list_find_value(lista, irandom(total)); show_debug_message(nombre); show_debug_message("Tiempo lectura lista: " + string(current_time - ahora) + " milisegundos"); //Ordenar lista ahora = current_time; ds_list_sort(lista, true); show_debug_message("Tiempo ordenar lista: " + string(current_time - ahora) + " milisegundos"); //mezclar lista ahora = current_time; ds_list_shuffle(lista); show_debug_message("Tiempo mezclar lista: " + string(current_time - ahora) + " milisegundos"); //Ordenar lista después mezclar ahora = current_time; ds_list_sort(lista, true); show_debug_message("Tiempo ordenar lista después mezclar: " + string(current_time - ahora) + " milisegundos");
¿Cuanto creemos que tardará?
**********************************. Entering main loop. **********************************. Tiempo llenar lista: 87 milisegundos Total lista: 24584 ALEXANDRUSTEFAN Tiempo lectura lista: 0 milisegundos Tiempo ordenar lista: 1748 milisegundos Tiempo mezclar lista: 5 milisegundos Tiempo ordenar lista después mezclar: 1810 milisegundos
El acceso a un valor de la lista es instantáneo, ordenar una lista es lo más costoso que hemos visto hasta ahora, pero es normal. Igualmente, para la cantidad de datos tratado sabemos que no nos tenemos que preocupar.
Probamos con una cola con prioridad
Crear y leer una cola con prioridad
Las colas con prioridad saca el elemento más prioritario, depende del valor que le demos. En nuestro ejemplo podemos usar el mismo orden como factor para decidir la prioridad
///cola con prioridad var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); prioridad = ds_priority_create(); var linea = ""; //var total=0; while (!file_text_eof(file)) { linea = file_text_readln(file); // total++; orden = string_copy(linea,0, string_pos(";",linea)-1); nombre = string_letters(linea); ds_priority_add(prioridad, nombre, orden); } file_text_close(file); show_debug_message("Tiempo llenar cola con prioridad: " + string(current_time - ahora) + " milisegundos"); show_debug_message("Total cola con prioridad: " + string(ds_priority_size(prioridad))); //lectura cola prioridad ahora = current_time; while(!ds_priority_empty(prioridad)){ nombre = ds_priority_delete_max(prioridad); //show_debug_message(nombre); } show_debug_message("Tiempo vaciar cola con prioridad: " + string(current_time - ahora) + " milisegundos");
¿Cuanto tiempo necesita para vaciar la cola?
**********************************. Entering main loop. **********************************. Tiempo llenar cola con prioridad: 184 milisegundos Total cola con prioridad: 24584 Tiempo vaciar cola con prioridad: 2287 milisegundos
Si tarda más de dos segundos en vaciarla, podemos deducir que en el momento de sacar un elemento es cuando decide cual es el más prioritario, no cuando añadimos el elemento. Pero está bastante bien.
Y para finalizar, usaremos una de las estructuras de datos más usadas: los mapas.
Crear y acceder a un mapa
Los mapas tienen una peculiaridad, pueden ser guardados (y leerlos) como ficheros JSON, muy usados en la web. Así que también vamos a hacerlo. Leemos los datos para meterlo en un mapa, de una manera parecida a como hemos hecho el fichero ini de muchas secciones, y buscamos un elemento al azar. También grabaremos el mapa como un json, lo volvemos a leer y a ver cuanto tarda en volver a buscar un elemento aleatorio. Este es el código completo:
///map var ahora = current_time; file = file_text_open_read("hombres_por_edad_media.csv"); mapa = ds_map_create(); var linea = ""; var total=0; while (!file_text_eof(file)) { linea = file_text_readln(file); total++; orden = string_copy(linea,0, string_pos(";",linea)-1); nombre = string_letters(linea); ds_map_add(mapa, orden,nombre); } file_text_close(file); show_debug_message("Tiempo llenar mapa: " + string(current_time - ahora) + " milisegundos"); show_debug_message("Total mapa: " + string(ds_map_size(mapa))); //lectura mapa ahora = current_time; nombre = ds_map_find_value(mapa,string(irandom(total))); show_debug_message(nombre); show_debug_message("Tiempo buscar en mapa: " + string(current_time - ahora) + " milisegundos"); //grabar mapa como json ahora = current_time; json = json_encode(mapa); file = file_text_open_write("mapa.json"); file_text_write_string(file, json); file_text_close(file); show_debug_message("Tiempo grabar json: " + string(current_time - ahora) + " milisegundos"); //leer json a mapa ahora = current_time; ds_map_clear(mapa); json = ""; file = file_text_open_read("mapa.json"); while (!file_text_eof(file)) { json += file_text_read_string(file); file_text_readln(file); } file_text_close(file); mapa = json_decode(json); nombre = ds_map_find_value(mapa,string(irandom(total))); show_debug_message(nombre); show_debug_message("Tiempo leer json: " + string(current_time - ahora) + " milisegundos");
Los tiempos totales son los siguientes:
**********************************. Entering main loop. **********************************. Tiempo llenar mapa: 88 milisegundos Total mapa: 24584 ALEC Tiempo buscar en mapa: 0 milisegundos Tiempo grabar json: 471 milisegundos MALCOLMJAMES Tiempo leer json: 404 milisegundos
Perfectos para gestionar este volumen de registros.
Conclusión final
Antes de nada, quiero comentar que al usar la función string_letters(), nos comemos los espacios al recuperar algunos nombres. No lo he rectificado porque para el experimento ya nos sirve.
La room con todos los botones para probar es la siguiente:
Y el enlace del proyecto para probar es éste:
Tutorial de gestión de ficheros enormes
Ahora volvemos a la pregunta
¿Necesitamos un motor de base de datos para nuestro juego?
Si es para leer y grabar datos en local, la respuesta está clara: NO. Por muchos datos que necesitemos almacenar, la gestión de ficheros y estructuras de datos es suficiente. Hemos probado todas, así que la elección de un formato de fichero o un tipo de estructura va más con la comodidad de usarla que como gestionar una optimización.
¿Te ha gustado el experimento? ¿Te interesaría otros para ver los resultados?