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

Поговорим о статических классах для хранения данных, различных способах подгрузки сцен движком 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);

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

Проблемы


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

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

Ссылки на документацию

Комментарии (21)


  1. FoterIS
    18.02.2019 14:31

    Не уверен, что 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);
        }    
    }
    
    


    1. FoterIS
      18.02.2019 14:42

      Даже в ссылках которые вы прикрпели (Static Class) используется Singleton


      1. apocatastas Автор
        18.02.2019 16:45

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


        1. indiega
          18.02.2019 17:58

          Если вы не претендуете, то вот это определенно лишнее

          Данная статья будет полезна начинающим разработчикам игр.
          )))
          Статик, мягко говоря, так себе решение. Представьте ситуацию, когда вам надо ресетнуться, полоностью перезапустить игру, а у вас в статике 100500+ полей. Будете ручками все это дефолтить? Статик для констант удобен. Хотя если аккуратно, то имеет право на жизнь, например в качестве контейнеров.


          1. AgentFire
            18.02.2019 18:30

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


            1. indiega
              18.02.2019 19:07

              Это ад перфекциониста )))


              1. AgentFire
                19.02.2019 01:08

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


          1. apocatastas Автор
            18.02.2019 19:03

            Я использовала здесь статик, потому что храню в нём ровно два поля :)
            Но да, полезная ремарка, если начинающий разработчик вдруг решит туда напихать гораздо больше.
            Попыталась намекнуть на это словами «простых игр» в названии, буду аккуратнее.


            1. indiega
              18.02.2019 19:13

              Вот, вот. А соблазн запихать туда всё, что можно и нельзя, появится в следущую секунду, после того, как человек узнает и поймет, что такое статик )))


        1. FoterIS
          18.02.2019 20:16

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


          1. apocatastas Автор
            18.02.2019 21:00

            А если его и не нужно уничтожать? Что значит «использовать в связке с другими объектами»?


            1. FoterIS
              18.02.2019 21:31

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


              1. apocatastas Автор
                19.02.2019 10:54

                Спасибо, это интересно! Покопаю эту тему тоже


    1. Bromesh
      18.02.2019 17:43

      Не уверен, что хранить очки игрока в синглтоне — хорошая идея. При вылете или намеренном снятии активити, весь прогресс в трубу. Для этого есть плеерпрефс или сохранение в облако. Не нужно заставлять игрока чувствовать боль из-за багов вашей игры


      1. FoterIS
        18.02.2019 20:11

        Я не являюсь разработчиком игр, это для меня только fo fun. Я лишь привел примел где показал способ передачи состояния между сценами.


  1. rumyancevpavel
    18.02.2019 17:28
    +2

    Just use Shared scriptable object


    1. Tutanhomon
      19.02.2019 17:05

      Примеры использования? И как жить с тем, что он хранится как ассет, и если в редакторе запустить игру — получим измененный ассет?


  1. splatt
    19.02.2019 07:40

    Советую посмотреть на 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#.


    1. apocatastas Автор
      19.02.2019 10:57

      О, спасибо! Нигде не встречала информацию про этот подход, а выглядит он очень круто. Можете ссылки посоветовать где почитать? Погуглить я конечно тоже погуглю


      1. Tutanhomon
        19.02.2019 17:04

        читайте ридми на гитхабе зенджекта, там все подробно расписано, в том числе с примерами


      1. AlexanderG
        21.02.2019 15:04

        А следующим шагом можно ознакомиться с ECS (Entity-Component-System) архитектурой.