Який правильний спосіб обробляти дані між сценами?


52

Я розвиваю свою першу 2D гру в Unity, і я натрапив на те, що здається важливим питанням.

Як обробляти дані між сценами?

Здається, на це є різні відповіді:

  • Хтось згадує використання PlayerPrefs , тоді як інші люди говорили мені, що це слід використовувати для зберігання інших речей, таких як яскравість екрана тощо.

  • Хтось сказав мені, що найкращим способом було переконатися, що потрібно записувати все в Savegame кожен раз, коли я міняю сцени, і переконайтесь, що коли завантажується нова сцена, отримайте інформацію з Savegame знову. Це здавалося мені марним у виконанні. Я помилявся?

  • Іншим рішенням, яке я реалізував поки що, є створення глобального ігрового об’єкта, який не руйнується між сценами, обробляючи всі дані між сценами. Отже, коли гра починається, я завантажую початкову сцену, куди цей об’єкт завантажений. Після цього він завантажує першу справжню ігрову сцену, як правило, головне меню.

Це моя реалізація:

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

З цим об’єктом можна обробляти й інші мої класи на зразок цього:

private GameController gameController = GameController.Instance;

Хоча це працювало дотепер, це представляє перед нами одну велику проблему: якщо я хочу завантажити безпосередньо сцену, скажімо, наприклад, про кінцевий рівень гри, я не можу завантажити її безпосередньо, оскільки ця сцена не містить цього глобальний ігровий об’єкт .

Чи я вирішую цю проблему неправильно? Чи є кращі практики для подібного виклику? Я хотів би почути ваші думки, думки та пропозиції щодо цього питання.

Дякую

Відповіді:


64

У цій відповіді перелічені основні способи вирішення цієї ситуації. Хоча більшість із цих методів не підходять до великих проектів. Якщо ви хочете чогось більш масштабованого і не боїтеся забруднити руки, ознайомтеся з відповіддю Леї Хейз про рамки введення залежностей .


1. Статичний сценарій для зберігання лише даних

Ви можете створити статичний сценарій для зберігання лише даних. Оскільки він статичний, вам не потрібно призначати його GameObject. Ви можете просто отримати доступ до своїх даних, наприклад ScriptName.Variable = data;тощо.

Плюси:

  • Ні екземпляр, ні одинарний не потрібно.
  • Ви можете отримати доступ до даних з будь-якого місця вашого проекту.
  • Немає зайвого коду для передачі значень між сценами.
  • Усі змінні та дані в одному сценарії, подібному до бази даних, спрощують їх обробку.

Мінуси:

  • Ви не зможете використовувати Coroutut всередині статичного сценарію.
  • Ви, мабуть, укладете величезні рядки змінних в одному класі, якщо ви не будете добре організовуватись.
  • Ви не можете призначити поля / змінні всередині редактора.

Приклад:

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

Якщо вам потрібно, щоб ваш скрипт був призначений GameObject або був похідний від MonoBehavior, тоді ви можете додати DontDestroyOnLoad(gameObject);рядок до свого класу, де він може бути виконаний один раз (розміщення його Awake()- це, як правило, шлях для цього) .

Плюси:

  • Усі завдання MonoBehaviour (наприклад, Coroutines) можна виконати безпечно.
  • Ви можете призначити поля всередині редактора.

Мінуси:

  • Ймовірно, вам буде потрібно відрегулювати свою сцену залежно від сценарію.
  • Можливо, вам доведеться перевірити, який сецен завантажений, щоб визначити, що робити в Update або інших загальних функціях / методах. Наприклад, якщо ви щось робите з користувальницьким інтерфейсом в Update (), вам потрібно перевірити, чи правильно завантажена сцена, щоб виконати роботу. Це спричиняє навантаження перевірок "case-else" або "case-switch".

3. PlayerPrefs

Ви можете реалізувати це, якщо ви також хочете, щоб ваші дані зберігалися, навіть якщо гра закрита.

Плюси:

  • Легко керувати, оскільки Unity обробляє весь фоновий процес.
  • Ви можете передавати дані не лише між сценами, але і між екземплярами (ігровими сеансами).

Мінуси:

  • Використовує файлову систему.
  • Дані можна легко змінити з файлу prefs.

4. Збереження у файл

Це трохи зайвий набір для зберігання значень між сценами. Якщо вам не потрібно шифрування, я відвертаю вас від цього методу.

