Qual è il modo corretto di gestire i dati tra le scene?


52

Sto sviluppando il mio primo gioco 2D in Unity e mi sono imbattuto in una domanda che sembra importante.

Come gestisco i dati tra le scene?

Sembra che ci siano diverse risposte a questo:

  • Qualcuno menziona l'uso di PlayerPrefs , mentre altre persone mi hanno detto che questo dovrebbe essere usato per memorizzare altre cose come la luminosità dello schermo e così via.

  • Qualcuno mi ha detto che il modo migliore era assicurarsi di scrivere tutto in un savegame ogni volta che cambiavo scena e di assicurarsi che quando la nuova scena si caricava, ottenessi di nuovo le informazioni dal savegame. Questo mi è sembrato dispendioso in termini di prestazioni. Ho sbagliato?

  • L'altra soluzione, che è quella che ho implementato finora è quella di avere un oggetto di gioco globale che non venga distrutto tra le scene, gestendo tutti i dati tra le scene. Quindi, quando inizia il gioco, carico una scena iniziale in cui viene caricato questo oggetto. Al termine, carica la prima scena di gioco reale, di solito un menu principale.

Questa è la mia implementazione:

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;
    //...
}

Questo oggetto può essere gestito su altre mie classi in questo modo:

private GameController gameController = GameController.Instance;

Mentre questo ha funzionato finora, mi presenta un grosso problema: se voglio caricare direttamente una scena, diciamo ad esempio il livello finale del gioco, non posso caricarlo direttamente, poiché quella scena non contiene questo oggetto di gioco globale .

Sto gestendo questo problema nel modo sbagliato? Esistono pratiche migliori per questo tipo di sfida? Mi piacerebbe sentire le tue opinioni, pensieri e suggerimenti su questo tema.

Grazie

Risposte:


64

In questa risposta sono elencati i modi fondamentali per gestire questa situazione. Tuttavia, la maggior parte di questi metodi non si adatta bene ai grandi progetti. Se vuoi qualcosa di più scalabile e non hai paura di sporcarti le mani, controlla la risposta di Lea Hayes sui framework di Iniezione delle dipendenze .


1. Uno script statico per contenere solo dati

È possibile creare uno script statico per contenere solo i dati. Poiché è statico, non è necessario assegnarlo a GameObject. Puoi semplicemente accedere ai tuoi dati come ScriptName.Variable = data;ecc.

Professionisti:

  • Nessuna istanza o singleton richiesti.
  • Puoi accedere ai dati da qualsiasi parte del tuo progetto.
  • Nessun codice aggiuntivo per passare valori tra le scene.
  • Tutte le variabili e i dati in un unico script simile a un database facilitano la loro gestione.

Contro:

  • Non sarai in grado di usare una Coroutine all'interno dello script statico.
  • Probabilmente finirai con enormi linee di variabili in una singola classe se non ti organizzi bene.
  • Non è possibile assegnare campi / variabili all'interno dell'editor.

Un esempio:

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

Se hai bisogno che il tuo script sia assegnato a un GameObject o derivi da MonoBehavior, puoi aggiungere una DontDestroyOnLoad(gameObject);linea alla tua classe in cui può essere eseguita una volta (inserirla Awake()è di solito la strada da percorrere per questo) .

Professionisti:

  • Tutti i lavori MonoBehaviour (ad esempio Coroutine) possono essere eseguiti in sicurezza.
  • È possibile assegnare campi all'interno dell'editor.

Contro:

  • Probabilmente dovrai adattare la scena a seconda della sceneggiatura.
  • Probabilmente dovrai verificare quale secene è caricato per determinare cosa fare in Update o in altre funzioni / metodi generali. Ad esempio, se si sta eseguendo un'operazione con l'interfaccia utente in Update (), è necessario verificare se è stata caricata la scena corretta per eseguire il lavoro. Ciò provoca un sacco di controlli if-else o switch-case.

3. PlayerPrefs

Puoi implementarlo se vuoi che i tuoi dati vengano archiviati anche se il gioco viene chiuso.

Professionisti:

  • Facile da gestire poiché Unity gestisce tutto il processo in background.
  • Puoi trasmettere i dati non solo tra le scene ma anche tra le istanze (sessioni di gioco).

Contro:

  • Utilizza il file system.
  • I dati possono essere facilmente modificati dal file prefs.

4. Salvataggio in un file

Questo è un po 'eccessivo per la memorizzazione di valori tra le scene. Se non hai bisogno di crittografia, ti scoraggio da questo metodo.

