Parte: 15 — Herramientas, editores y automatización (tooling) · Fuente: Documentación de Godot 4 (FileAccess, JSON, ResourceSaver) y patrón de diseño data-driven ⏱️ Duración estimada: 75 min · Nivel: Avanzado
Cuando las estadísticas de cada enemigo viven en el código, cambiar el daño de un goblin obliga a tocar y recompilar lógica. El diseño data-driven separa los datos del comportamiento: el código lee tablas (CSV, JSON, recursos) y cualquiera puede ajustar el balance editando una hoja de cálculo. Pero datos externos significan datos potencialmente erróneos, así que la validación deja de ser opcional.
En esta clase construimos una herramienta de importación: leemos un catálogo de enemigos desde CSV/JSON con FileAccess, validamos cada registro (campos presentes, tipos correctos, rangos razonables, referencias que existen), acumulamos un informe legible de errores, y solo con los datos limpios generamos recursos .tres con ResourceSaver.save(). El principio rector: fallar temprano y ruidosamente en la importación, nunca en silencio durante el juego.
Al finalizar, el alumno podrá:
FileAccess y parsear CSV y JSON en Godot 4..tres a partir de los datos validados con ResourceSaver.save().| # | Tema | Por qué importa |
|---|---|---|
| 1 | Qué es el diseño data-driven | Permite iterar el balance sin tocar código. |
| 2 | Separar datos de comportamiento | Diseñadores editan datos; programadores, lógica. |
| 3 | Leer archivos con FileAccess |
Puerta de entrada a CSV/JSON externos. |
| 4 | Parsear CSV (get_csv_line) |
Formato cómodo desde hojas de cálculo. |
| 5 | Parsear JSON (JSON.parse_string) |
Formato para datos anidados. |
| 6 | Validación de campos y rangos | Atrapa errores antes de que lleguen al juego. |
| 7 | Informe de errores acumulado | Reporta todos los fallos, no solo el primero. |
| 8 | Generar .tres validados |
Convierte datos crudos en recursos usables. |
FileAccess: clase para leer y escribir archivos. Clave: se abre con FileAccess.open(ruta, modo) y devuelve null si falla.get_csv_line(): lee una línea CSV y la devuelve como PackedStringArray. Clave: respeta comillas y separadores estándar.JSON.parse_string(): convierte texto JSON en tipos nativos (Dictionary, Array). Clave: devuelve null si el JSON es inválido.ResourceSaver.save(): escribe un Resource a disco como .tres o .res. Clave: materializa los datos validados en recursos del proyecto.escena o id que no está). Clave: se detecta comprobando contra el conjunto de ids conocidos.Necesitas Godot 4.x. Prepara un archivo de datos de ejemplo; usaremos JSON por su claridad, pero el CSV se menciona en paralelo. Crea res://datos/enemigos.json con varios registros, incluyendo al menos uno inválido a propósito (un campo faltante o un valor fuera de rango) para ver la validación en acción.
Consulta la referencia de FileAccess para los modos de apertura: https://docs.godotengine.org/en/stable/classes/class_fileaccess.html. Para el parseo, la clase JSON: https://docs.godotengine.org/en/stable/classes/class_json.html. Y para guardar recursos, ResourceSaver: https://docs.godotengine.org/en/stable/classes/class_resourcesaver.html.
Construiremos un importador-validador que lee enemigos desde JSON, valida, reporta y genera un .tres por cada enemigo correcto. Lo haremos como script @tool para poder ejecutarlo en el editor.
enemigo_data.gd, para saber qué campos esperamos:@tool
class_name EnemigoData
extends Resource
@export var id: StringName = &""
@export var nombre: String = ""
@export var vida: int = 1
@export var velocidad: float = 0.0
importar_enemigos.gd. Lee el archivo con FileAccess y parsea el JSON, controlando los fallos de apertura y de formato:@tool
extends Node
const RUTA_ENTRADA := "res://datos/enemigos.json"
const DIR_SALIDA := "res://datos/enemigos/"
func importar() -> void:
var f := FileAccess.open(RUTA_ENTRADA, FileAccess.READ)
if f == null:
push_error("No se pudo abrir %s (error %d)" % [RUTA_ENTRADA, FileAccess.get_open_error()])
return
var texto := f.get_as_text()
f.close()
var datos = JSON.parse_string(texto)
if typeof(datos) != TYPE_ARRAY:
push_error("El JSON raíz debe ser un Array de enemigos.")
return
_procesar(datos)
func _procesar(filas: Array) -> void:
var errores: Array[String] = []
var validos: Array[EnemigoData] = []
var ids_vistos := {}
for i in filas.size():
var fila = filas[i]
var prefijo := "Fila %d" % i
if typeof(fila) != TYPE_DICTIONARY:
errores.append("%s: no es un objeto." % prefijo)
continue
# Campos obligatorios.
for campo in ["id", "nombre", "vida", "velocidad"]:
if not fila.has(campo):
errores.append("%s: falta el campo '%s'." % [prefijo, campo])
if not fila.has("id"):
continue
var id := StringName(str(fila.get("id", "")))
if id in ids_vistos:
errores.append("%s: id duplicado '%s'." % [prefijo, id])
ids_vistos[id] = true
# Rangos razonables.
var vida := int(fila.get("vida", 0))
if vida < 1 or vida > 9999:
errores.append("%s: vida %d fuera de rango [1, 9999]." % [prefijo, vida])
var vel := float(fila.get("velocidad", -1.0))
if vel < 0.0:
errores.append("%s: velocidad %.2f no puede ser negativa." % [prefijo, vel])
# Si esta fila no acumuló errores nuevos, se construye.
var rec := EnemigoData.new()
rec.id = id
rec.nombre = str(fila.get("nombre", ""))
rec.vida = vida
rec.velocidad = vel
validos.append(rec)
_reportar_y_guardar(errores, validos)
.tres solo si no hubo errores. Fallar temprano evita meter datos corruptos al proyecto:func _reportar_y_guardar(errores: Array[String], validos: Array[EnemigoData]) -> void:
if errores.size() > 0:
push_warning("Importación con %d errores:" % errores.size())
for e in errores:
print(" - " + e)
push_error("Corrige los errores antes de generar recursos.")
return
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(DIR_SALIDA))
for rec in validos:
var ruta := DIR_SALIDA + str(rec.id) + ".tres"
var err := ResourceSaver.save(rec, ruta)
if err == OK:
print("Generado: " + ruta)
else:
push_error("Fallo al guardar %s (error %d)" % [ruta, err])
@export tipo botón que llame a importar():@export var lanzar_importacion: bool = false:
set(v):
if v:
importar()
.tres generado. Corrige el dato, vuelve a marcar y esta vez aparecerán los recursos en res://datos/enemigos/. Para CSV, el patrón es idéntico cambiando el parseo por f.get_csv_line() en un bucle con la primera fila como cabecera.La lección observable: los datos malos se detienen en la importación con un informe claro, y solo los limpios se convierten en recursos.
get_csv_line() y mapea columnas a los campos.nombre no esté vacío y reporta la fila exacta si lo está.tipo con @export_enum y valida que su valor esté entre los permitidos.suelta que debe apuntar a un id existente..tres de "base de datos" que contenga el Array[EnemigoData] completo.Construye un importador-validador para una categoría de datos de tu juego (objetos, armas, niveles). Debe leer desde CSV o JSON con FileAccess, validar al menos cuatro reglas (campo obligatorio, tipo, rango y referencia o unicidad), acumular todos los errores en un informe legible, y generar recursos .tres con ResourceSaver.save() solo si el informe está vacío.
Criterio de aceptación: con un archivo que contenga al menos un registro inválido, la herramienta reporta todos los errores por fila y no genera ningún recurso; tras corregir los datos, la reejecución genera los .tres correctos y muestra un resumen del conteo.
| Síntoma | Causa y arreglo |
|---|---|
FileAccess.open devuelve null |
Ruta incorrecta o archivo inexistente. Comprueba con FileAccess.get_open_error() y usa rutas res://. |
El JSON se parsea como null |
Formato inválido (coma final, comillas). Valida el JSON y comprueba typeof(datos). |
| La validación aborta al primer error | Usaste return en vez de acumular. Guarda los mensajes en un array y reporta al final. |
Se generan .tres aun con datos malos |
No condicionaste el guardado al informe vacío. Guarda solo si errores.is_empty(). |
Los .tres no aparecen en el FileSystem |
La carpeta no existe o falta refrescar. Crea el dir con make_dir_recursive_absolute y reescanea. |
❓ ¿CSV o JSON para datos de juego? CSV es cómodo si tus datos son tabulares y los editan en una hoja de cálculo. JSON conviene cuando hay estructura anidada (listas dentro de un registro). Ambos se validan igual.
❓ ¿Por qué validar si los datos los hago yo? Porque el punto del data-driven es que otros los editen, y porque un error tipográfico en una hoja de cálculo es fácil. Validar en la importación evita bugs opacos en runtime.
❓ ¿Debo generar .tres o leer el JSON directamente en el juego? Generar .tres te da recursos tipados, autocompletado y carga rápida. Leer JSON en caliente es útil para modding, a costa de validar cada vez.
❓ ¿Puedo correr esto en un pipeline automático? Sí: el mismo script puede invocarse desde un EditorScript o desde la línea de comandos con --headless, encajando en la automatización de builds de la clase siguiente de la parte.
FileAccess: https://docs.godotengine.org/en/stable/classes/class_fileaccess.htmlJSON: https://docs.godotengine.org/en/stable/classes/class_json.htmlResourceSaver: https://docs.godotengine.org/en/stable/classes/class_resourcesaver.htmlClase 258 - Gizmos e inspectores personalizados
Clase 260 - Recursos personalizados y bases de datos de juego