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
-
Орр, Джон Бойд
19 Oct, 24 -
Podthings, Выпуск 125
19 Oct, 24 -
«Налог На Бланки» Могут Отменить
19 Oct, 24