Как мы переосмыслили работу со сценами в Unity

от автора

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

Сейчас я вам расскажу о том, как мы написали плагин для Unity на основе пост-процессинга проектов и кодогенератора CodeDom.

Проблема

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

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

Решение

При добавлении сцены в проект, генерируется одноимённый класс с методом Load.

Если мы добавим сцену Menu, то в проекте сгенерируется класс Menu и в дальнейшем мы можем запустить сцену следующим образом:

Menu.Load();

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

//------------------------------------------------------------------------------ // <auto-generated> //     This code was generated by a tool. //     Runtime Version:4.0.30319.42000 // //     Changes to this file may cause incorrect behavior and will be lost if //     the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------  namespace IJunior.TypedScenes {        public class Menu : TypedScene     {         private const string GUID = "a3ac3ba38209c7744b9e05301cbfa453";                  public static void Load()         {             LoadScene(GUID);         }     } } 

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

namespace IJunior.TypedScenes {     public abstract class TypedScene     {         protected static void LoadScene(string guid)         {             var path = AssetDatabase.GUIDToAssetPath(guid);             SceneManager.LoadScene(path);         }          protected static void LoadScene<T>(string guid, T argument)         {             var path = AssetDatabase.GUIDToAssetPath(guid);              UnityAction<Scene, Scene> handler = null;             handler = (from, to) =>             {                 if (to.name == Path.GetFileNameWithoutExtension(path))                 {                     SceneManager.activeSceneChanged -= handler;                     HandleSceneLoaders(argument);                 }             };              SceneManager.activeSceneChanged += handler;             SceneManager.LoadScene(path);         }          private static void HandleSceneLoaders<T>(T loadingModel)         {             foreach (var rootObjects in SceneManager.GetActiveScene().GetRootGameObjects())             {                 foreach (var handler in rootObjects.GetComponentsInChildren<ISceneLoadHandler<T>>())                 {                     handler.OnSceneLoaded(loadingModel);                 }             }         }     } }

В этой реализации видна ещё одна фишка — передача параметров сценам.

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

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

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

В таком случае мы можем сами создать такой компонент.

using IJunior.TypedScenes; using System.Collections.Generic; using UnityEngine;  public class GameLoadHandler : MonoBehaviour, ISceneLoadHandler<IEnumerable<Player>> {     public void OnSceneLoaded(IEnumerable<Player> players)     {         foreach (var player in players)         {             //make avatars         }     } }

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

//------------------------------------------------------------------------------ // <auto-generated> //     This code was generated by a tool. //     Runtime Version:4.0.30319.42000 // //     Changes to this file may cause incorrect behavior and will be lost if //     the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------  namespace IJunior.TypedScenes {     public class Game : TypedScene     {         private const string GUID = "976661b7057d74e41abb6eb799024ada";                  public static void Load(System.Collections.Generic.IEnumerable<Player> argument)         {             LoadScene(GUID, argument);         }     } }

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

Это не фишка, а скорее недоработка, так как такой функционал быстрее создаст путаницу, нежели будет полезен.

А почему не сделать через N?

Первую версию плагина я осветил на своём YouTube канале в этом видео.

Там задали ряд вопросов и предложили альтернативные решения. Есть интересные подходы, которые имеют право на жизнь, а есть откровенно идиотские, которые я хочу разобрать.

Чем плох статический класс с полями, через которые передаются данные для сцены?

Нередко встречаю и такое. Речь идёт о классе по типу этого:

public class GameArguments {     public IEnumerable<Player> Players { get; set; } }

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

Основная проблема в том, что нет формализации: при загрузки сцены не совсем ясно, что нам нужно предоставить для корректной работы. Получателей в целом определить будет проще, так как мы можем воспользоваться поиском по ссылке.

Ну и опять же сцену придётся запускать по ID или имени.

Чем плох PlayerPerfs

Предлагали и такой экзотический вариант. Можно было бы начать с того, что PlayerPrefs вообще не предназначен для передачи значений внутри одного инстанса. Этим можно было бы и закончить, но продолжим критику тем, что вам также придётся работать с неформальными строковыми идентификаторами параметров.

Параллель с ASPNet

Мне хотелось получить что-то схожее с строго типизированными View из ASPNet Core. Мы считаем плохим тоном использовать ViewData и стараемся определять ViewModel. В Unity хочется что-то такого же толка с теми же преимуществами.

Отличие Unity в первую очередь в том, что сцена — это обычно более громоздкое предприятие, нежели View в ASPNet. Это решается разбивкой одной сцены на несколько подсцен с режимом загрузки Additive (наш плагин, к слову, его поддерживает), что позволяет скомпоновать сцену из сцен поменьше со своими более атомарными моделями.

Но такой подход не очень распространён, к сожалению, и на это, я думаю, есть свои причины.

Где скачать

Плагин мы сделали в паре с Владиславом Койдо в рамках Proof-of-concept. Он ещё не стабилен и не обкатан как следует, но с ним уже можно поиграться.

Репозиторий на GitHub — https://github.com/HolyMonkey/unity-typed-scenes

Если вам интересно, я попрошу Владислава в следующей статье рассказать, как он работал с Code Dom в Unity и как работать с пост-процессингом на примере того, что мы сегодня обсуждали.

ссылка на оригинал статьи https://habr.com/ru/post/523860/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *