Creando un Sistema de Guardado y Carga de Partidas en Godot 4: Persistencia para tus Juegos
Este tutorial te guiará paso a paso en la creación de un sistema de guardado y carga de partidas en Godot 4. Aprenderás a serializar datos, guardarlos en archivos y recuperarlos para que los jugadores nunca pierdan su progreso. ¡Haz tus juegos realmente persistentes!

🚀 Introducción: La Importancia del Guardado y Carga de Partidas
¿Qué sería de un juego si cada vez que el jugador lo cierra, pierde todo su progreso? Frustrante, ¿verdad? Un sistema de guardado y carga de partidas es fundamental en casi cualquier videojuego. Permite a los jugadores continuar su aventura donde la dejaron, manteniendo su inventario, estadísticas, posición en el mapa y cualquier otro dato relevante del estado del juego.
En este tutorial, exploraremos cómo implementar un sistema robusto y flexible de guardado y carga en Godot 4. Nos centraremos en la serialización de datos, la gestión de archivos y la integración de este sistema con diferentes aspectos de tu juego.
🎯 Objetivos del Tutorial
Al finalizar este tutorial, serás capaz de:
- Entender los conceptos básicos de persistencia de datos en Godot.
- Utilizar la clase
FileAccesspara leer y escribir archivos. - Serializar y deserializar datos de juego complejos (diccionarios, arrays, objetos Godot).
- Diseñar una estructura para los datos a guardar.
- Implementar funciones de guardado y carga automáticas y manuales.
- Manejar posibles errores durante el proceso de E/S de archivos.
📖 Conceptos Fundamentales de Persistencia
Antes de sumergirnos en el código, es crucial entender algunos conceptos:
- Serialización: El proceso de convertir un objeto o estructura de datos en un formato que puede ser almacenado (por ejemplo, en un archivo) o transmitido. En Godot, esto a menudo implica convertir tipos de datos complejos (como
Vector2,Color, objetos personalizados) en tipos básicos que pueden ser guardados directamente (números, cadenas, booleanos, arrays, diccionarios). - Deserialización: El proceso inverso a la serialización; reconstruir un objeto o estructura de datos a partir de su representación almacenada.
- Ruta de Archivo: La ubicación donde se guardará o leerá el archivo. En Godot, es común usar
user://para rutas específicas del usuario, que apuntan a un directorio persistente para la aplicación en el sistema operativo del usuario. Esto es crucial para la portabilidad y para evitar permisos de escritura en directorios del sistema.
💾 Formatos de Archivo para Guardar Datos
Existen varios formatos que podemos utilizar para guardar nuestros datos. Los más comunes son:
| Formato | Descripción | Ventajas | Desventajas |
|---|---|---|---|
| --- | --- | --- | --- |
| JSON | Formato de texto ligero y legible por humanos, basado en pares clave-valor. | Legible, fácil de parsear en muchos lenguajes. | Puede ser menos eficiente para datos muy grandes o binarios. |
| CSV | Formato de texto simple para datos tabulares, separados por comas. | Muy simple, ideal para tablas de datos sencillas. | No adecuado para estructuras de datos complejas o jerárquicas. |
| --- | --- | --- | --- |
| BINARIO | Datos almacenados en formato binario puro. | Muy eficiente en espacio y velocidad de lectura/escritura. | No legible por humanos, más complejo de implementar. |
| Godot (Config) | ConfigFile de Godot, similar a un archivo INI. | Nativo de Godot, ideal para configuraciones simples. | Limitado para datos de juego complejos, no soporta arrays anidados. |
Para este tutorial, utilizaremos JSON debido a su equilibrio entre legibilidad y facilidad de uso con los tipos de datos nativos de Godot (especialmente diccionarios y arrays).
🛠️ Configurando el Proyecto Godot
Comienza creando un nuevo proyecto Godot 4. No necesitamos nada complejo para el escenario, simplemente una escena base para probar el guardado.
- Crea una nueva escena 2D (o 3D si prefieres). Llama al nodo raíz
Main. Guarda esta escena comomain_scene.tscn. - Añade algunos nodos de UI para interactuar con nuestro sistema de guardado:
- Un
ButtonllamadoGuardarButton(Texto: "Guardar Partida") - Un
ButtonllamadoCargarButton(Texto: "Cargar Partida") - Un
LabelllamadoMensajeLabelpara mostrar el estado del guardado/carga. - Un
LineEditllamadoNombreJugadorLineEditpara guardar el nombre del jugador. - Un
SpinBoxllamadoPuntosVidaSpinBoxpara guardar puntos de vida. - Un
ButtonllamadoIncrementarPuntosButton(Texto: "+1 Vida").
- Un
Organiza estos nodos de forma que sean visibles y accesibles en la escena.
⚙️ Creando el Script de Guardado y Carga (SaveLoadManager.gd)
Crearemos un singleton (Autoload) para gestionar el guardado y la carga globalmente en nuestro juego. Esto significa que podemos acceder a sus funciones desde cualquier parte sin tener que instanciarlo.
- Crea un nuevo script GDScript llamado
SaveLoadManager.gden tu carpetares://scripts/. - Actívalo como Autoload: Ve a
Proyecto->Ajustes del Proyecto->Autoload. Haz clic en el icono de carpeta para seleccionarSaveLoadManager.gd, asígnale el nombreSaveLoadManagery haz clic enAñadir.
Ahora, abramos SaveLoadManager.gd y empecemos a programar.
extends Node
const SAVE_FILE_NAME = "user://savegame.json"
# --- Propiedades de ejemplo para guardar ---
var player_name: String = "Jugador_Default"
var health: int = 100
var level: int = 1
var inventory: Array[String] = ["Espada", "Poción", "Escudo"]
var player_position: Vector2 = Vector2(0, 0)
# --- Función para obtener los datos del juego actuales ---
func _get_game_data() -> Dictionary:
# Esta función debería recolectar el estado actual de tu juego
# Aquí usamos las propiedades de ejemplo del manager para simplificar
# En un juego real, obtendrías estos datos de los nodos correspondientes
return {
"player_name": player_name,
"health": health,
"level": level,
"inventory": inventory,
"player_position": player_position
}
# --- Función para aplicar los datos cargados al juego ---
func _apply_game_data(data: Dictionary):
player_name = data.get("player_name", "")
health = data.get("health", 0)
level = data.get("level", 0)
inventory = data.get("inventory", [])
player_position = data.get("player_position", Vector2(0, 0))
print("Datos aplicados: ", data)
emit_signal("game_data_loaded") # Emitimos una señal para que otros nodos actualicen su UI/estado
# --- Señal para notificar que los datos han sido cargados ---
signal game_data_loaded
# --- GUARDAR PARTIDA ---
func save_game() -> Error:
var data_to_save: Dictionary = _get_game_data()
# Convertimos el diccionario a una cadena JSON
var json_string = JSON.stringify(data_to_save, " ") # " " para pretty-print
var file = FileAccess.open(SAVE_FILE_NAME, FileAccess.WRITE)
if file == null:
print_error("Error al abrir archivo para guardar: ", FileAccess.get_open_error())
return FileAccess.get_open_error()
file.store_string(json_string)
file.close()
print("Partida guardada exitosamente en ", SAVE_FILE_NAME)
return OK
# --- CARGAR PARTIDA ---
func load_game() -> Error:
if not FileAccess.file_exists(SAVE_FILE_NAME):
print_error("Archivo de guardado no encontrado: ", SAVE_FILE_NAME)
return ERR_FILE_NOT_FOUND
var file = FileAccess.open(SAVE_FILE_NAME, FileAccess.READ)
if file == null:
print_error("Error al abrir archivo para cargar: ", FileAccess.get_open_error())
return FileAccess.get_open_error()
var content = file.get_as_text()
file.close()
var json_parse_result = JSON.parse_string(content)
if json_parse_result == null:
print_error("Error al parsear JSON del archivo de guardado.")
return ERR_PARSE_ERROR
var loaded_data: Dictionary = json_parse_result
_apply_game_data(loaded_data)
print("Partida cargada exitosamente desde ", SAVE_FILE_NAME)
return OK
# --- Eliminar Archivo de Guardado (opcional, para debug) ---
func delete_save_file():
if FileAccess.file_exists(SAVE_FILE_NAME):
DirAccess.remove_absolute(SAVE_FILE_NAME)
print("Archivo de guardado eliminado.")
else:
print("No hay archivo de guardado para eliminar.")
📝 Explicación del Código SaveLoadManager.gd
SAVE_FILE_NAME: Define la ruta del archivo de guardado.user://es una ruta especial que Godot resuelve a un directorio de datos persistente para tu aplicación, ideal para guardar datos de usuario.- Propiedades de Ejemplo:
player_name,health,level,inventory,player_positionson datos de ejemplo que queremos guardar. En un juego real, estos datos provendrían de nodos específicos (un nodo de jugador, un gestor de inventario, etc.). _get_game_data(): Esta función es clave. Es la encargada de recolectar todos los datos del estado actual del juego que deseamos guardar. Los agrupa en unDictionaryque es fácilmente serializable a JSON.- Importante: Si tu juego tiene nodos con propiedades dinámicas, necesitarás iterar sobre ellos y recoger sus datos. Por ejemplo,
for item in inventory_manager.get_items(): data["inventory_items"].append(item.to_dictionary()).
- Importante: Si tu juego tiene nodos con propiedades dinámicas, necesitarás iterar sobre ellos y recoger sus datos. Por ejemplo,
_apply_game_data(data: Dictionary): Hace lo opuesto. Toma un diccionario de datos cargados y los aplica al estado del juego. Esto implica actualizar las propiedades del jugador, el inventario, la posición de los enemigos, etc.save_game(): Abre el archivo de guardado en modo escritura (FileAccess.WRITE). Serializa el diccionario de datos a una cadena JSON usandoJSON.stringify()y la guarda en el archivo. Es crucial manejar errores si el archivo no puede ser abierto.load_game(): Primero verifica si el archivo existe. Luego, lo abre en modo lectura (FileAccess.READ), lee todo su contenido como texto, y lo parsea de JSON a un diccionario de Godot usandoJSON.parse_string(). Finalmente, llama a_apply_game_data()para restaurar el estado del juego.game_data_loadedseñal: Esta señal es muy útil. Permite que otros nodos en tu juego (como la UI) sepan cuándo se han cargado nuevos datos y necesitan actualizarse.
🎮 Integrando el Sistema en la Escena Principal (main_scene.gd)
Ahora, vamos a conectar nuestra UI en main_scene.tscn con las funciones del SaveLoadManager.
- Adjunta un nuevo script a tu nodo
Mainllamadomain_scene.gd. - Conecta las señales de los botones:
GuardarButton->pressed()aMainCargarButton->pressed()aMainIncrementarPuntosButton->pressed()aMain- También conecta la señal
game_data_loadeddelSaveLoadManagera una función enMain.
extends Node2D
@onready var player_name_line_edit = $VBoxContainer/NombreJugadorLineEdit
@onready var health_spin_box = $VBoxContainer/PuntosVidaSpinBox
@onready var message_label = $VBoxContainer/MensajeLabel
func _ready():
# Inicializamos la UI con los datos actuales del manager (o defaults)
_update_ui()
# Conectamos a la señal del SaveLoadManager para actualizar la UI cuando se carguen datos
SaveLoadManager.game_data_loaded.connect(_on_game_data_loaded)
func _update_ui():
player_name_line_edit.text = SaveLoadManager.player_name
health_spin_box.value = SaveLoadManager.health
message_label.text = "Estado actual: " + SaveLoadManager.player_name + ", Vida: " + str(SaveLoadManager.health)
func _on_GuardarButton_pressed():
# Primero, actualizamos los datos en el manager con lo que hay en la UI
SaveLoadManager.player_name = player_name_line_edit.text
SaveLoadManager.health = int(health_spin_box.value)
var error = SaveLoadManager.save_game()
if error == OK:
message_label.text = "Partida guardada exitosamente!"
else:
message_label.text = "Error al guardar partida: " + str(error)
func _on_CargarButton_pressed():
var error = SaveLoadManager.load_game()
if error == OK:
message_label.text = "Partida cargada exitosamente!"
_update_ui() # La UI se actualizará automáticamente via la señal, pero lo forzamos aquí también si la señal aún no está conectada
else:
message_label.text = "Error al cargar partida: " + str(error)
func _on_IncrementarPuntosButton_pressed():
SaveLoadManager.health += 1
_update_ui()
func _on_game_data_loaded():
print("Señal 'game_data_loaded' recibida en Main. Actualizando UI.")
_update_ui()
📝 Explicación del Código main_scene.gd
@onready var: Obtiene referencias a los nodos de la UI para poder interactuar con ellos._ready(): Se encarga de inicializar la UI con los datos actuales (por defecto o cargados si es un juego que carga al inicio) y conecta la señalgame_data_loadeddelSaveLoadManager._update_ui(): Es una función de ayuda que toma los datos delSaveLoadManagery actualiza los elementos de la UI (LineEdit, SpinBox, Label)._on_GuardarButton_pressed(): Antes de llamar aSaveLoadManager.save_game(), es crucial recoger los datos actuales de la UI (como el nombre del jugador o la vida) y asignárselos a las propiedades delSaveLoadManager._on_CargarButton_pressed(): Llama aSaveLoadManager.load_game(). Una vez que los datos se cargan y se aplican en elSaveLoadManager, se emite la señalgame_data_loaded, lo que a su vez llama a_on_game_data_loaded()en esta escena para actualizar la UI._on_IncrementarPuntosButton_pressed(): Simplemente incrementa los puntos de vida delSaveLoadManagery actualiza la UI.
✨ Mejoras Avanzadas y Consideraciones
Este sistema es una base sólida, pero puedes expandirlo con varias mejoras:
🔒 Seguridad y Encriptación
Para juegos donde la manipulación de los archivos de guardado podría ser un problema (por ejemplo, para evitar trampas), considera encriptar o ofuscar los datos. Godot ofrece herramientas como HashingContext o puedes implementar tus propios algoritmos XOR simples para dificultar la lectura directa del JSON.
Ejemplo de Ofuscación Básica (No es encriptación real)
# En SaveLoadManager.gd
func _obfuscate_string(input_string: String, key: int) -> String:
var obfuscated_chars = []
for char_code in input_string.to_utf8_buffer():
obfuscated_chars.append(char_code ^ key) # XOR con una clave
return obfuscated_chars.hex_encode()
func _deobfuscate_string(obfuscated_hex: String, key: int) -> String:
var deobfuscated_chars = []
var buffer = ParsedJSON.new()
buffer.parse(obfuscated_hex.hex_decode())
for byte_value in buffer:
deobfuscated_chars.append(byte_value ^ key)
return String.new().parse_utf8(PackedByteArray(deobfuscated_chars))
# Usa estas funciones en save_game y load_game
# var json_string = _obfuscate_string(JSON.stringify(data_to_save), 123)
# var content = _deobfuscate_string(file.get_as_text(), 123)
🔄 Múltiples Slots de Guardado
En lugar de un único archivo savegame.json, puedes permitir que los jugadores tengan múltiples ranuras de guardado (savegame_slot1.json, savegame_slot2.json, etc.). Esto implicaría pasar un parámetro slot_number a las funciones save_game() y load_game().
📦 Guardado Automático
Implementa un guardado automático periódico (cada X minutos) o en puntos clave del juego (al pasar de nivel, al entrar en una nueva área). Esto se puede lograr con un Timer en el SaveLoadManager.
🧩 Guardado de Objetos Complejos (Nodos, Recursos)
Guardar la posición de un Vector2 o un Color es sencillo porque JSON lo maneja bien con arrays [x, y] o [r, g, b, a]. Pero ¿qué pasa con objetos más complejos como enemigos con estados, recursos o inventarios complejos?
- Nodos: En lugar de guardar los propios nodos, guarda sus propiedades clave (posición, vida, ID de tipo de enemigo, inventario). Al cargar, recrea los nodos y aplica esas propiedades.
- Recursos (Resources): Algunos recursos pueden ser serializados. Si tienes un recurso
PlayerStats.tres, puedes guardar las propiedades de ese recurso en tu diccionario de guardado. Al cargar, creas un nuevo recurso de ese tipo y le asignas las propiedades.
# Ejemplo de cómo un objeto personalizado podría serializarse
class_name EnemyData
extends RefCounted # O Resource
var id: String
var position: Vector2
var health: int
var defeated: bool
func to_dictionary():
return {
"id": id,
"position": position.x,
"position_y": position.y, # Godot JSON serializa Vector2 a array, pero aquí lo hacemos explícito
"health": health,
"defeated": defeated
}
static func from_dictionary(data: Dictionary) -> EnemyData:
var enemy_data = EnemyData.new()
enemy_data.id = data.get("id", "")
enemy_data.position = Vector2(data.get("position_x", 0), data.get("position_y", 0))
enemy_data.health = data.get("health", 0)
enemy_data.defeated = data.get("defeated", false)
return enemy_data
# En _get_game_data():
# var enemies_to_save = []
# for enemy in get_tree().get_nodes_in_group("enemies"):
# enemies_to_save.append(enemy.get_save_data())
# data["enemies"] = enemies_to_save
# En _apply_game_data():
# for enemy_data_dict in data.get("enemies", []):
# var enemy_data = EnemyData.from_dictionary(enemy_data_dict)
# # Recrear el enemigo en el juego y aplicar enemy_data
📝 Debugging y Archivos de Guardado
Durante el desarrollo, querrás inspeccionar tus archivos de guardado. Recuerda que la ruta user:// se mapea a diferentes lugares según el sistema operativo:
- Windows:
%APPDATA%/Godot/app_userdata/<your_project_name> - Linux:
~/.local/share/godot/app_userdata/<your_project_name> - macOS:
~/Library/Application Support/Godot/app_userdata/<your_project_name>
Puedes imprimir OS.get_user_data_dir() para ver la ruta exacta en tiempo de ejecución.
✅ Conclusión
Has aprendido a construir un sistema robusto de guardado y carga de partidas en Godot 4 utilizando archivos JSON. Este es un componente esencial para la mayoría de los juegos, y dominarlo te permitirá crear experiencias de juego mucho más ricas y persistentes para tus jugadores. Experimenta con diferentes tipos de datos y complejidades para adaptar este sistema a las necesidades específicas de tu proyecto.
¡Ahora puedes garantizar que tus jugadores nunca perderán su progreso y siempre podrán retomar la aventura!
Tutoriales relacionados
- Navegación entre Escenas y Gestión de Estado en Godot 4: Creando un Flujo de Juego Dinámicointermediate15 min
- Creando Sistemas de Dialogo Interactivos en Godot 4: Da Voz a tus Personajesintermediate20 min
- Creando un Sistema de Inventario Básico en Godot 4: Gestión de Objetos y Mochilaintermediate20 min
- Creando Efectos de Partículas Espectaculares en Godot 4: Explosiones, Magia y Másintermediate20 min
- Creando Personajes Animados 2D en Godot 4: Diseñando y Programando Movimientobeginner15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!