Передача данных между сценами в Unity — применение мультисценности в разработке простых игр

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

Поговорим о статических классах для хранения данных, различных способах подгрузки сцен движком Unity, а также коснёмся кейса подключения Admob к проекту. Информация предоставлена официальной документацией и доброжелательным коммьюнити разработчиков.

Ситуация


Моя игра представляет собой две сцены — главное меню, которое видно сразу при загрузке и, непосредственно, игровая сцена с механикой, в которую в зависимости от выбранной опции подгружается префаб объекта. Объединить их в одну сцену мне не представлялось возможным, так как на меню завязано несколько довольно сложных объектов, да и удобнее всё же разделять сущности.

Ранее для хранения данных я бы просто использовала некий объект-контроллер, но с выгрузкой сцены он перестаёт существовать.

Передача данных (static class)


Оказалось, что Unity превосходно умеет работать с кодом, даже если он мирно лежит файликом в папке скриптов и не прикреплён компонентом к объекту на сцене (это было не очевидно для новичка). Например, таким файлом может быть статический класс такого вида:

using UnityEngine;

public static class DataHolder
{
    private static GameObject prefabName;

    public static GameObject Prefab
    {
        get
        {
            return prefabName;
        }
        set
        {
            prefabName = value;
        }
    }
}


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

С помощью такого механизма можно также передавать в другие сцены настройки из меню, например — язык локализации, настройки звуков и музыки и многое другое.

Мультисценность (SceneManagement)


Меня всё устраивало и так, пока не поступила задача подключить к проекту Admob (рекламу), таким образом, чтобы ролик показывался прямо в начале игровой сцены. Как выяснилось, тут есть тонкости: запрос ролика занимает существенное время, и он просто не успевает прийти при переключении сцен. Лепить дополнительные задержки в проекте не хотелось, тем более, что у нас есть куча времени, пока игрок «залипает» в меню. Тут я и узнала, что переключать сцены «жёстко» нет никакой необходимости, ведь есть замечательная опция аддитивной загрузки (без выгрузки предыдущей сцены).

Подгружаю игровую сцену контроллером меню (сцена с меню и объектом рекламы остаётся загруженной тоже):

SceneManager.LoadScene(1,LoadSceneMode.Additive);

По завершению уровня, выгружаю сцену игры игровым контроллером (чтобы не висела в памяти):

SceneManager.LoadScene(0,LoadSceneMode.Single);

С использованием такой схемы, реклама загружается сразу при старте приложения, и вызвать показ рекламного ролика можно в любой нужный момент. Аналогично можно поступать с любыми необходимыми вам объектами.

Проблемы


К сожалению, даже при аддитивной загрузке сцен, не удастся вволю покопаться в объектах одной сцены из другой. Ссылки на объекты придётся передавать через некий «медиатор» (в моём случае использовался тот самый статический класс).

Будьте внимательны при инстанцировании префабов, если активны несколько сцен — у меня они все решили затолкаться в неправильную сцену (об этом в другой раз).

Ссылки на документацию
Поделиться публикацией

