Как стать автором
Обновить

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

Крутой обзор.

Под какую платформу делаете игры? Делаете в рамках команды или единолично?

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

Да неужели? Уже ведь была обсосана тема на хабре про синглтоны и их время жизни. Пример в этой статье — плохой, отсутствует механизм проверки существования инстанса для учета, например, в OnDestroy и тп деструктивных эвентах. Более «прямой» вариант можно посмотреть тут: можно узнать, создан ли синглтон, можно сделать обработку Awake-а для одного инстанса (а не для всех попыток создания копию), по умолчанию это локальный для сцены синглтон (умрет при смене сцены корректно), если требуется глобальный — можно в перегрузке OnConstruct() добавить DontDestroyOnLoad (полезно для всяких глобальных провайдеров данных и сетевых решений).
С чем вы хотите поспорить? С тем, что для передачи данных между сценами лучше использовать не синглтоны или что синглтоны надо использовать только в крайнем случае?
Поспорить? Я вроде как процитировал то, к чему писал комментарий, где там было про передачу данных между сценами? И да, в этом синглтон без привязки к данным сцены тоже неплох (например, хранение настроек клиента игрока, его прогресса, для централизованного сохранения и использования в качестве контейнера для данных, которые требуются во всех сценах), ибо возможна реакция на события жизненного цикла MonoBehaviour, например, можно поймать момент загрузки сцены. Вот повсеместное и бездумное использование статик-классов считаю вредным, за редким исключением — синглтоны довольно просто перепиливаются под отдельный инстанс, а вот статику так просто не переделать.
Хранение настроек клиента это не передача данных между сценами. Это две разные функции. Про статик согласен.
namespace Client.Common {
    sealed class PlayerManager : UnitySingleton<PlayerManager> {
        public PlayerSettings Settings { get; private set; }

        public PlayerProgress Progress { get; private set; }

        public PlayerSession Session { get; private set; }

        protected override void OnConstruct () {
            base.OnConstruct ();
            DontDestroyOnLoad (gameObject);

            Session = new PlayerSession ();

            LoadSettings ();
            SaveSettings ();

            LoadProgress ();
            SaveProgress ();
        }

        void LoadSettings () {
            // грузим Settings из PlayerPrefs или из Persistent-хранилища
        }

        public void SaveSettings () {
            // сохраняем Settings в PlayerPrefs или в Persistent-хранилище
        }

        public void LoadProgress () {
            // грузим локальный прогресс
        }

        public void SaveProgress () {
            // сохраняем локальный прогресс
        }
    }
}

    sealed class PlayerSettings {
        [JsonName ("o1")]
        public float SoundVolume = 1f;

        [JsonName ("o2")]
        public float MusicVolume = 1f;
    }

// по аналогии - другие классы-холдеры данных, с Json-сериализацией.

Смысл в том, что не нужно ничего никуда вешать — просто потрогать инстанс этого провайдера данных и он будет сам следить за собой + предоставлять централизованное апи для работы с кросс-сценовыми данными. PlayerSettings — настройки пользователя (звук и прочее), PlayerProgress — если есть локальная кампания в синглплеере, то данные можно хранить и предоставлять так, PlayerSession — данные текущей игровой сессии, должны сбрасываться при смерти игрока и начале новой сессии. Как-то так. Т.е. основной смысл — хранить данные, по которым уже можно выполнять генерацию контента в каждой отдельно взятой сцене. Можно сделать и на pure-C# классах и как-то кидать линки, но если нужна реакция на события жизненного цикла MonoBehaviour, то это самый простой способ.
Ммм, 2016год синглтоны в «советах и рекомендациях». Сами по себе синглтоны как паттерн времени жизни объекта не так уж и плохи, но советовать делать глобальные объекты с состоянием (XXXManager, XXXController etc) это уже вредительство.

Это усугубляется еще тем, что в юнити это не только глобальное состояние, но еще поток исполнения (через Update). Остается только навешать на его событий и пожелать удачи тому, кто будет это поддерживать.
Исключая синглтоны, вполне дельные советы, не считая пары тех, сути которых я вообще не понял.
Ну я и не писал что все советы плохие. Часть странных (вроде тех что про массивы), остальные вполне себе нормальные советы с обоснованием. Но про «синглтон» очень плохой совет, особенно с мотивацией «удобно же».

