beasty-save-system / guides / scene-state.md

Estado de la escena

Cómo guardar los objetos que ya están en tu escena: BeastySaveable marca un objeto para que se guarde, BeastySaveManager los escribe todos en un archivo. Esta página cubre ambos, y los cuatro comportamientos en torno a los ids que deciden si tus objetos vuelven o no.

Si aún no has construido la configuración básica, save-without-code.md la explica clic por clic. Esta página explica qué hace realmente esa configuración.

Los dos componentes

BeastySaveable va en cualquier objeto cuyo estado quieras conservar. Tiene un id y una lista de componentes a capturar. Uno por GameObject — el componente no permite duplicados.

BeastySaveManager va en un objeto de la escena. Mantiene el BeastySaveSettings usado por el guardado de escena, y es en el que llamas a SaveAll y LoadAll. Expone una Instance estática.

Cada BeastySaveable habilitado se registra automáticamente con el manager. SaveAll recorre ese registro, le pide su estado a cada componente marcado de cada saveable, y escribe todo en un archivo, indexado por id.

Ids

El id es la forma en que un archivo de guardado reconoce un objeto entre sesiones. Todo en el guardado de escena se reduce a si el id es el mismo la próxima vez que el juego corra.

El campo Save Id con su botón New

Se autogenera, y es editable

Añade un BeastySaveable y recibe un id automáticamente. El inspector lo muestra como Save Id, con un botón New al lado que genera uno nuevo.

Puedes editar el id a mano, y hay buenas razones para hacerlo — player, door.cellar, chest.tutorial son más fáciles de encontrar en un archivo de guardado que un string aleatorio.

Advertencia Cambiar un id deja huérfanos los datos ya guardados bajo el anterior. El estado sigue en el archivo, bajo una clave que ya nada en la escena reclama, y el objeto con el nuevo id no lo encontrará. Decide tus ids antes de tener guardados que te importen. El botón New tiene exactamente el mismo efecto: rompe la conexión del objeto con todos los guardados existentes.

Un asset de prefab no lleva ningún id

El asset de prefab en sí no tiene id. Cada instancia recibe el suyo propio, generado cuando se crea. Dos copias del mismo prefab en una escena son dos saveables distintos con dos ids distintos, que es lo que quieres — son dos cofres diferentes.

Un objeto generado en tiempo de ejecución obtiene un id nuevo cada vez

Este es el que atrapa a la gente.

Un objeto que instancias con Instantiate mientras el juego corre recibe un id completamente nuevo, cada vez. Juega, genera un enemigo, guarda: el estado del enemigo va al archivo bajo un id generado en esa sesión. Reinicia, genera un enemigo, carga: el enemigo nuevo tiene otro id, no encuentra nada en el archivo, y se queda con su estado por defecto. El estado antiguo se queda en el archivo, inalcanzable, para siempre.

La solución es darle al objeto generado un id que tú controles, en el momento de generarlo:

using Beasty_SaveSystem;
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    public GameObject enemyPrefab;

    public GameObject Spawn(string enemyId, Vector3 position)
    {
        GameObject enemy = Instantiate(enemyPrefab, position, Quaternion.identity);

        // Un id estable, y los componentes a capturar.
        BeastySaveManager.Register(enemy, $"enemy.{enemyId}", enemy.transform);

        return enemy;
    }
}

Register añade el BeastySaveable por ti, configura el id, y asigna la lista de componentes — así que el objeto es guardable desde ese momento. Hay dos overloads:

static BeastySaveable Register(GameObject target, params Component[] components);
static BeastySaveable Register(GameObject target, string id, params Component[] components);

El que no lleva id conserva el que el objeto ya tiene — y si el objeto aún no tiene un BeastySaveable, se le añade uno con un id recién generado. Un id recién generado no puede coincidir con nada de un guardado anterior, lo cual está bien para objetos de escena y es inútil para copias generadas en tiempo de ejecución. Para cualquier cosa que generes, usa el overload que recibe un id.

El id tiene que ser algo que puedas reproducir en la siguiente ejecución. enemy.goblin.3 derivado de los datos de tu propio juego es estable. enemy. más Guid.NewGuid() no lo es — es el mismo problema otra vez, ahora en tu propio código.

Las contrapartes, cuando un objeto desaparece definitivamente:

BeastySaveManager.Unregister(gameObject);   // este ya no está
BeastySaveManager.UnregisterAll();          // limpia el registro
BeastySaveManager.SyncSceneSaveables();     // vuelve a escanear la escena en busca de saveables

Dos objetos con el mismo id: el segundo desaparece

Si dos saveables terminan con el mismo id, el segundo no se registra. Se registra un error, y ese objeto queda fuera del guardado sin más aviso — no se escribe, y no se restaura.