Комментарии 23

    0
    Не уверен, что Static класс это хорошее решение проблемы, почему не использовать возможности Unity (Object.DontDestroyOnLoad, Game Manager, Singleton Unity3D) для продления жизни GameObject между сценами? По крайне мере на основе паттерна Singleton я сделал, чтобы очки игрока сохранялись между сценами.

    public class GameStatus : MonoBehaviour
    {
        ...
    
        // State variables
        [SerializeField] private int _currentScore = 0;
    
        void Awake()
        {
            int objectsCount = FindObjectsOfType<GameStatus>().Length;
    
            if (objectsCount > 1)
            {
                gameObject.SetActive(false);
                Destroy(gameObject);
            }
            else
            {
                DontDestroyOnLoad(gameObject );
            }
        }
    
        ...
    
        public void ResetGame()
        {
            Destroy(gameObject);
        }    
    }
    
    
      0
      Даже в ссылках которые вы прикрпели (Static Class) используется Singleton
        0
        В статье описан личный опыт в основном, на идеальность решения не претендую.
        Вообще нужно посчитать, что будет затратнее по ресурсам, они дороги, так как проект ориентирован на мобильные. Меня учили, что на сцене лучше лишних объектов не держать, но буду рада если расскажете почему решение с синглтоном лучше.
          0
          Если вы не претендуете, то вот это определенно лишнее
          Данная статья будет полезна начинающим разработчикам игр.
          )))
          Статик, мягко говоря, так себе решение. Представьте ситуацию, когда вам надо ресетнуться, полоностью перезапустить игру, а у вас в статике 100500+ полей. Будете ручками все это дефолтить? Статик для констант удобен. Хотя если аккуратно, то имеет право на жизнь, например в качестве контейнеров.
            0

            эм, но в таком случае в статик кладется объект а-ля AllStaticData и обнуляется только он.

              –2
              Это ад перфекциониста )))
                +1

                И почему это плохо, иметь статичный, доступный отовсюду некий GameState?

              +1
              Я использовала здесь статик, потому что храню в нём ровно два поля :)
              Но да, полезная ремарка, если начинающий разработчик вдруг решит туда напихать гораздо больше.
              Попыталась намекнуть на это словами «простых игр» в названии, буду аккуратнее.
                0
                Вот, вот. А соблазн запихать туда всё, что можно и нельзя, появится в следущую секунду, после того, как человек узнает и поймет, что такое статик )))
              0
              Данный способ позволяет уничтожить объект в случаее необходимости, а также использовать в связке с другими игровыми объектами на экране, например с общим HUD между сценами. Касаемо статика, клас будет болтаться в памяти с момента первого к нему обращения и до закрытия приложения.
                0
                А если его и не нужно уничтожать? Что значит «использовать в связке с другими объектами»?
                  0
                  Как пример, что вы можете поместить в данный объект логику не только по хранению чего-то, но и взаимодествия с элементами управления на UI. Обновление счетчика жизний и брони, очков и т.д. Касаемо данного пункта это не совсем про вашу проблему, а один из способов применения данного функционала.
                    0
                    Спасибо, это интересно! Покопаю эту тему тоже
            0
            Не уверен, что хранить очки игрока в синглтоне — хорошая идея. При вылете или намеренном снятии активити, весь прогресс в трубу. Для этого есть плеерпрефс или сохранение в облако. Не нужно заставлять игрока чувствовать боль из-за багов вашей игры
              0
              Я не являюсь разработчиком игр, это для меня только fo fun. Я лишь привел примел где показал способ передачи состояния между сценами.
            +3
            Just use Shared scriptable object
              0
              Примеры использования? И как жить с тем, что он хранится как ассет, и если в редакторе запустить игру — получим измененный ассет?
              0

              Советую посмотреть на Dependency Injection и DI / IoC фреймворки вроде ZenJect (линк).
              Знаю, что для новичка это не простая концепция, но она имеет большое количество преимуществ перед синглтонами/статик классами/DontDestroyOnLoad.


              Данный подход позволит сделать единую точку входа в приложение, а для хранения данных пользователя вместо singleton объекта можно использовать сервис (который потом можно заинъектить в необходимые вам скрипты):


              public interface IGameDataService
              {
                  public int DifficultyLevel { get; set; }
              }
              
              public class EnemyInstaller : MonoInstaller
              {
                  public override void InstallBindings()
                  {
                      // IGameDataService будет автоматически инъекцировано в зависимые классы
                      // включая другие сцены, если использован Scene Container Parenting
                      Container.Bind<IGameDataService>().To<GameDataService>().AsSingleton();
                  }
              }
              
              public class EnemyFactory : IEnemyFactory
              {
                  private readonly IGameDataService _gameDataService;
              
                  // Зависимость будет добавлена в конструктор автоматически при резолвинге EnemyFactory
                  // Для инъекции в MonoBehavior, можно добавить специальный компонент ZenjectBinding и аттрибут [Inject]
                  public EnemyFactory(IGameDataService gameDataService)
                  {
                      _gameDataService = gameDataService;
                  } 
              
                  public void SpawnEnemy(string enemyName)
                  {
                       var level = _gameDataService.DifficultyLevel;
                       ...
                       var enemy = GameObject.Instantiate(...).GetComponent<IEnemy>();
                       enemy.SetLevel(level);
                  }
              }

              В идеале, GameObject'ы и MonoBehavior'ы — для объектов игрового мира. Для всего остального есть обычный C#.

                0
                О, спасибо! Нигде не встречала информацию про этот подход, а выглядит он очень круто. Можете ссылки посоветовать где почитать? Погуглить я конечно тоже погуглю
                  +1
                  читайте ридми на гитхабе зенджекта, там все подробно расписано, в том числе с примерами
                    0
                    А следующим шагом можно ознакомиться с ECS (Entity-Component-System) архитектурой.
                      0
                      Окей, спасибо, посмотрю
                  +1
                  Спасибо тебе, что Unity называешь не Unity3d

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое