¿Cuál es la forma correcta de manejar datos entre escenas?

52

Estoy desarrollando mi primer juego 2D en Unity y me he encontrado con lo que parece una pregunta importante.

¿Cómo manejo los datos entre escenas?

Parece que hay diferentes respuestas a esto:

  • Alguien mencionó el uso de PlayerPrefs , mientras que otras personas me dijeron que esto debería usarse para almacenar otras cosas como el brillo de la pantalla, etc.

  • Alguien me dijo que la mejor manera era asegurarse de escribir todo en un juego guardado cada vez que cambiaba las escenas, y asegurarme de que cuando se cargara la nueva escena, volviera a obtener la información del juego guardado. Esto me pareció un desperdicio en el rendimiento. ¿Estaba equivocado?

  • La otra solución, que es la que he implementado hasta ahora, es tener un objeto de juego global que no se destruya entre escenas, manejando todos los datos entre escenas. Entonces, cuando comienza el juego, cargo una escena de inicio donde se carga este objeto. Después de que esto termina, carga la primera escena real del juego, generalmente un menú principal.

Esta es mi implementación:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class GameController : MonoBehaviour {

    // Make global
    public static GameController Instance {
        get;
        set;
    }

    void Awake () {
        DontDestroyOnLoad (transform.gameObject);
        Instance = this;
    }

    void Start() {
        //Load first game scene (probably main menu)
        Application.LoadLevel(2);
    }

    // Data persisted between scenes
    public int exp = 0;
    public int armor = 0;
    public int weapon = 0;
    //...
}

Este objeto se puede manejar en mis otras clases como esta:

private GameController gameController = GameController.Instance;

Si bien esto ha funcionado hasta ahora, me presenta un gran problema: si quiero cargar directamente una escena, digamos, por ejemplo, el nivel final del juego, no puedo cargarlo directamente, ya que esa escena no contiene este Objeto de juego global .

¿Estoy manejando este problema de manera incorrecta? ¿Existen mejores prácticas para este tipo de desafío? Me encantaría escuchar sus opiniones, pensamientos y sugerencias sobre este tema.

Gracias

Tienda Enrique Moreno
fuente

Respuestas:

64

En esta respuesta se enumeran las formas fundamentales de manejar esta situación. Aunque, la mayoría de estos métodos no se adaptan bien a proyectos grandes. Si desea algo más escalable y no tiene miedo de ensuciarse las manos, lea la respuesta de Lea Hayes sobre los marcos de inyección de dependencia .


1. Un script estático para contener solo datos

Puede crear un script estático para contener solo datos. Como es estático, no necesita asignarlo a un GameObject. Simplemente puede acceder a sus datos como ScriptName.Variable = data;etc.

Pros:

  • No se requiere instancia o singleton.
  • Puede acceder a los datos desde cualquier parte de su proyecto.
  • No hay código extra para pasar valores entre escenas.
  • Todas las variables y datos en un solo script similar a una base de datos facilitan su manejo.

Contras:

  • No podrá usar una Corutina dentro del script estático.
  • Probablemente terminarás con grandes líneas de variables en una sola clase si no te organizas bien.
  • No puede asignar campos / variables dentro del editor.

Un ejemplo:

public static class PlayerStats
{
    private static int kills, deaths, assists, points;

    public static int Kills 
    {
        get 
        {
            return kills;
        }
        set 
        {
            kills = value;
        }
    }

    public static int Deaths 
    {
        get 
        {
            return deaths;
        }
        set 
        {
            deaths = value;
        }
    }

    public static int Assists 
    {
        get 
        {
            return assists;
        }
        set 
        {
            assists = value;
        }
    }

    public static int Points 
    {
        get 
        {
            return points;
        }
        set 
        {
            points = value;
        }
    }
}

2. DontDestroyOnLoad

Si necesita que su secuencia de comandos se asigne a un GameObject o se derive de MonoBehavior, entonces puede agregar una DontDestroyOnLoad(gameObject);línea a su clase donde se pueda ejecutar una vez (colocarla Awake()es la forma habitual de hacerlo ) .

