Как правильно обрабатывать данные между сценами?

52

Я разрабатываю свою первую 2D-игру в Unity, и я столкнулся с тем, что кажется важным вопросом.

Как мне обрабатывать данные между сценами?

Там, кажется, разные ответы на это:

  • Кто-то упоминал об использовании PlayerPrefs , в то время как другие говорили мне, что это следует использовать для хранения других вещей, таких как яркость экрана и так далее.

  • Кто-то сказал мне, что лучший способ - записывать все в сохраненную игру каждый раз, когда я меняю сцены, и чтобы при загрузке новой сцены снова получать информацию из сохраненной игры. Это показалось мне расточительным в исполнении. Был ли я не прав?

  • Другое решение, которое я реализовал до сих пор, заключается в создании глобального игрового объекта, который не разрушается между сценами и обрабатывает все данные между сценами. Поэтому, когда игра начинается, я загружаю стартовую сцену, куда загружается этот объект. После этого он загружает первую настоящую игровую сцену, обычно главное меню.

Это моя реализация:

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

В этом ответе перечислены основные способы решения этой ситуации. Хотя большинство из этих методов не подходят для больших проектов. Если вы хотите что-то более масштабируемое и не боитесь испачкать руки, ознакомьтесь с ответом Леа Хейс о платформе Dependency Injection .


1. Статический скрипт для хранения только данных

Вы можете создать статический скрипт для хранения только данных. Поскольку он статический, вам не нужно присваивать его GameObject. Вы можете просто получить доступ к своим данным, как ScriptName.Variable = data;и т.д.

Плюсы:

  • Никакой экземпляр или синглтон не требуется.
  • Вы можете получить доступ к данным из любого места в вашем проекте.
  • Нет дополнительного кода для передачи значений между сценами.
  • Все переменные и данные в одном похожем на базу данных сценарии облегчают их обработку.

Минусы:

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

Пример:

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 (например, сопрограммы) могут быть выполнены безопасно.
  • Вы можете назначить поля внутри редактора.

Минусы:

  • Вам, вероятно, нужно будет настроить сцену в зависимости от сценария.
  • Вам, вероятно, нужно будет проверить, какой secene загружен, чтобы определить, что делать в Update или других общих функциях / методах. Например, если вы что-то делаете с пользовательским интерфейсом в Update (), вам нужно проверить, загружена ли правильная сцена для выполнения работы. Это вызывает множество проверок if-else или switch-case.

3. PlayerPrefs

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

Плюсы:

  • Прост в управлении, поскольку Unity обрабатывает все фоновые процессы.
  • Вы можете передавать данные не только между сценами, но и между экземплярами (игровыми сеансами).

Минусы:

  • Использует файловую систему.
  • Данные могут быть легко изменены из файла prefs.

4. Сохранение в файл

Это немного излишне для хранения значений между сценами. Если вам не нужно шифрование, я отговариваю вас от этого метода.

Плюсы:

  • Вы контролируете данные, сохраненные в отличие от PlayerPrefs.
  • Вы можете передавать данные не только между сценами, но и между экземплярами (игровыми сеансами).
  • Вы можете передать файл (концепция, основанная на содержимом пользователя, основана на этом).

Минусы:

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

5. Синглтон

Синглтон-паттерн - действительно актуальная тема в объектно-ориентированном программировании. Некоторые предполагают это, а некоторые нет. Исследуйте это сами и сделайте соответствующий звонок в зависимости от условий вашего проекта.

Плюсы:

  • Прост в настройке и использовании.
  • Вы можете получить доступ к данным из любого места в вашем проекте.
  • Все переменные и данные в одном похожем на базу данных сценарии облегчают их обработку.

Минусы:

  • Много шаблонного кода, единственная задача которого - поддерживать и защищать экземпляр Singleton.
  • Существуют веские аргументы против использования одноэлементного шаблона . Будьте осторожны и сделайте свое исследование заранее.
  • Возможность столкновения данных из-за плохой реализации.
  • Единство может иметь трудности обработки одноточечны моделями 1 .

1 : В обзоре OnDestroyметода Singleton Script, представленном в Unify Wiki , вы можете увидеть автора, описывающего объекты-призраки, которые попадают в редактор из среды выполнения:

Когда Unity выходит, он уничтожает объекты в случайном порядке. В принципе, синглтон уничтожается только при выходе из приложения. Если какой-либо скрипт вызывает Instance после его уничтожения, он создаст ошибочный объект-призрак, который останется на сцене редактора даже после остановки воспроизведения приложения. Действительно плохо! Итак, это сделано для того, чтобы мы не создавали этот ошибочный объект-призрак.

С. Тарык Четин
источник
8

Немного более сложный вариант - выполнить внедрение зависимостей с помощью фреймворка, такого как Zenject .

Это позволяет вам структурировать ваше приложение так, как вы хотите; например,

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

Затем вы можете привязать тип к контейнеру IoC (инверсия управления). С Zenject это действие выполняется внутри MonoInstallerили ScriptableInstaller:

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

Затем экземпляр Singleton PlayerProfileвнедряется в другие классы, которые создаются с помощью Zenject. В идеале, через внедрение в конструктор, но внедрение свойств и полей также возможно путем добавления их к атрибуту Zenject Inject.

Последний метод атрибутов используется для автоматического ввода игровых объектов вашей сцены, так как Unity создает эти объекты для вас:

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


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

По какой-либо причине вам может потребоваться привязать реализацию по интерфейсу, а не по типу реализации. (Отказ от ответственности, следующее не должно быть удивительным примером; я сомневаюсь, что вам нужны методы Save / Load в этом конкретном месте ... но это просто показывает пример того, как реализации могут отличаться в поведении).

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

Обе первые две опции также могут быть необходимы вашей игре для сохранения игры между сеансами, но это неправильные инструменты для решения этой проблемы.

jhocking
источник
Я только что прочитал немного ссылки, которую вы разместили. Похоже, есть способ автозагрузки начальной сцены, где я загружаю глобальный игровой объект. Это выглядит немного сложным, поэтому мне понадобится некоторое время, чтобы решить, если это то, что решает мою проблему. Спасибо за ваш отзыв!
Энрике Морено Палатка
Сценарий, с которым я связался, решает эту проблему, так как вы можете нажимать кнопку воспроизведения в любой сцене, вместо того, чтобы каждый раз переключаться на сцену запуска. Тем не менее, он все же начинает игру с самого начала, а не запускается непосредственно на последнем уровне; Вы можете добавить чит-код, чтобы перейти на любой уровень, или просто изменить скрипт автозагрузки, чтобы передать уровень в игру.
Джоккинг
Да хорошо. Проблема была не столько в «раздражении» необходимости помнить о необходимости переключиться на стартовую сцену, сколько в том, чтобы взломать, чтобы загрузить определенный уровень в памяти. Спасибо, в любом случае!
Энрике Морено Палатка
1

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

Другой вариант - использовать PlayerPrefsкласс. PlayerPrefsпредназначен для сохранения данных между сеансами воспроизведения , но все равно будет служить средством сохранения данных между сценами .

Используя одноэлементный класс и DoNotDestroyOnLoad()

Следующий скрипт создает постоянный одноэлементный класс. Класс Singleton - это класс, предназначенный для одновременного запуска только одного экземпляра. Предоставляя такие функциональные возможности, мы можем безопасно создавать статические ссылки на себя, чтобы получить доступ к классу из любого места. Это означает, что вы можете напрямую обращаться к классу 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 , где пользователь загрузил более обширный PlayerPrefsXскрипт для поддержки большего разнообразия типов, таких как векторы и массивы.

Gnemlock
источник