Professionisti:

  • Hai il controllo dei dati salvati rispetto a PlayerPrefs.
  • Puoi trasmettere i dati non solo tra le scene ma anche tra le istanze (sessioni di gioco).
  • È possibile trasferire il file (il concetto di contenuto generato dall'utente si basa su questo).

Contro:

  • Lento.
  • Utilizza il file system.
  • Possibilità di leggere / caricare conflitti causati dall'interruzione del flusso durante il salvataggio.
  • I dati possono essere facilmente modificati dal file se non si implementa una crittografia (che renderà il codice ancora più lento).

5. Modello Singleton

Il modello Singleton è un argomento molto caldo nella programmazione orientata agli oggetti. Alcuni lo suggeriscono, altri no. Ricercalo tu stesso ed effettua la chiamata appropriata in base alle condizioni del tuo progetto.

Professionisti:

  • Facile da installare e da utilizzare.
  • Puoi accedere ai dati da qualsiasi parte del tuo progetto.
  • Tutte le variabili e i dati in un unico script simile a un database facilitano la loro gestione.

Contro:

  • Un sacco di codice boilerplate il cui unico lavoro è mantenere e proteggere l'istanza singleton.
  • Ci sono argomenti forti contro l'uso del modello singleton . Sii cauto e fai le tue ricerche in anticipo.
  • Possibilità di scontro di dati a causa della scarsa implementazione.
  • Unity potrebbe avere difficoltà a gestire i modelli singleton 1 .

1 : Nel riepilogo del OnDestroymetodo di Singleton Script fornito in Unify Wiki , puoi vedere l'autore che descrive gli oggetti fantasma che sanguinano nell'editor dal runtime:

Quando Unity si chiude, distrugge gli oggetti in un ordine casuale. In linea di principio, un Singleton viene distrutto solo quando l'applicazione viene chiusa. Se uno script chiama Instance dopo che è stato distrutto, creerà un oggetto fantasma difettoso che rimarrà sulla scena dell'Editor anche dopo aver interrotto la riproduzione dell'applicazione. Veramente male! Quindi, questo è stato fatto per essere sicuro che non stiamo creando quell'oggetto fantasma difettoso.


8

Un'opzione leggermente più avanzata è eseguire l'iniezione di dipendenza con un framework come Zenject .

Questo ti lascia libero di strutturare la tua applicazione come preferisci; per esempio,

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

È quindi possibile associare il tipo al contenitore IoC (inversione del controllo). Con Zenject questa azione viene eseguita all'interno di a MonoInstallero a ScriptableInstaller:

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

L'istanza singleton di PlayerProfileviene quindi iniettata in altre classi che vengono istanziate tramite Zenject. Idealmente tramite l'iniezione del costruttore, è possibile anche l'iniezione di proprietà e di campo annotandole con l' Injectattributo Zenject .

Quest'ultima tecnica di attributo viene utilizzata per iniettare automaticamente gli oggetti di gioco della tua scena poiché Unity crea un'istanza di questi oggetti per te:

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


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

Per qualsiasi motivo, è possibile che si desideri associare un'implementazione per interfaccia anziché per tipo di implementazione. (Dichiarazione di non responsabilità, il seguente non dovrebbe essere un esempio sorprendente; dubito che vorresti metodi di salvataggio / caricamento in questa posizione particolare ... ma questo mostra solo un esempio di come le implementazioni potrebbero variare nel comportamento).

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()
    {
        ...
    }
}

Che può quindi essere associato al contenitore IoC in un modo simile a prima:

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,
    }
}

3

Stai facendo le cose in modo positivo. È il modo in cui lo faccio, e chiaramente il modo in cui molte persone lo fanno perché questo script del caricatore automatico (puoi impostare una scena da caricare automaticamente ogni volta che premi Play) esiste: http://wiki.unity3d.com/index.php/ SceneAutoLoader

Entrambe le prime due opzioni sono anche cose di cui il tuo gioco potrebbe aver bisogno per salvare il gioco tra le sessioni, ma quelli sono strumenti sbagliati per questo problema.


Ho appena letto un po 'del link che hai pubblicato. Sembra che ci sia un modo per caricare automaticamente la scena iniziale in cui sto caricando l'oggetto di gioco globale. Sembra un po 'complesso, quindi avrò bisogno di un po' di tempo per decidere se è qualcosa che risolve il mio problema. Grazie per il tuo feedback!
Tenda Enrique Moreno

La sceneggiatura che ho collegato a una sorta di risolve quel problema, in quanto puoi colpire la riproduzione in qualsiasi scena piuttosto che dover ricordare di passare ogni volta alla scena di avvio. Inizia comunque il gioco dall'inizio, piuttosto che iniziare direttamente nell'ultimo livello; potresti mettere un trucco per permetterti di saltare a qualsiasi livello, o semplicemente modificare lo script di caricamento automatico per passare il livello al gioco.
derisione del

