Un juego sin memoria es un juego que el jugador abandona en la primera sesión. Hoy le damos a tu juego la capacidad de recordar el progreso entre partidas.
En las lecciones anteriores trabajamos con estructuras de datos que viven en memoria mientras el juego corre: posiciones, velocidades, estados de animación. En cuanto cierras el programa, todo eso desaparece. Un sistema de guardado toma ese estado y lo escribe a disco de forma que pueda reconstruirse exactamente igual la próxima vez.
El riesgo principal no es escribir el archivo — eso es fácil. El riesgo es la compatibilidad: qué pasa cuando actualizas tu juego y agregas un campo nuevo a la estructura de datos, y ahora tienes archivos de guardado viejos que no coinciden con el nuevo formato. Vamos a diseñar el sistema pensando en esto desde el principio.
La forma más directa de guardar datos en C++ es escribir la memoria de una estructura directamente a un archivo con `fstream` en modo binario. Es rápida y compacta, pero frágil ante cambios de estructura si no se maneja con cuidado.
Nota el campo `version` al inicio de la estructura. Siempre debes incluir un número de versión en tus formatos de guardado — es lo que te permite, en el futuro, detectar un archivo viejo y migrarlo en vez de leerlo mal y corromper el estado del juego.
Escribir un `struct` completo con `write()` funciona mientras la estructura no tenga punteros ni contenedores dinámicos (`std::string`, `std::vector`). Si tu `DatosPartida` incluyera un `std::vector<Item> inventario`, escribir la memoria cruda escribiría el puntero interno del vector, no sus datos — y al cargar el archivo en otra ejecución, ese puntero sería inválido. Este es exactamente el tipo de bug que conviene pedirle a tu asistente de IA que revise antes de confiar en un sistema de guardado: "¿esta estructura es segura de serializar con `write()` directo, o tiene algo que apunte a memoria dinámica?"
Para datos que necesitan ser legibles, editables a mano o versionados con más flexibilidad (como listas de inventario de tamaño variable), un formato de texto tipo JSON es más robusto. Puedes usar una librería como `nlohmann/json`, pero para entender el concepto de raíz vamos a escribir un serializador manual mínimo.
Este serializador manual es útil para entender el problema, pero para un proyecto real te recomiendo usar una librería probada como `nlohmann/json` en vez de mantener un parser propio — escribir un parser JSON correcto (con escapes de comillas, números negativos, anidamiento) es más trabajo del que parece. Este es un caso perfecto para pedirle a tu asistente de IA que integre la librería en tu `CMakeLists.txt` y te muestre el equivalente usando `nlohmann::json` en vez del serializador manual.
Nunca confíes ciegamente en un archivo de guardado, incluso si lo generó tu propio juego — puede estar corrupto por un cierre abrupto, un disco lleno, o edición manual del jugador.
Si tu juego se cierra (o se cae) justo mientras escribe el archivo de guardado, puedes terminar con un archivo a medio escribir. La técnica estándar es escribir primero a un archivo temporal y, solo si la escritura fue exitosa, reemplazar el archivo original.
`std::rename` en la mayoría de los sistemas de archivos es una operación atómica: o el archivo nuevo reemplaza completamente al viejo, o no pasa nada — nunca queda un estado intermedio corrupto.
Con guardado persistente resuelto, en la próxima lección vamos a organizar el flujo general del juego (menú principal, juego en curso, pausa, pantalla de fin) con una máquina de estados, que es la estructura que te va a permitir cargar una partida guardada desde un menú de forma limpia.
Carlos Montiel es arquitecto de soluciones IA empresarial. Implementa LLMs, Agentes, RAG y orquestadores en empresas de Guatemala y Latinoamérica. Contáctalo para una consultoría.
Contactar a Carlos Montiel