Как пример: писать в штаны удобно, и снимать не надо, и в начале тепло. Но в долгосрочной перспективе этот совет не очень.
А какие альтернативы? Я в последнем проекте повсеместно использовал синглтоны для менеджеров, и их вышло довольно много. Обращения к менеджерам шли через одну точку — главный ГеймМенеджер. Много раз слышал о том, что это не лучшая практика, но более простого метода пока не нащупал
Альтернативы IoC, ServiceLocator, либо просто своя иерархия менеджеров. Глобальный доступ это зло, которое рано или поздно портит тестируемость и расширяемость кода.
Если к объекту можно получить доступ из любого места, то это глобальный объект. Если такой объект может хранить состояние, то это глобальный объект с состоянием. На сцене он, в TLS или в static поле, не имеет значения.

Представьте что вам надо параллельно запустить 2 игры в том же контексте. Если бы вы не использовали глобальное состояние, то это было бы просто:
game1 = new Game()
game2 = new Game()
В вашем случае надо создать изолированный контекст, это AppDomain или Process. Спросите, зачем создавать 2 игры парралельно? Это исскуственный пример! Скажу сразу, что нет, вполне реальный (дешевый split-screen через уже готовый сетевой код игры).
Вот более частая задача — Перезапустить игру.
Как и в пером примере, если нет глобального состояния, мне надо просто создать новый экземпляр Game и радоваться жизни. В вашем случае надо найти все объекты с глобальным состоянием и почистить их состояние. Чувствуете как высокая связанность мешает вам решать обычные задачи?

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

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

Представьте что вам надо параллельно запустить 2 игры в том же контексте. (дешевый split-screen через уже готовый сетевой код игры).

Так скорее такой «дешевый» вариант попахивает — каждый игрок должен иметь состояние, обслуживаемое менеджером-синглтоном, а не быть самим менеджером-синглтоном. Те игрок не должен представлять собой «игру» как процесс, а должен быть представлен сущностью в одной игре. Если требуется полная изоляция, то это редкий кейс, под который все-равно нужно думать архитектуру.

А уж о тестировании, и параллельном тестировании, когда есть зависимости от сцены и глобалов, можете забыть.

Тут частично соглашусь, но не каждый менеджер должен быть глобальным, тестирование глобальных — это по сути тестирование изолированной системы, которая мало завязана на какую-то сцену, те мы по сути тестируем какое-то глобальное апи — это можно протестировать. Локальные менеджеры не должны иметь завязки на менеджеры из других сцен, это тоже все тестируемо (+ автопроверки в редакторе, которые дропаются в билде и не убивают производительность).
>Нельзя получить, посмотрите пример по ссылке
я специально указал еще TLS, т.к. это не «глобально» на весь процесс, а глобально на поток, но это всё равно глобальное состояние, как и сцена. То что из любого метода можно получить доступ к сцене и есть глобальный доступ. Как ваш код будет вести себя без сцены? Сломается? Как будет себя вести код расчитывающий на TLS, если его запустить в через async/await? Сломается. Раньше работало, сегодня сломалось.

>полная изоляция, то это редкий кейс
т.е. рестарт игры это редкий кейс? Ре-логин, перенос сохранений, ну и в крайнем случае непоправимый сбой. И это только такой сущности как «игра», другие предоставляют свои бенефиты от отсутствия связанности.
То что из любого метода можно получить доступ к сцене и есть глобальный доступ. Как ваш код будет вести себя без сцены?

Нельзя получить, посмотрите пример по ссылке.

Добавить нечего. Нельзя получить из любого метода из другой сцены, откуда доступ запрещен, защита реализована в самом синглтоне. Неужели так сложно сходить посмотреть?
Как будет себя вести код расчитывающий на TLS, если его запустить в через async/await?

Как и любой код, основанный на MonoBehaviour и вызовах апи из юнити — скажет, что щупать его можно только в основном потоке.
Раньше работало, сегодня сломалось.

Покажите код по щупанию MonoBehaviour из асинка, который работал и покажите, что поломается, иначе разговор ни о чем.
рестарт игры это редкий кейс?

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

Релогин на сплит-скрине? Сомневаюсь. Если говорим про сетевое решение, то тоже зависит от геймплея, обычно сетевой менеджер является глобальной сущностью и доступен по всем сценам. Восстановление сессии зависит опять же от геймплея — обычно это геймовер (с редиректом на сцену логина), либо восстановление данных по сети (что выглядит как сброс локальных данных менеджеров и подсасывание их из сервера / других игроков).
это только такой сущности как «игра», другие предоставляют свои бенефиты от отсутствия связанности.

Не вижу бенефитов, выглядит как подкостыливание чего-то нового и незапланированного на базе существующего — GDD не зря пишут и согласовывают в несколько этапов.
>Нельзя получить из любого метода из другой сцены
Но можно из любого метода этой сцены. Эдакий «контекст» в виде сцены. т.е. в метод этот объект не передается, но воспользоваться им можно. Замечательно, скрытая зависимость. Хотел бы я посмотреть хоть один положительный отзыв о скрытых зависимостях. Ну пахнет же, а вы говорите «фича».