Pros:

  • Todos los trabajos de MonoBehaviour (por ejemplo, Coroutines) se pueden realizar de forma segura.
  • Puede asignar campos dentro del editor.

Contras:

  • Probablemente necesitará ajustar su escena según el guión.
  • Probablemente necesitará verificar qué escena está cargada para determinar qué hacer en Actualización u otras funciones / métodos generales. Por ejemplo, si está haciendo algo con la interfaz de usuario en Update (), debe verificar si la escena correcta está cargada para hacer el trabajo. Esto causa un montón de verificaciones if-else o switch-case.

3. PlayerPrefs

Puedes implementar esto si también quieres que tus datos se almacenen incluso si el juego se cierra.

Pros:

  • Fácil de administrar ya que Unity maneja todos los procesos en segundo plano.
  • Puede pasar datos no solo entre escenas sino también entre instancias (sesiones de juego).

Contras:

  • Utiliza el sistema de archivos.
  • Los datos se pueden cambiar fácilmente desde el archivo de preferencias.

4. Guardar en un archivo

Esto es un poco exagerado para almacenar valores entre escenas. Si no necesita cifrado, lo desaconsejo con este método.

Pros:

  • Usted tiene el control de los datos guardados en lugar de PlayerPrefs.
  • Puede pasar datos no solo entre escenas sino también entre instancias (sesiones de juego).
  • Puede transferir el archivo (el concepto de contenido generado por el usuario depende de esto).

Contras:

  • Lento.
  • Utiliza el sistema de archivos.
  • Posibilidad de leer / cargar conflictos causados ​​por la interrupción del flujo mientras se guarda.
  • Los datos se pueden cambiar fácilmente desde el archivo a menos que implemente un cifrado (lo que hará que el código sea aún más lento).

5. Patrón Singleton

El patrón Singleton es un tema realmente candente en la programación orientada a objetos. Algunos lo sugieren y otros no. Investígalo tú mismo y realiza la llamada adecuada según las condiciones de tu proyecto.

Pros:

  • Fácil de configurar y usar.
  • Puede acceder a los datos desde cualquier parte de su proyecto.
  • Todas las variables y datos en un solo script similar a una base de datos facilitan su manejo.

Contras:

  • Un montón de código repetitivo cuyo único trabajo es mantener y asegurar la instancia singleton.
  • Existen fuertes argumentos en contra del uso del patrón singleton . Tenga cuidado y haga su investigación de antemano.
  • Posibilidad de choque de datos debido a una implementación deficiente.
  • La unidad puede tener dificultades para manejar patrones únicos 1 .

1 : En el resumen del OnDestroymétodo de Singleton Script proporcionado en Unify Wiki , puede ver al autor describiendo los objetos fantasmas que sangran en el editor desde el tiempo de ejecución:

Cuando Unity se cierra, destruye objetos en un orden aleatorio. En principio, un Singleton solo se destruye cuando se cierra la aplicación. Si algún script llama a Instance después de haber sido destruido, creará un objeto fantasma con errores que permanecerá en la escena del Editor incluso después de dejar de reproducir la Aplicación. ¡Muy mal! Entonces, esto se hizo para asegurarnos de que no estamos creando ese objeto fantasma con errores.

S. Tarık Çetin
fuente
8

Una opción un poco más avanzada es realizar una inyección de dependencia con un marco como Zenject .

Esto te deja libre para estructurar tu aplicación como quieras; por ejemplo,

public class PlayerProfile
{
    public string Nick { get; set; }
    public int WinCount { get; set; }
}

Luego puede vincular el tipo al contenedor IoC (inversión de control). Con Zenject esta acción se realiza dentro de a MonoInstallero a ScriptableInstaller:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        this.Container.Bind<PlayerProfile>()
            .ToSelf()
            .AsSingle();
    }
}

