Jaki jest właściwy sposób obsługi danych między scenami?


52

Rozwijam swoją pierwszą grę 2D w Unity i natknąłem się na to, co wydaje się ważnym pytaniem.

Jak obsługiwać dane między scenami?

Wydaje się, że istnieją różne odpowiedzi na to:

  • Ktoś wspomniał o użyciu PlayerPrefs , podczas gdy inni powiedzieli mi, że należy to wykorzystać do przechowywania innych rzeczy, takich jak jasność ekranu i tak dalej.

  • Ktoś powiedział mi, że najlepszym sposobem jest zapisanie wszystkiego w zapisanej grze za każdym razem, gdy zmieniam sceny, i upewnienie się, że kiedy nowa scena się załaduje, ponownie uzyskam informacje z zapisanej gry. Wydawało mi się to marnotrawstwem w wydajności. Czy się myliłem?

  • Drugim rozwiązaniem, które wdrożyłem do tej pory, jest posiadanie globalnego obiektu gry, który nie jest niszczony między scenami, obsługując wszystkie dane między scenami. Kiedy więc gra się rozpoczyna, ładuję scenę początkową, w której ten obiekt jest ładowany. Po zakończeniu wczytuje pierwszą prawdziwą scenę gry, zwykle menu główne.

To jest moja realizacja:

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

Ten obiekt może być obsługiwany w moich innych klasach w następujący sposób:

private GameController gameController = GameController.Instance;

Chociaż to działało do tej pory, przedstawia mi jeden duży problem: jeśli chcę załadować scenę bezpośrednio, powiedzmy na przykład, ostateczny poziom gry, nie mogę załadować jej bezpośrednio, ponieważ ta scena nie zawiera tego globalny obiekt gry .

Czy radzę sobie z tym problemem w niewłaściwy sposób? Czy istnieją lepsze praktyki dla tego rodzaju wyzwań? Bardzo chciałbym usłyszeć wasze opinie, przemyślenia i sugestie na ten temat.

Dzięki

Odpowiedzi:


64

W tej odpowiedzi wymieniono podstawowe sposoby radzenia sobie z tą sytuacją. Chociaż większość z tych metod nie daje się dobrze skalować do dużych projektów. Jeśli chcesz czegoś bardziej skalowalnego i nie boisz się zabrudzić sobie rąk, sprawdź odpowiedź Lei Hayes na temat ram wstrzykiwania zależności .


1. Skrypt statyczny do przechowywania tylko danych

Możesz utworzyć skrypt statyczny do przechowywania tylko danych. Ponieważ jest statyczny, nie musisz przypisywać go do GameObject. Możesz po prostu uzyskać dostęp do swoich danych, takich jak ScriptName.Variable = data;itp.

Plusy:

  • Nie jest wymagana żadna instancja ani singleton.
  • Możesz uzyskać dostęp do danych z dowolnego miejsca w swoim projekcie.
  • Bez dodatkowego kodu do przekazywania wartości między scenami.
  • Wszystkie zmienne i dane w jednym skrypcie podobnym do bazy danych ułatwiają ich obsługę.

Cons:

  • W skrypcie statycznym nie będzie można używać Coroutine.
  • Prawdopodobnie skończysz z ogromnymi liniami zmiennych w jednej klasie, jeśli nie zorganizujesz się dobrze.
  • Nie można przypisywać pól / zmiennych w edytorze.

Przykład:

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

Jeśli potrzebujesz, aby skrypt został przypisany do GameObject lub pochodzić z MonoBehavior, możesz dodać DontDestroyOnLoad(gameObject);wiersz do swojej klasy, w którym można go raz wykonać (umieszczenie go Awake()jest zwykle sposobem na to) .

Plusy:

  • Wszystkie zadania MonoBehaviour (na przykład Korpusy) można wykonywać bezpiecznie.
  • Możesz przypisać pola do edytora.

Cons:

  • Prawdopodobnie będziesz musiał dostosować scenę w zależności od skryptu.
  • Prawdopodobnie będziesz musiał sprawdzić, który secene jest załadowany, aby określić, co robić w Aktualizacji lub innych ogólnych funkcjach / metodach. Na przykład, jeśli robisz coś z interfejsem użytkownika w Update (), musisz sprawdzić, czy załadowano prawidłową scenę do wykonania zadania. Powoduje to mnóstwo sprawdzeń typu „if-else” lub „case-case”.

3. PlayerPrefs

Możesz to zaimplementować, jeśli chcesz również przechowywać dane, nawet jeśli gra zostanie zamknięta.

Plusy:

  • Łatwy w zarządzaniu, ponieważ Unity obsługuje cały proces w tle.
  • Możesz przekazywać dane nie tylko między scenami, ale także między instancjami (sesjami gry).

Cons:

  • Używa systemu plików.
  • Dane można łatwo zmienić z pliku prefs.

4. Zapisywanie do pliku

Jest to trochę przesada w przechowywaniu wartości między scenami. Jeśli nie potrzebujesz szyfrowania, odradzam stosowanie tej metody.