>Покажите код по щупанию MonoBehaviour из асинка, который работал и покажите, что поломается, иначе разговор ни о чем.
https://www.assetstore.unity3d.com/en/#!/content/17276 Из асинка щупается вся юнити. Удобно же. Если вы про async/await, то никто не запрещает навешать на основной юнитевский поток SyncronizationContext и async/await заработает как надо!
>Не нужно подменять понятия, ок? Разговор был про конкретный кейс — запуск сплит-скрина.
Нет, вы просмотрели что я дал 2 примера. Один сплит-скрин и параллельные игры, и второй с рестартом и последовательные. Второй вы проигнорировали. Удобство такой реализации сплит-скрина я не буду обсуждать, это пример.
>GDD не зря пишут и согласовывают в несколько этапов
Вот это вообще из раздела фантастики. Нет, GDD только на клон тетриса не будут переписывать, как и любой дизайн документ он доделывается под ветра индустрии.
Но можно из любого метода этой сцены. Эдакий «контекст» в виде сцены.

Ну а менеджер (не синглтон) / контроллер любой сущности при таком определении тоже может быть получен в любом месте сцены. Если это настолько критично и требуется такой уровень изоляции с дальнейшими потерями на согласование / хранение линков на другие контроллеры, то почему бы и нет? Не бывает серебряной пули, нужно выбирать решение под задачу.
Если вы про async/await, то никто не запрещает навешать на основной юнитевский поток SyncronizationContext и async/await заработает как надо!

Ну тогда ничего и не поломается, как было написано выше :)
Второй вы проигнорировали.

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

Почему же, делается прототип, его обкатывают, выкидывают ненужные фичи, подкидывают новые — но это не конечная игра / механика, можно реализовывать как угодно. Когда оно относительно устаканиться по основным фичам (а сеть / сплит-скрин я считаю одной из основных фич, влияющих на архитектуру) — уже пилится как нужно. Да, доработки / изменения возможны, но не по основным фичам. Такое — только в мажорных релизах.
Перечитал еще раз. No offense, возможно мне не нравится такой уровень абстракции как Game из примера выше потому, что мне ближе идея data-driven-programming, те идея относительно изолированных подсистем, знающих только о своей подзадаче и процессирующих входящие данные.
2. Делай каждую сцену запускаемой: Не всегда возможно, к этому стоит стремиться, но в реальности проще сделать объект Persistent запихнуть его в префаб а потом скидывать на все сцены, а сам объект инициализировать в первой сцене с логотипом.
5. Обновляйся одновременно с командой: Выполняйте обновление юнити со страхом, а лучше прочитайте что это вам даст, оцените стоит ли это того, может и текущая версия неплоха. Обязательно читайте release notes и исправления на странице выпуски патчей. Возможно та неуловимая проблема на каком-нибудь редком устройстве которое вы уже пару месяцев не можете поймать, уже исправлена в патче для вашей версии? Но и обновляйтесь со страхом, получить ещё много новых занимательных неуловимых проблем, которые будут исправлены следующим патчем.
6. Импортируй ассеты в чистый проект: Хорошая практика, плюс заведите папку addons или extensions и все-все-все сторонние дополнения старайтесь сохранить в ней. Это уменьшит путаницу в иерархии папок.
9. Используй пространство имён с умом: Пространство имён, штука классная, но и с ними бывают коллизии, избегайте имён типа Utils каждое второе дополнение имеет его, лучше начинать пространство имён с, например, названия компании, WeslomPo.Utils — имеет меньше шансов столкнуться с другим пространством (так принято называть классы в других языках программирования, например Java или AS3, только ещё более жёстко: ru.weslompo.projectname.etc). Папки в должны следовать пространству имён. Т.е. файл лежащий в корне «Scripts» не имеет пространства имён, а файл «Scripts\Puzzle\Pieces\PieceOfCake.cs» должен иметь пространство имён «Puzzle.Pieces».
25. Инспектируемые поля только private + [SerializeField]: Я бы был более настойчивым в утверждении что следует использовать только private поля + [SerializeField] для всего что можно, кроме того что нужно вызывать снаружи класса. Не смотря на то что юнитеки этот метод не пропагандируют (я думаю им просто лень писать это в примерах). Это делает классы чуть более защищенным от нежелательных изменений, и помогает меньше заглядывать внутрь кода чтобы определить что можно с ним делать а что нельзя. Плюс это просто хорошая практика (good practice).
29. Запечатывай MonoBehaviour: Стоит дополнить, что sealed классы работают чуть быстрее чем не sealed, особенно когда проект компилируется в il2cpp, недавно была статья по этой теме.
43. Используй префабы для всего: Стандартные префабы в юнити это пи… ужас-ужас. Постоянно с ними какие-нибудь гадости творятся (например нет поддержки вложенности, встроили префаб в префаб — потерял ссылку, обещают исправить… но пока ещё нету стабильной версии с этой фичей). Поэтому используйте префабы на свой страх и риск, и лучшее только для объектов которые используются ОДИН раз, тогда вы избежите нежелательного поведения.
46-50. Советы по работе со ScriptableObject: «скриптуемые объекты» — я думаю это, как переводчик любит выражаться, «более сильно непонятно» чем ScriptableObject, особенно для тех кто никогда не трогал его ни разу в жизни, и даже не может понять что гуглить чтобы пояснить этот момент.
Без нумерации: Структура папок, имхо излишне усложнённая, на первом уровне иерархии нужно иметь как можно меньше папок, и всё что является assets (Props, Prefabs, Texures, Meshes, Materials etc) на самом деле лучше засунуть куда-нибудь ниже, например, в папку «Data\», и там уже делать разветвлённую иерархию (DarkVampire, LightVampire, Plants etc.). Близкая к идеалу структура:
image
(Addons, Data, Editor, Plugins, Resources, Scenes, Scripts).
Но и тут заметно что Soomla вылезла в коернь, потому что пути у неё захардкожены (их исправление приводит к неопределённому поведению).
Еще стоит добавить обязательную установку расширения RainbowFolders. Прям вот начал проект, первым делом установил. Сидит в редакторе, в билд не попадает, а наглядность папок возрастает.
Структура сцены излишне оптимистичная, ибо большая часть экстеншенов любит «срать» на сцену своими компонентами, либо хотят быть самыми близкими к корню (из-за метода DontDestroyOnLoad). Возможно меня закидают палками, но я обычно выпиливаю этот метод из всех нужных мне компонентов-дополнений. Просто завёл один объект Persistent который в единственном экземпляре использует метод DontDestroyOnLoad и самоудаляется если находит ещё один такой компонент на сцене. А все компоненты запихиваю в него ниже по иерархии с группировкой по типа (Advertise, Shop, Analytics etc.). Так получается чище сцена. А то посмотришь на неё как-нибудь, и ужаснёшься какой там кошмар устроили сторонние разработчики своими объектами без спросу.