La instancia de singleton PlayerProfilese inyecta en otras clases que se instancian a través de Zenject. Idealmente a través de la inyección del constructor, pero la inyección de propiedades y campos también es posible al anotarlos con el Injectatributo de Zenject .

La última técnica de atributo se utiliza para inyectar automáticamente los objetos del juego de su escena, ya que Unity crea una instancia de estos objetos para usted:

public class WinDetector : MonoBehaviour
{
    [Inject]
    private PlayerProfile playerProfile = null;


    private void OnCollisionEnter(Collision collision)
    {
        this.playerProfile.WinCount += 1;
        // other stuff...
    }
}

Por alguna razón, es posible que también desee vincular una implementación por interfaz en lugar de por tipo de implementación. (Descargo de responsabilidad, se supone que lo siguiente no es un ejemplo sorprendente; dudo que desee los métodos Guardar / Cargar en esta ubicación en particular ... pero esto solo muestra un ejemplo de cómo las implementaciones pueden variar en el comportamiento).

public interface IPlayerProfile
{
    string Nick { get; set; }
    int WinCount { get; set; }

    void Save();
    void Load();
}

[JsonObject]
public class PlayerProfile_Json : IPlayerProfile
{
    [JsonProperty]
    public string Nick { get; set; }
    [JsonProperty]
    public int WinCount { get; set; }


    public void Save()
    {
        ...
    }

    public void Load()
    {
        ...
    }
}

[ProtoContract]
public class PlayerProfile_Protobuf : IPlayerProfile
{
    [ProtoMember(1)]
    public string Nick { get; set; }
    [ProtoMember(2)]
    public int WinCount { get; set; }


    public void Save()
    {
        ...
    }

    public void Load()
    {
        ...
    }
}

Que luego se puede vincular al contenedor de IoC de una manera similar a la anterior:

public class GameInstaller : MonoInstaller
{
    // The following field can be adjusted using the inspector of the
    // installer component (in this case) or asset (in the case of using
    // a ScriptableInstaller).
    [SerializeField]
    private PlayerProfileFormat playerProfileFormat = PlayerProfileFormat.Json;


    public override void InstallBindings()
    {
        switch (playerProfileFormat) {
            case PlayerProfileFormat.Json:
                this.Container.Bind<IPlayerProfile>()
                    .To<PlayerProfile_Json>()
                    .AsSingle();
                break;

            case PlayerProfileFormat.Protobuf:
                this.Container.Bind<IPlayerProfile>()
                    .To<PlayerProfile_Protobuf>()
                    .AsSingle();
                break;

            default:
                throw new InvalidOperationException("Unexpected player profile format.");
        }
    }


    public enum PlayerProfileFormat
    {
        Json,
        Protobuf,
    }
}
Lea Hayes
fuente
3

Estás haciendo las cosas de una buena manera. Es la forma en que lo hago, y claramente la forma en que muchas personas lo hacen porque este script de autocargador (puede configurar una escena para cargarse automáticamente cada vez que presiona Reproducir) existe: http://wiki.unity3d.com/index.php/ SceneAutoLoader

Las dos primeras opciones también son cosas que su juego puede necesitar para guardar el juego entre sesiones, pero esas son herramientas incorrectas para este problema.

jhocking
fuente
Acabo de leer un poco del enlace que publicaste. Parece que hay una manera de cargar automáticamente la escena inicial en la que estoy cargando el objeto de juego global. Parece un poco complejo, por lo que necesitaré algo de tiempo para decidir si es algo que resuelva mi problema. Gracias por sus comentarios!
Tienda Enrique Moreno
El guión que vinculé para resolver ese problema, en el sentido de que puedes presionar play en cualquier escena en lugar de tener que recordar cambiar a la escena de inicio cada vez. Sin embargo, todavía comienza el juego desde el principio, en lugar de comenzar directamente en el último nivel; podrías hacer un truco para que puedas saltar a cualquier nivel, o simplemente modificar el script de carga automática para pasar el nivel al juego.
Jhocking
Sí, bueno. El problema no era tanto la "molestia" de tener que recordar cambiar a la escena de inicio, sino tener que hackear para cargar el nivel específico en mente. ¡Gracias de cualquier manera!
Tienda Enrique Moreno
1