Los ids duplicados ocurren cuando copias y pegas un GameObject que ya tiene un id escrito a mano, o cuando generas dos objetos y pasas el mismo id a Register. Si un objeto se niega misteriosamente a persistir, revisa la consola en busca del error de id duplicado, y luego revisa su id en el inspector.

Los objetos inactivos se guardan

Un BeastySaveable se registra en OnEnable y se desregistra en OnDestroyno en OnDisable.

Así que un objeto que desactivas con SetActive(false) sigue en el registro, sigue escribiéndose en el guardado, y sigue restaurándose al cargar. Ese es el comportamiento que quieres: un enemigo deshabilitado o un panel de UI cerrado mantiene su estado en lugar de perderlo en el momento en que desaparece.

Destruir el objeto es lo que lo elimina del guardado.

Varios componentes del mismo tipo

Dos componentes BoxCollider en un mismo GameObject se guardan y recuperan bien los dos. Se almacenan por separado en el archivo (el segundo lleva un sufijo #1 en su clave) y cada uno se restaura donde corresponde. No tienes que hacer nada para que esto funcione.

Una lista Saved Components con dos componentes del mismo tipo marcados

Guardando y cargando

Desde un botón, sin código

SaveAll(string) y LoadAll(string) son métodos void que reciben un único string, lo que los hace conectables directamente al On Click () de un Button uGUI con el nombre del slot escrito en el inspector. Este es el camino sin código, y está cubierto paso a paso en save-without-code.md.

Desde código

BeastySaveManager manager = BeastySaveManager.Instance;

SaveResult saved = manager.SaveAllNow("slot1");
if (!saved.Success)
    Debug.LogError($"Save failed: {saved}");

LoadResult loaded = manager.LoadAllNow("slot1");
if (!loaded.Success)
    Debug.LogError($"Load failed: {loaded}");

SaveAllNow y LoadAllNow son los que devuelven un resultado. Úsalos siempre que quieras saber si funcionó — o sea, siempre, en código de producción. SaveAllNow también recibe un diccionario de metadatos opcional:

manager.SaveAllNow("slot1", new Dictionary<string, string>
{
    ["chapter"] = "3",
    ["playtime"] = "01:22",
});

SaveAll y LoadAll hacen el mismo trabajo pero no devuelven nada, porque un UnityEvent no puede usar un valor de retorno. En su lugar almacenan el resultado:

manager.SaveAll("slot1");
SaveResult result = manager.LastSaveResult;   // y manager.LastLoadResult después de una carga

manager.DeleteSlot("slot1") elimina un slot y su backup.

Los eventos

void OnEnable()
{
    BeastySaveManager.Instance.SaveCompleted += OnSaved;
    BeastySaveManager.Instance.LoadCompleted += OnLoaded;
}

void OnDisable()
{
    if (BeastySaveManager.Instance == null) return;
    BeastySaveManager.Instance.SaveCompleted -= OnSaved;
    BeastySaveManager.Instance.LoadCompleted -= OnLoaded;
}

void OnSaved(SaveResult result)
{
    if (result.Success) ShowToast("Game saved");
    else ShowError(result.Message);
}

void OnLoaded(LoadResult result)
{
    if (result.Success) return;
    if (result.BackupAvailable) OfferBackupRestore();
    else ShowError(result.Message);
}

SaveCompleted y LoadCompleted se disparan en cada guardado y carga, tanto desde los botones como desde código. Son el lugar correcto para poner el toast de “Game saved”, y también para ofrecer la copia de seguridad cuando una carga falla. LoadResult.BackupAvailable se rellena en cada resultado de carga, éxito o fallo — consulta backups-and-corruption.md.

Qué termina realmente en el archivo

Un guardado de escena tiene el tipo Beasty.SaveGroup y se ve así:

{
  "saveables": {
    "player": {
      "UnityEngine.Transform": { "module": "core", "data": { } }
    },
    "door.cellar": {
      "UnityEngine.BoxCollider":   { "module": "physics3d", "data": { } },
      "UnityEngine.BoxCollider#1": { "module": "physics3d", "data": { } }
    }
  }
}

Una entrada por id de saveable, una entrada por componente guardado dentro de él, y el módulo que escribió cada uno. El sufijo #1 es el segundo componente del mismo tipo en ese objeto.

Un saveable que está en la escena pero no tiene datos en el archivo — un objeto nuevo añadido después de que se escribió el guardado — se deja intacto, con una advertencia. No es un error, y no hace fallar la carga.

Marcar un componente que no se puede guardar

La lista Saved Components etiqueta cada componente con la capa que puede convertirlo: core, un id de módulo como ugui, o dev para un convertidor que registraste tú mismo.

Si marcas un componente que nada sabe convertir, el inspector te avisa, y un guardado fallará con TypeUnavailable. Desmárcalo, activa el módulo que necesita (converter-modules.md), o escribe un convertidor para él (custom-converters.md).

Ver también