Плюси:

  • Ви контролюєте збережені дані на відміну від PlayerPrefs.
  • Ви можете передавати дані не лише між сценами, але і між екземплярами (ігровими сеансами).
  • Ви можете перенести файл (концепція вмісту, створена користувачем, покладається на це).

Мінуси:

  • Повільно.
  • Використовує файлову систему.
  • Можливість зчитування / завантаження конфліктів, викликаних перериванням потоку при збереженні.
  • Дані можна легко змінити з файлу, якщо ви не реалізуєте шифрування (Це зробить код ще повільнішим.)

5. Однотонний візерунок

Одинарний малюнок - дійсно гаряча тема об’єктно-орієнтованого програмування. Деякі пропонують це, а деякі ні. Дослідіть це самостійно та здійсніть відповідний дзвінок залежно від умов вашого проекту.

Плюси:

  • Легка як у налаштуванні, так і у використанні.
  • Ви можете отримати доступ до даних з будь-якого місця вашого проекту.
  • Усі змінні та дані в одному сценарії, подібному до бази даних, спрощують їх обробку.

Мінуси:

  • Багато кодового коду, єдиним завданням якого є підтримка та безпека одиночного екземпляра.
  • Є вагомі аргументи проти використання однотонної картини . Будьте обережні і заздалегідь зробіть свої дослідження.
  • Можливість зіткнення даних через погану реалізацію.
  • У єдності можуть виникнути труднощі в роботі з однотонними зразками 1 .

1 : У стислому описі OnDestroyметоду Singleton Script, представленому в Unify Wiki , ви можете побачити автора, що описує привидні об'єкти, які кровоточать у редактор з часу виконання:

Коли Unity виходить, він руйнує об'єкти у випадковому порядку. В принципі, Singleton знищується лише тоді, коли програма закривається. Якщо будь-який скрипт викликає інстанцію після того, як він був знищений, він створить баггі-об’єкт-привид, який залишиться на сцені редактора навіть після припинення відтворення програми. Справді погано! Отже, це було впевнено, що ми не створюємо цей глючний привид-об’єкт.


8

Трохи більш вдосконаленим варіантом є виконання ін'єкції залежності з такою рамкою, як Zenject .

Це дозволяє вам структурувати свою програму, як тільки ви хочете; наприклад,

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

Потім ви можете прив’язати тип до контейнера IoC (інверсія управління). З Zenject ця дія виконується всередині a MonoInstallerабо a ScriptableInstaller:

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

Одиничний екземпляр PlayerProfileпотім вводиться в інші класи, які інстанціюються через Zenject. В ідеалі через конструкторський впорскування, але введення властивостей і полів також можливе, анотувавши їх Injectатрибутом Zenject .

Останній метод атрибутів використовується для автоматичного введення ігрових об’єктів вашої сцени, оскільки Unity створює ці об’єкти для вас:

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


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

З будь-якої причини ви також можете прив’язати реалізацію до інтерфейсу, а не до типу реалізації. (Відмова від відповідальності. Наведене нижче не повинно бути дивовижним прикладом; я сумніваюся, що ви хочете зберегти / завантажувати методи в цьому конкретному місці ... але це лише показує приклад того, як реалізація може змінюватися в поведінці).

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

Потім можна прив’язати до контейнера IoC аналогічним чином, як і раніше:

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

Ти робиш справи добре. Це так, як я це роблю, і зрозуміло, як це роблять багато людей, тому що цей сценарій автозавантажувача (ви можете встановити сцену для автоматичного завантаження першим, коли натискаєте Play): http://wiki.unity3d.com/index.php/ SceneAutoLoader

Обидва перші два варіанти - це також речі, які можуть знадобитися вашій грі для збереження гри між сеансами, але це неправильні інструменти для вирішення цієї проблеми.


Я просто прочитав трохи посилання, яке ви опублікували. Здається, є спосіб автозавантажити початкову сцену, куди я завантажую глобальний ігровий об’єкт. Це виглядає трохи складно, тому мені знадобиться певний час, щоб вирішити, чи це щось вирішує мою проблему. Дякуємо за ваш відгук!
Енріке Морено намет

Сценарій, з яким я пов'язаний, вирішує цю проблему, тому що ви можете потрапити на гру в будь-яку сцену, а не потрібно кожного разу переходити до сцени запуску. Він все ще починає гру з самого початку, а не починає безпосередньо з останнього рівня; ви можете поставити в чіт, щоб ви могли перейти на будь-який рівень, або просто змінити сценарій автозавантаження, щоб передати рівень в гру.
поштовх