Una forma ideal de almacenar variables entre escenas es a través de una clase de administrador singleton. Al crear una clase para almacenar datos persistentes y establecer esa clase en DoNotDestroyOnLoad(), puede asegurarse de que sea inmediatamente accesible y persista entre escenas.

Otra opción que tienes es usar la PlayerPrefsclase. PlayerPrefsestá diseñado para permitirle guardar datos entre sesiones de reproducción , pero seguirá sirviendo como un medio para guardar datos entre escenas .

Usando una clase singleton y DoNotDestroyOnLoad()

El siguiente script crea una clase singleton persistente. Una clase singleton es una clase que está diseñada para ejecutar solo una instancia al mismo tiempo. Al proporcionar dicha funcionalidad, podemos crear de forma segura una autorreferencia estática, para acceder a la clase desde cualquier lugar. Esto significa que puede acceder directamente a la clase con DataManager.instance, incluidas las variables públicas dentro de la clase.

using UnityEngine;

/// <summary>Manages data for persistance between levels.</summary>
public class DataManager : MonoBehaviour 
{
    /// <summary>Static reference to the instance of our DataManager</summary>
    public static DataManager instance;

    /// <summary>The player's current score.</summary>
    public int score;
    /// <summary>The player's remaining health.</summary>
    public int health;
    /// <summary>The player's remaining lives.</summary>
    public int lives;

    /// <summary>Awake is called when the script instance is being loaded.</summary>
    void Awake()
    {
        // If the instance reference has not been set, yet, 
        if (instance == null)
        {
            // Set this instance as the instance reference.
            instance = this;
        }
        else if(instance != this)
        {
            // If the instance reference has already been set, and this is not the
            // the instance reference, destroy this game object.
            Destroy(gameObject);
        }

        // Do not destroy this object, when we load a new scene.
        DontDestroyOnLoad(gameObject);
    }
}

Puedes ver el singleton en acción, a continuación. Tenga en cuenta que tan pronto como ejecuto la escena inicial, el objeto DataManager se mueve del encabezado específico de la escena al encabezado "DontDestroyOnLoad", en la vista de jerarquía.

Una grabación de pantalla de varias escenas que se están cargando, mientras que DataManager persiste bajo el encabezado "DoNotDestroyOnLoad".

Usando la PlayerPrefsclase

Unity tiene una clase integrada para administrar los datos básicos persistentes llamadosPlayerPrefs . Cualquier dato comprometido con el PlayerPrefsarchivo persistirá en las sesiones del juego , por lo que, naturalmente, es capaz de persistir en las escenas.

El PlayerPrefsarchivo puede almacenar variables de tipos string, inty float. Cuando insertamos valores en el PlayerPrefsarchivo, proporcionamos un adicional stringcomo clave. Usamos la misma clave para luego recuperar nuestros valores del PlayerPrefarchivo.

using UnityEngine;

/// <summary>Manages data for persistance between play sessions.</summary>
public class SaveManager : MonoBehaviour 
{
    /// <summary>The player's name.</summary>
    public string playerName = "";
    /// <summary>The player's score.</summary>
    public int playerScore = 0;
    /// <summary>The player's health value.</summary>
    public float playerHealth = 0f;

    /// <summary>Static record of the key for saving and loading playerName.</summary>
    private static string playerNameKey = "PLAYER_NAME";
    /// <summary>Static record of the key for saving and loading playerScore.</summary>
    private static string playerScoreKey = "PLAYER_SCORE";
    /// <summary>Static record of the key for saving and loading playerHealth.</summary>
    private static string playerHealthKey = "PLAYER_HEALTH";