Plusy:

  • Masz kontrolę nad danymi zapisanymi w przeciwieństwie do PlayerPrefs.
  • Możesz przekazywać dane nie tylko między scenami, ale także między instancjami (sesjami gry).
  • Możesz przesłać plik (koncepcja treści generowana przez użytkownika polega na tym).

Cons:

  • Powolny.
  • Używa systemu plików.
  • Możliwość konfliktu odczytu / ładowania spowodowanego przerwaniem strumienia podczas zapisywania.
  • Dane można łatwo zmienić z pliku, chyba że zaimplementujesz szyfrowanie (co spowoduje, że kod będzie jeszcze wolniejszy).

5. Wzór singletonu

Wzorzec singletonu jest naprawdę gorącym tematem w programowaniu obiektowym. Niektórzy to sugerują, a inni nie. Przeszukaj go sam i wykonaj odpowiednie połączenie w zależności od warunków projektu.

Plusy:

  • Łatwy w konfiguracji i użytkowaniu.
  • Możesz uzyskać dostęp do danych z dowolnego miejsca w swoim projekcie.
  • Wszystkie zmienne i dane w jednym skrypcie podobnym do bazy danych ułatwiają ich obsługę.

Cons:

  • Dużo kodu typu „plateplate”, którego jedynym zadaniem jest utrzymanie i zabezpieczenie wystąpienia singletonu.
  • Istnieją mocne argumenty przeciwko zastosowaniu wzorca singletonu . Zachowaj ostrożność i wcześniej dokonaj badań.
  • Możliwość kolizji danych z powodu złej implementacji.
  • Jedność może mieć trudności z obsługą wzorców singletonów 1 .

1 : W podsumowaniu OnDestroymetody Singleton Script podanej na Unify Wiki możesz zobaczyć autora opisującego obiekty-duchy, które przelewają się do edytora z środowiska wykonawczego:

Kiedy Unity kończy pracę, niszczy obiekty w losowej kolejności. Zasadniczo Singleton jest niszczony tylko po zakończeniu aplikacji. Jeśli dowolny skrypt wywoła Instancję po jej zniszczeniu, utworzy błędny obiekt-duch, który pozostanie na scenie edytora nawet po zatrzymaniu odtwarzania aplikacji. Naprawdę źle! Dlatego upewniono się, że nie tworzymy tego błędnego obiektu-widma.


8

Nieco bardziej zaawansowaną opcją jest wstrzyknięcie zależności za pomocą frameworka takiego jak Zenject .

Dzięki temu możesz dowolnie tworzyć strukturę aplikacji; na przykład,

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

Następnie można powiązać typ z kontenerem IoC (inwersja kontroli). W Zenject ta akcja jest wykonywana wewnątrz a MonoInstallerlub a ScriptableInstaller:

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

Instancja singletona PlayerProfilejest następnie wstrzykiwana do innych klas, które są tworzone przez Zenject. Idealnie przez wstrzyknięcie konstruktora, ale wstrzyknięcie właściwości i pola jest również możliwe poprzez opatrzenie ich Injectatrybutem Zenject .

Ta ostatnia technika atrybutu służy do automatycznego wstrzykiwania obiektów gry w Twojej scenie, ponieważ Unity tworzy dla Ciebie te obiekty:

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


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

Z jakiegokolwiek powodu możesz również powiązać implementację interfejsem, a nie typem implementacji. (Zastrzeżenie: poniższy przykład nie powinien być niesamowitym przykładem; wątpię, czy chcesz zapisać / wczytywać metody w tej konkretnej lokalizacji ... ale to tylko pokazuje przykład, w jaki sposób implementacje mogą różnić się zachowaniem).

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

Które można następnie powiązać z kontenerem IoC w podobny sposób jak wcześniej:

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

Robisz rzeczy w dobry sposób. Jest to sposób, w jaki to robię, i oczywiście sposób, w jaki robi to wiele osób, ponieważ ten skrypt autoloadera (możesz ustawić scenę, aby automatycznie ładował się jako pierwszy po uruchomieniu Play): http://wiki.unity3d.com/index.php/ SceneAutoLoader

Obie pierwsze dwie opcje to także rzeczy, których Twoja gra może potrzebować do zapisania gry między sesjami, ale są to złe narzędzia do tego problemu.


Właśnie przeczytałem trochę linku, który opublikowałeś. Wygląda na to, że istnieje sposób na automatyczne załadowanie początkowej sceny, w której ładuję globalny obiekt gry. Wygląda to trochę skomplikowane, więc potrzebuję trochę czasu, aby zdecydować, czy jest to coś, co rozwiązuje mój problem. Dziękuję za opinię!
Namiot Enrique Moreno

Skrypt, który podłączyłem, rozwiązuje ten problem, polegający na tym, że możesz grać w dowolną scenę, zamiast pamiętać, aby za każdym razem przełączać się na scenę startową. Nadal jednak zaczyna grę od samego początku, a nie bezpośrednio na ostatnim poziomie; możesz wprowadzić kod, aby umożliwić przejście do dowolnego poziomu, lub po prostu zmodyfikować skrypt autoload, aby przejść poziom do gry.
jhocking