Так, добре. Біда полягала не в тому, що "роздратування" того, що потрібно пам'ятати, щоб перейти на стартову сцену, як стільки, скільки потрібно рубати, щоб завантажити конкретний рівень на увазі. Все одно, дякую!
Енріке Морено Монт

1

Ідеальний спосіб зберігання змінних між сценами - через клас одиночного менеджера. Створивши клас для зберігання стійких даних та встановивши його для цього DoNotDestroyOnLoad(), ви можете переконатися, що він є негайно доступним та зберігається між сценами.

Ще одним варіантом є використання PlayerPrefsкласу. PlayerPrefsпризначений для того, щоб ви могли зберігати дані між ігровими сесіями , але він все одно буде служити засобом для збереження даних між сценами .

Використання однокласного класу та DoNotDestroyOnLoad()

Наступний скрипт створює стійкий однокласний клас. Клас синглтон - це клас, який призначений для запуску лише одного екземпляра одночасно. Забезпечивши таку функціональність, ми можемо сміливо створити статичну само посилання, щоб отримати доступ до класу з будь-якого місця. Це означає, що ви можете безпосередньо отримувати доступ до класу DataManager.instance, включаючи будь-які загальнодоступні змінні всередині класу.

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

Ви можете бачити одиночне дію нижче. Зауважте, що як тільки я запускаю початкову сцену, об’єкт DataManager переміщується від заголовка, що відповідає сцені, до заголовка "DontDestroyOnLoad", у вікні ієрархії.

Екранна запис декількох сцен, що завантажуються, а DataManager зберігається під заголовком "DoNotDestroyOnLoad".

Використання PlayerPrefsкласу

Unity має вбудований клас для управління базовими стійкими даними, що називаютьсяPlayerPrefs . Будь-які дані, присвячені PlayerPrefsфайлу, зберігатимуться протягом ігрових сеансів , тому, природно, він може зберігати дані на різних сценах.

PlayerPrefsФайл може зберігати змінні типів string, intі float. Коли ми вставляємо значення у PlayerPrefsфайл, ми надаємо додаткові stringяк ключові. Ми використовуємо той самий ключ, щоб пізніше отримати свої значення з PlayerPrefфайлу.

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

Зауважте, що я вживаю додаткових запобіжних заходів під час обробки PlayerPrefsфайлу:

  • Я зберегла кожен ключ як private static string. Це дозволяє мені гарантувати, що я завжди використовую правильний ключ, і це означає, що якщо мені доведеться змінити ключ з будь-якої причини, мені не потрібно переконатися, що я змінюю всі посилання на нього.
  • Я зберігаю PlayerPrefsфайл на диску після запису на нього. Це, ймовірно, не змінить значення, якщо ви не реалізуєте збереження даних протягом ігрових сесій. PlayerPrefs буде зберегти на диск під час звичайного застосування близько, але він не може природно назвати , якщо ваша гра вилітає.
  • Я фактично перевіряю, чи існує кожен ключ у PlayerPrefs, перш ніж спробувати отримати значення, пов'язане з ним. Це може здатися безглуздою подвійну перевірку, але це хороша практика.
  • У мене є Deleteметод, який негайно витирає PlayerPrefsфайл. Якщо ви не збираєтесь включати збереження даних протягом ігрових сесій, ви можете попросити запустити цей метод Awake. Знявши PlayerPrefsфайл на початку кожної гри, ви переконаєтеся , що будь-які дані , які так завзято від попередньої сесії не помилково обробляється як дані з поточної сесії.

PlayerPrefsДії ви можете побачити нижче. Зауважте, що натискаючи "Зберегти дані", я безпосередньо викликаю Saveметод, а коли натискаю "Завантажити дані", я безпосередньо викликаю Loadметод. Ваша власна реалізація, ймовірно, буде різною, але вона демонструє основи.

Екранний запис даних, що зберігаються, перезаписується з інспектора через функції Save () та Load ().


На завершення слід зазначити, що ви можете розширити основні PlayerPrefs, щоб зберігати більше корисних типів. JPTheK9 дає хорошу відповідь на аналогічне запитання , в якому вони надають сценарій для серіалізації масивів у строкову форму, що зберігається у PlayerPrefsфайлі. Вони також вказують нам на Unify Community Wiki , де користувач завантажив більш розширений PlayerPrefsXсценарій, щоб забезпечити підтримку більшої кількості типів, таких як вектори та масиви.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.