заведите папку addons или extensions и все-все-все сторонние дополнения старайтесь сохранить в ней.

К сожалению это часто невозможно, т.к. в плагинах прошиты абсолютные пути от Assets или используется папка Plugins/Android или Plugins/iOS — юнити смотрит платформозависимые вещи только в корневой папке.
Я написал об этом. Обычно после обновления нахожу переменную ответственную за путь и меняю на соответствующий (это легко правится в google ads например). Но Soomla, как видно, грешит тем что исправление этой переменной не помогает, и вызывает неопределённое поведение иногда, потому лежит в корне. В общем, рекомендация, скорее звучит держать папку Assets как можно чище.
Обычно после обновления нахожу переменную ответственную за путь и меняю на соответствующий

Это адская боль при апдейте ассетов, особенно если их больше 3-4.

вызывает неопределённое поведение иногда

я написал, почему так и это не фиксится, ибо специфика работы текущей версии юнити. Оно заработает как только юнитеки будут брать платформозависимые папки Android/iOS из любой вложенности иерархии проекта (как сейчас делается для Editor) — на данный момент все постбилд-скрипты должны лежать в /Plugins/<платформа> папке.
Никто и не говорит что это просто и правильно — это плохо и аморально. Но один два раза сделать можно во имя благой цели. Как в лифте, первый этаж нажимают чаще всего как и поиск через первый уровень дерева в иерархии.
А неопределённое поведение, конкретно в этом случае, потому что конструируется пути внутри скрипта из различных констант. При этом частенько вместо того чтобы использовать уже объявленную константу, скрипт конструирует путь из двух других констант не связанных между собой. В общем это сложно отследить, и не стоит на это время тратить. Папка Plugins — в этом конкретном случае не причем (учитывая что её путь поменять нельзя). Вообще папка Plugins и как её содержать в чистоте и уюте — отдельный разговор.
Существует и иной подход: создать папку для проекта внутри Assets и работать в ней.
Стоит дополнить, что весь код который не изменяется и который не ссылается на игровые классы, лучше перенести в Plugins, так он попадёт в другую DLL (Assembly-Firstpass.dll) и будет компилироваться намного реже. Это ускорит компиляцию при изменении кода в проекте. В некоторых случаях в разы.
IEnumerator RunInParallel()
{
   yield return StartCoroutine(Coroutine1());
   yield return StartCoroutine(Coroutine2());
}