Sì bene. Il problema non era tanto il "fastidio" di dover ricordare di passare alla scena iniziale, quanto di dover hackerare per caricare il livello specifico in mente. Grazie comunque!
Tenda Enrique Moreno

1

Un modo ideale per memorizzare variabili tra le scene è attraverso una classe manager singleton. Creando una classe per l'archiviazione di dati persistenti e impostandola su DoNotDestroyOnLoad(), puoi assicurarti che sia immediatamente accessibile e persista tra le scene.

Un'altra opzione che hai è usare la PlayerPrefsclasse. PlayerPrefsè progettato per consentire di salvare i dati tra le sessioni di gioco , ma servirà comunque come mezzo per salvare i dati tra le scene .

Utilizzando una classe singleton e DoNotDestroyOnLoad()

Lo script seguente crea una classe singleton persistente. Una classe singleton è una classe progettata per eseguire solo una singola istanza contemporaneamente. Fornendo tale funzionalità, possiamo tranquillamente creare un riferimento automatico statico, per accedere alla classe da qualsiasi luogo. Ciò significa che puoi accedere direttamente alla classe con DataManager.instance, comprese le variabili pubbliche all'interno della classe.

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);
    }
}

Puoi vedere il singleton in azione, sotto. Si noti che non appena eseguo la scena iniziale, l'oggetto DataManager si sposta dall'intestazione specifica della scena all'intestazione "DontDestroyOnLoad", nella vista gerarchica.

Una schermata di registrazione di più scene caricate, mentre DataManager persiste sotto l'intestazione "DoNotDestroyOnLoad".

Usando la PlayerPrefsclasse

Unity ha una classe integrata per gestire i dati persistenti di base chiamatiPlayerPrefs . Tutti i dati impegnati nel PlayerPrefsfile persisteranno per tutte le sessioni di gioco , quindi, naturalmente, è in grado di conservare i dati attraverso le scene.

Il PlayerPrefsfile può memorizzare le variabili di tipo string, inte float. Quando inseriamo valori nel PlayerPrefsfile, ne forniamo un ulteriore stringcome chiave. Usiamo la stessa chiave per recuperare in seguito i nostri valori dal PlayerPreffile.

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();
    }
}

Si noti che prendo ulteriori precauzioni durante la gestione del PlayerPrefsfile:

  • Ho salvato ciascuna chiave come a private static string. Questo mi permette di garantire che sto sempre usando la chiave giusta e significa che se devo cambiare la chiave per qualsiasi motivo, non ho bisogno di assicurarmi di cambiare tutti i riferimenti ad essa.
  • Salvo il PlayerPrefsfile sul disco dopo averlo scritto. Questo probabilmente non farà differenza, se non si implementa la persistenza dei dati nelle sessioni di gioco. PlayerPrefs si salva sul disco nel corso di una normale applicazione stretta, ma non può naturalmente chiamare se il gioco si blocca.
  • Veramente controllo che ogni chiave esista in PlayerPrefs, prima di tentare di recuperare un valore ad essa associato. Questo potrebbe sembrare inutile doppio controllo, ma è una buona pratica avere.
  • Ho un Deletemetodo che cancella immediatamente il PlayerPrefsfile. Se non si intende includere la persistenza dei dati nelle sessioni di gioco, è possibile prendere in considerazione la possibilità di attivare questo metodo Awake. Deselezionando la PlayerPrefslima all'inizio di ogni partita, ci si assicura che tutti i dati che ha persistono dalla sessione precedente non viene erroneamente trattati come dati dalla corrente sessione.

Puoi vedere PlayerPrefsin azione, sotto. Nota che quando faccio clic su "Salva dati", chiamo direttamente il Savemetodo e quando faccio clic su "Carica dati", chiamo direttamente il Loadmetodo. La tua implementazione probabilmente varierà, ma dimostra le basi.

Una schermata di registrazione dei dati persistenti è stata sovrascritta dall'ispettore, tramite le funzioni Save () e Load ().


Come nota finale, dovrei sottolineare che è possibile espandere su base PlayerPrefs, per memorizzare tipi più utili. JPTheK9 fornisce una buona risposta a una domanda simile , in cui forniscono uno script per la serializzazione di array in forma di stringa, da archiviare in un PlayerPrefsfile. Ci indicano anche il Wiki della community di Unify , in cui un utente ha caricato uno PlayerPrefsXscript più ampio per consentire il supporto di una varietà più ampia di tipi, come vettori e array.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.