Sistemas de guardado — juegos con memoria persistente

By Carlos Montiel | Especialista en IA Empresarial
Publicado: 2026-07-28 | Por: Carlos Montiel | Lectura: ~5 minutos

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.

Por qué el guardado es más delicado de lo que parece

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.

Opción 1: guardado binario simple

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.

#include <fstream> #include <cstdint> struct DatosPartida { uint32_t version = 1; int nivel_actual; int vidas; float posicion_x; float posicion_y; int monedas; }; bool guardar_binario(const DatosPartida& datos, const std::string& ruta) { std::ofstream archivo(ruta, std::ios::binary); if (!archivo) return false; archivo.write(reinterpret_cast<const char*>(&datos), sizeof(DatosPartida)); return archivo.good(); } bool cargar_binario(DatosPartida& datos, const std::string& ruta) { std::ifstream archivo(ruta, std::ios::binary); if (!archivo) return false; archivo.read(reinterpret_cast<char*>(&datos), sizeof(DatosPartida)); return archivo.good(); }

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.

bool cargar_binario_seguro(DatosPartida& datos, const std::string& ruta) { if (!cargar_binario(datos, ruta)) return false; if (datos.version < 1 || datos.version > 1) { SDL_Log("Version de guardado no soportada: %u", datos.version); return false; } return true; }

Por qué el binario directo tiene límites

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?"

Opción 2: un formato de texto simple (JSON manual)

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.

#include <fstream> #include <sstream> #include <vector> #include <string> struct Item { std::string nombre; int cantidad; }; struct PartidaJSON { int version = 1; int nivel_actual; int vidas; std::vector<Item> inventario; }; std::string serializar(const PartidaJSON& p) { std::ostringstream json; json << "{\n"; json << " \"version\": " << p.version << ",\n"; json << " \"nivel_actual\": " << p.nivel_actual << ",\n"; json << " \"vidas\": " << p.vidas << ",\n"; json << " \"inventario\": [\n"; for (size_t i = 0; i < p.inventario.size(); ++i) { json << " { \"nombre\": \"" << p.inventario[i].nombre << "\", \"cantidad\": " << p.inventario[i].cantidad << " }"; if (i + 1 < p.inventario.size()) json << ","; json << "\n"; } json << " ]\n}\n"; return json.str(); } bool guardar_json(const PartidaJSON& p, const std::string& ruta) { std::ofstream archivo(ruta); if (!archivo) return false; archivo << serializar(p); return archivo.good(); }

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.

Validando datos al cargar

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.

bool validar_partida(const DatosPartida& datos) { if (datos.vidas < 0 || datos.vidas > 99) return false; if (datos.nivel_actual < 1 || datos.nivel_actual > 50) return false; if (datos.monedas < 0) return false; return true; } DatosPartida cargar_partida_o_default(const std::string& ruta) { DatosPartida datos; if (cargar_binario_seguro(datos, ruta) && validar_partida(datos)) { return datos; } SDL_Log("Guardado inválido o inexistente, usando datos por defecto"); return DatosPartida{}; // valores por defecto del struct }

Guardado atómico: evitando archivos corruptos a medias

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.

#include <cstdio> bool guardar_atomico(const DatosPartida& datos, const std::string& ruta) { std::string temporal = ruta + ".tmp"; if (!guardar_binario(datos, temporal)) return false; if (std::rename(temporal.c_str(), ruta.c_str()) != 0) { std::remove(temporal.c_str()); return false; } return true; }

`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
Arquitecto de Soluciones IA Empresarial
Especialista en LLMs, Agentes y Orquestación
guatemalia.com/#contacto · info@guatemalia.com

¿Necesitas implementar IA en tu empresa?

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

info@guatemalia.com