No cóż. Kłopot polegał nie tyle na „irytacji”, że trzeba było przejść do sceny początkowej, co na włamywaniu się, aby załadować konkretny poziom. W każdym razie dzięki!
Namiot Enrique Moreno

1

Idealnym sposobem przechowywania zmiennych między scenami jest wykorzystanie klasy menedżera singletonów. Tworząc klasę do przechowywania trwałych danych i ustawiając tę ​​klasę na DoNotDestroyOnLoad(), możesz zapewnić, że będzie ona natychmiast dostępna i będzie się utrzymywać między scenami.

Inną opcją jest użycie PlayerPrefsklasy. PlayerPrefsjest przeznaczony do zapisywania danych między sesjami odtwarzania , ale nadal będzie służył do zapisywania danych między scenami .

Korzystanie z zajęć singleton i DoNotDestroyOnLoad()

Poniższy skrypt tworzy trwałą klasę singleton. Klasa singleton jest klasą zaprojektowaną do uruchamiania tylko jednej instancji w tym samym czasie. Zapewniając taką funkcjonalność, możemy bezpiecznie stworzyć statyczne odniesienie, aby uzyskać dostęp do klasy z dowolnego miejsca. Oznacza to, że możesz bezpośrednio uzyskać dostęp do klasy DataManager.instance, w tym dowolne zmienne publiczne w klasie.

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

Poniżej możesz zobaczyć singletona w akcji. Zauważ, że jak tylko uruchomię scenę początkową, obiekt DataManager przechodzi od nagłówka specyficznego dla sceny do nagłówka „DontDestroyOnLoad” w widoku hierarchii.

Nagrywanie ekranu wielu ładowanych scen, podczas gdy menedżer danych nadal występuje pod nagłówkiem „DoNotDestroyOnLoad”.

Korzystanie z PlayerPrefsklasy

Unity ma wbudowaną klasę do zarządzania podstawowymi trwałymi danymi o nazwiePlayerPrefs . Wszelkie dane przypisane do PlayerPrefspliku będą się utrzymywać podczas sesji gry , więc oczywiście jest w stanie utrwalać dane między scenami.

PlayerPrefsPlik można zapisać zmienne typów string, inta float. Kiedy wstawiamy wartości do PlayerPrefspliku, dostarczamy dodatkowy stringjako klucz. Używamy tego samego klucza, aby później pobrać nasze wartości z PlayerPrefpliku.

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

Pamiętaj, że podczas obsługi PlayerPrefspliku podejmuję dodatkowe środki ostrożności :

  • Zapisałem każdy klucz jako private static string. To pozwala mi zagwarantować, że zawsze używam właściwego klucza, a to oznacza, że ​​jeśli muszę zmienić klucz z jakiegokolwiek powodu, nie muszę się upewnić, że zmienię wszystkie odniesienia do niego.
  • Zapisuję PlayerPrefsplik na dysku po zapisaniu na nim. Prawdopodobnie nie zrobi to różnicy, jeśli nie zaimplementujesz utrwalania danych w sesjach odtwarzania. PlayerPrefs będzie zapisać na dysku w trakcie normalnego zamknięcia aplikacji, ale to nie może naturalnie zadzwonić jeśli gra wywala.
  • Rzeczywiście sprawdzam , czy każdy klucz istnieje w PlayerPrefs, zanim spróbuję pobrać wartość z nim powiązaną. Może się to wydawać bezcelowym podwójnym sprawdzaniem, ale warto to zrobić.
  • Mam Deletemetodę, która natychmiast czyści PlayerPrefsplik. Jeśli nie zamierzasz uwzględniać trwałości danych w sesjach odtwarzania, możesz rozważyć włączenie tej metody Awake. Przez wyczyszczenie PlayerPrefspliku na początku każdej gry, upewnij się, że wszelkie dane, które nie utrzymują się od poprzedniej sesji nie jest błędnie traktowane jako dane z bieżącej sesji.

Możesz zobaczyć PlayerPrefsw akcji poniżej. Zauważ, że kiedy klikam „Zapisz dane”, bezpośrednio wywołuję Savemetodę, a kiedy klikam „Ładuj dane”, bezpośrednio wywołuję Loadmetodę. Twoja implementacja prawdopodobnie będzie się różnić, ale pokazuje podstawy.

Zapis ekranowy trwających danych został zastąpiony przez inspektora za pomocą funkcji Save () i Load ().


Na koniec chciałbym zauważyć, że możesz rozwinąć podstawowe PlayerPrefs, aby przechowywać bardziej przydatne typy. JPTheK9 zapewnia dobrą odpowiedź na podobne pytanie , w którym zapewniają skrypt do szeregowania tablic w postaci ciągów, które mają być przechowywane w PlayerPrefspliku. Wskazują nas także na Wiki społeczności Unify , gdzie użytkownik przesłał bardziej rozbudowany PlayerPrefsXskrypt, aby umożliwić obsługę większej liczby typów, takich jak wektory i tablice.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.