    /// <summary>Saves playerName, playerScore and 
    /// playerHealth to the PlayerPrefs file.</summary>
    public void Save()
    {
        // Set the values to the PlayerPrefs file using their corresponding keys.
        PlayerPrefs.SetString(playerNameKey, playerName);
        PlayerPrefs.SetInt(playerScoreKey, playerScore);
        PlayerPrefs.SetFloat(playerHealthKey, playerHealth);

        // Manually save the PlayerPrefs file to disk, in case we experience a crash
        PlayerPrefs.Save();
    }

    /// <summary>Saves playerName, playerScore and playerHealth 
    // from the PlayerPrefs file.</summary>
    public void Load()
    {
        // If the PlayerPrefs file currently has a value registered to the playerNameKey, 
        if (PlayerPrefs.HasKey(playerNameKey))
        {
            // load playerName from the PlayerPrefs file.
            playerName = PlayerPrefs.GetString(playerNameKey);
        }

        // If the PlayerPrefs file currently has a value registered to the playerScoreKey, 
        if (PlayerPrefs.HasKey(playerScoreKey))
        {
            // load playerScore from the PlayerPrefs file.
            playerScore = PlayerPrefs.GetInt(playerScoreKey);
        }

        // If the PlayerPrefs file currently has a value registered to the playerHealthKey,
        if (PlayerPrefs.HasKey(playerHealthKey))
        {
            // load playerHealth from the PlayerPrefs file.
            playerHealth = PlayerPrefs.GetFloat(playerHealthKey);
        }
    }

    /// <summary>Deletes all values from the PlayerPrefs file.</summary>
    public void Delete()
    {
        // Delete all values from the PlayerPrefs file.
        PlayerPrefs.DeleteAll();
    }
}

Tenga en cuenta que tomo precauciones adicionales cuando manejo el PlayerPrefsarchivo:

  • He guardado cada clave como a private static string. Esto me permite garantizar que siempre estoy usando la clave correcta, y significa que si tengo que cambiar la clave por cualquier motivo, no necesito asegurarme de cambiar todas las referencias a ella.
  • Guardo el PlayerPrefsarchivo en el disco después de escribir en él. Esto probablemente no hará la diferencia si no implementa la persistencia de datos en las sesiones de juego. PlayerPrefs se guardará en el disco durante el cierre normal de una aplicación, pero es posible que no llame naturalmente si el juego falla.
  • De hecho, verifico que cada clave exista en el PlayerPrefs, antes de intentar recuperar un valor asociado con él. Esto puede parecer una doble verificación sin sentido, pero es una buena práctica tenerlo.
  • Tengo un Deletemétodo que borra inmediatamente el PlayerPrefsarchivo. Si no tiene la intención de incluir la persistencia de datos en las sesiones de juego, puede considerar la activación de este método Awake. En la limpieza del PlayerPrefsarchivo al comienzo de cada juego, se asegura de que todos los datos que tenía persisten de la sesión anterior no se maneja erróneamente como datos de la actual sesión.

Puedes ver PlayerPrefsen acción, a continuación. Tenga en cuenta que cuando hago clic en "Guardar datos", llamo directamente al Savemétodo, y cuando hago clic en "Cargar datos", llamo directamente al Loadmétodo. Es probable que su propia implementación varíe, pero demuestra lo básico.

Una grabación en pantalla de los datos persistentes pasados ​​se sobrescribe del inspector, a través de las funciones Guardar () y Cargar ().


Como nota final, debo señalar que puede ampliar el básico PlayerPrefspara almacenar tipos más útiles. JPTheK9 proporciona una buena respuesta a una pregunta similar , en la que proporcionan un script para serializar matrices en forma de cadena, para ser almacenado en un PlayerPrefsarchivo. También nos señalan a Unify Community Wiki , donde un usuario ha subido un PlayerPrefsXscript más expansivo para permitir el soporte de una mayor variedad de tipos, como vectores y matrices.

Gnemlock
fuente