public void RunInSequence()
{
   StartCoroutine(Coroutine1());
   StartCoroutine(Coroutine1());
}

Мне кажется, или всё наоброт?
А мне кажется, вообще нет никакой разницы.
Пока из первой корутины не будет вызван yield, вторая не запустится. А когда yield вызовется, управление перейдет ко второй корутине в обоих вариантах. И где здесь параллельное выполнение?
да, не совсем одновременно. Но во втором случае корутина не будет ждать окончания работы первой, а запустится почти одновременно (ровно настолько НЕ одновременно, насколько виртуальная машина переходит от вызова первого метода ко второму)
Если первая корутина будет содержать хотя бы один yield return, то вторая корутина
   StartCoroutine(Coroutine1());
   StartCoroutine(Coroutine2());

в этом случае начнёт выполнение до окончания выполнения первой,
   yield return StartCoroutine(Coroutine1());
   yield return StartCoroutine(Coroutine2());

а в этом случае вторая корутина начнёт выполнение только после полного завершения первой корутины.
Иначе говоря, если обе корутины будут выглядеть так:
IEnumerator Coroutine()
{
   Debug.Log("1");
   yield return;
   Debug.Log("2");
}

то в первом случае вывод будет
1122, а во втором 1212.
Да, вы правы. У переводчика сейчас неверно код скопирован. В оригинале как раз так:
IEnumerator RunInSequence()
{
   yield return StartCoroutine(Coroutine1());
   yield return StartCoroutine(Coroutine2());
}

public void RunInParallel()
{
   StartCoroutine(Coroutine1());
   StartCoroutine(Coroutine1());
}

Спасибо. Статья хорошая, но реализация шаблонного синглтона, которая тут описана — плохая. Если я не ошибаюсь, она мне не запрещает иметь два синглтона в одной сцене (первый найденный файндом и будет прав) Тут должна быть ещё проверка на то, что если объект существует, то его нельзя прицепить. Хотя я бы вообще не советовал делать монобехи синглтонами.
Тут должна быть ещё проверка на то, что если объект существует, то его нельзя прицепить.

Зачем, если все остальные инстансы просто сами по себе умрут при проверке, являются ли они настоящим синглтоном.
Хотя я бы вообще не советовал делать монобехи синглтонами.

Если нужна реакция на события жизненного цикла юнити — без этого не обойтись.
Зачем, если все остальные инстансы просто сами по себе умрут при проверке, являются ли они настоящим синглтоном.

В плане умрут в той реализации, которая в статье? Если в сцене по какой-то причине будет два синглтона, то как определить какой из них правильный? И какой из них удалится? Просто реализацию из статьи я видел в майкрософтовской библиотеке Holotoolkit (если его не обновили) и ничего там не удаляется, просто второй инстанс бесцельно висит, так как он никогда не вызывается.
Если нужна реакция на события жизненного цикла юнити — без этого не обойтись.

Может быть в некоторых случаях соглашусь, хотя у меня получалось выстраивать архитектуру так, чтобы синглтоны по сути вызывались в монобехах, но сами монобехами не являлись. Просто по сути получалось вроде того, что синглтоны у меня выполняли роль «модели» (правильнее сказать они управляли моделью), а монобехи — это «отображение+контроллер», и в них синглтоны как-то не требовались.
В плане умрут в той реализации, которая в статье?

Нет, я вставил линк в текст, думал, будет заметно :)
Может быть в некоторых случаях соглашусь, хотя у меня получалось выстраивать архитектуру так, чтобы синглтоны по сути вызывались в монобехах, но сами монобехами не являлись.

Те делать синглтон pure классом, из которого плодить GO + MonoBehaviour с мониторингом событий и пробросом их обратно — тоже вариант, но что-то много телодвижений.
20. Создайте и поддерживайте свой собственный класс времени, чтобы сделать работу с паузами удобнее.

А бороться с рассинхронизацией как? Ведь эти данные вы будите обновлять в апдейте, и часть кода может получить старые Time, а часть новые за один фрейм.

В Unity есть такая настройка как Script Execution Order. Если добавить туда класс времени и поставить отрицательное значение, то он будет выполняться раньше всех остальных классов. Это касается и Awake, и Start, и Update, и всех остальных юнитёвых встроенных методов.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории