Как Мы Переосмыслили Работу Со Сценами В Unity

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

Теперь расскажу о том, как мы писали плагин для Unity на основе постобработки проекта и генератора кода CodeDom.



Проблема

В Unity сцены загружаются через строковый идентификатор.

Он нестабилен, то есть его легко изменить без очевидных последствий.

Например, при переименовании сцены все пойдет не так, но это станет понятно только в самом конце на этапе исполнения.

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



Решение

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

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); } } } } }

В этой реализации видна еще одна особенность — передача параметров сценам.

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

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

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

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



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

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

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

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



Где скачать

Мы создали плагин вместе с Владиславом Койдо в рамках проверки концепции.

Он еще не стабилен и не тестировался должным образом, но с ним уже можно поиграть.

Репозиторий на GitHub — https://github.com/HolyMonkey/unity-typed-scenes Если вам интересно, в следующей статье я попрошу Владислава рассказать, как он работал с Code Dom в Unity и как работать с постобработкой на примере того, что мы сегодня обсуждали.

Теги: #Разработка игр #gamedev #C++ #unity #csharp #unity3d

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.