[Разработка игр, C#, Unity] Управление сценами в Unity без боли и страданий
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Вам когда-либо приходилось думать над тем, как сделать управление сценами в вашем проекте менее болезненным? Когда у вас достаточно простая игра, в которой всего несколько сцен идущих одна за другой, то, зачастую, всё проходит гладко. Но когда количество сцен растёт и усложняются переходы между ними — они могу загружаться в разном порядке и поведение некоторых из них должно зависеть от входящих параметров — задача становится менее тривиальной.
Ниже несколько подходов к ее решению, которые мне приходилось видеть чаще всего:
- Файлы — при переходе из одной сцены в другую, все необходимые данные записываются в JSON/XML файл, а когда следующая сцена загрузилась, считывают их обратно. Как минимум, это медленно (говоря о чтении и записи в файл), да и процесс дебага становится менее удобным.
- Огромный статический класс, который обрабатывает все возможные переходы между сценами. Они очень похожи на божественные объекты и довольно часто являются причиной утечек памяти, а также боли в нижней части спины, когда новый разработчик пытается понять, что вообще происходит в этой тысяче строк статического кода.
- DontDestroyOnLoad GameObject — этот подход похож на предыдущий, но представлен GameObject'а в сцене с кучей ссылок в Инспекторе. По сути, это один из тех синглтонов, которые каждый из нас видел в большинстве проектов...
Я хочу показать вам подход, который использую уже не один год. Он помогает сделать переходы более прозрачными для разработчика, становится проще разобраться где и что происходит, а также дебажить.
В каждой сцене у меня есть SceneController. Он отвечает за проброс всех необходимых ссылок и инициализацию ключевых объектов. В некотором смысле, его можно считать точкой входа сцены. Для представления аргументов я использую класс SceneArgs и у каждой сцены есть свой класс, представляющий ее аргументы и являющийся наследником SceneArgs.
public abstract class SceneArgs
{
public bool IsNull { get; private set; }
}
Как я уже написал выше, у каждой сцены есть свой контроллер, который наследуется от SceneController.
public abstract class SceneController<TController, TArgs> : MonoBehaviour
where TController : SceneController<TController, TArgs>
where TArgs : SceneArgs, new()
{
protected TArgs Args { get; private set; }
private void Awake()
{
Args = SceneManager.GetArgs<Tcontroller, TArgs>();
OnAwake();
}
protected virtual void OnAwake() {}
}
Я использую отдельный класс для представления аргументов по одной простой причине. Изначально, метод загрузки сцены принимал аргументы в виде массива объектов params object[] args. Это был унифицированный способ для загрузки любой сцены с возможностью передать аргументы. Когда контроллер сцены получал управление, он парсил этот массив объектов и получал все необходимые данные. Но, кроме банального боксинга, здесь была ещё одна проблема — ни для кого, кроме разработчика, который писал этот контроллер (а со временем и для него самого) не было очевидно, какого типа аргументы и в каком порядке нужно передать, чтобы потом не возникло ошибок кастинга. Когда мы создаём новый метод, то в его сигнатуре указываем порядок и типы аргументов и затем IDE может нам подсказать, если при его вызове мы допустили ошибку. Но с аргументом params object[] args мы видим лишь то, что нужно передать массив аргументов и каждый раз, чтобы понять их порядок и типы, разработчику нужно лезть в код контроллера и смотреть как же они парсятся. Мне хотелось сохранить метод запуска таким же унифицированным (один метод для запуска любой сцены), но при этом дать возможность жестко ограничить типы аргументов для каждой из сцен. И для этого нужны ограничения where, которые есть в SceneController.
Как известно, мы должны каждый раз передавать name или buildIndex сцены, чтобы загрузить её через метод LoadScene() или LoadSceneAsync() в Unity API. Этого хотелось бы избежать, потому я использую кастомный атрибут SceneControllerAttribute, чтобы привязать конкретный контроллере к конкретной сцене. Здесь используется имя сцены, а не её buildIndex лишь по той причине, что, на моём опыте, оно реже подвергается изменениям.
[AttributeUsage(AttributeTargets.Class)]
public sealed class SceneControllerAttribute : Attribute
{
public string SceneName { get; private set; }
public SceneControllerAttribute(string name)
{
SceneName = name;
}
}
Допустим, у нас есть сцена MainMenu. В таком случае, классы её аргументов и контроллера будут иметь следующий вид:
public sealed class MainMenuArgs : SceneArgs
{
// args' properties
}
[SceneControllerAttribute]
public sealed class MainMenuController : SceneController<MainMenuController, MainMenuArgs>
{
protected override void OnAwake()
{
// scene initialization
}
}
Собственно, это всё (из того, что касается контроллера сцены и её аргументов). Осталось лишь понять, как происходит переход от одной сцены к другой. Этим занимается внезапно статический класс SceneManager. Очень важно, чтобы он был как можно меньше, проще и понятнее. Чтобы он не превратился в один из тех ненавистных божественных объектов с тоннами зависимостей. У него всего лишь одна простая задача — передать управление от контроллера одной сцены к контроллеру следующей. За все последующие инициализации и прочее отвечает уже сам контроллер.
public static class SceneManager
{
private static readonly Dictionary<Type, SceneArgs> args;
static SceneManager()
{
args = new Dictionary<Type, SceneArgs>();
}
private static T GetAttribute<T>(Type type) where T : Attribute
{
object[] attributes = type.GetCustomAttributes(true);
foreach (object attribute in attributes)
if (attribute is T targetAttribute)
return targetAttribute;
return null;
}
public static AsyncOperation OpenSceneWithArgs<TController, TArgs>(TArgs sceneArgs)
where TController : SceneController<TController, TArgs>
where TArgs : SceneArgs, new()
{
Type type = typeof(TController);
SceneControllerAttribute attribute = GetAttribute<SceneControllerAttribute>(type);
if (attribute == null)
throw new NullReferenceException($"You're trying to load scene controller without {nameof(SceneControllerAttribute)}");
string sceneName = attribute.SceneName;
if (sceneArgs == null)
args.Add(type, new TArgs { IsNull = true });
return UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName);
}
public static TArgs GetArgs<TController, TArgs>()
where TController : SceneController<TController, TArgs>
where TArgs : SceneArgs, new()
{
Type type = typeof(TController);
if (!args.ContainsKey(type) || args[type] == null)
return new TArgs { IsNull = true };
TArgs sceneArgs = (TArgs)args[type];
args.Remove(type);
return sceneArgs;
}
}
Позвольте мне немного объяснить этот код. При вызове OpenSceneWithArgs() вы передаёте тип контроллера (TController) сцены, которую нужно загрузить, тип параметров (TArgs) и, собственно, сами параметры (sceneArgs). В первую очередь, SceneManager проверяет, есть ли у TController атрибут SceneControllerAttribute. Он должен быть, потому именно он определяет, к какой сцен привязан контроллер TController. Дальше мы просто добавляем аргументы sceneArgs в словарь. Если не было передано каких-либо аргументов, мы создаём экземпляр типа TArgs и присваиваем его свойству IsNull значение true. Если всё прошло гладко, то будет вызван метод из Unity API LoadSceneAsynс() и ему будет передано имя сцены, которое берётся из атрибута SceneControllerAttribute.
Загружается следующая сцена и у её контроллера вызывается метод Awake(). Дальше, как видим в SceneController, TController вызывает SceneManager.GetArgs(), чтобы получить аргументы, которые были переданы и записаны в словарь, а затем производит все необходимые инициализации.
В результате у нас каждая сцена сама отвечает сама за себя, а переходы происходят через небольшой класс SceneManager, отвечающий исключительно за передачу управления между ними. Просто попробуйте применить этот подход в одном из ваших пет проектов и вы заметите, на сколько прозрачнее и понятнее для вас и всех кто работает с вами станут переходы между сценами и их инициализации. Буду рад вашим комментариям. Успехов в разработке!
===========
Источник:
habr.com
===========
Похожие новости:
- [Ненормальное программирование, .NET, C#] Вызываем конструктор базового типа в произвольном месте
- [Работа с 3D-графикой, Разработка игр, CGI (графика), Игры и игровые приставки] Освещение в VFX и видеоиграх: сравнение подходов к рендерингу (перевод)
- [Разработка веб-сайтов, Разработка игр, Разработка мобильных приложений, Разработка под Linux, Разработка под Windows] Свободная веб-энциклопедия для любых IT-проектов на собственном движке
- [.NET, C#] Интеграция с «Госуслугами». Место СМЭВ в общей картине (часть I)
- [Разработка игр, Учебный процесс в IT, Мозг, Изучение языков] [Фреимворк формирования полезных привычек] и максимального вовлечения юзеров на примере изучения английского языка
- [.NET, C#, Математика] Тензоры для C#. И матрицы, и векторы, и кастомный тип, и сравнительно быстро
- [Игры и игровые приставки, Киберспорт, Разработка игр] Чего не хватает современным шутерам?
- [Unity, Игры и игровые приставки, Разработка игр, Разработка мобильных приложений] Сказ о разработке амбициозного проекта 16-ти летним парнем (file547)
- [.NET] Как я понимаю асинхронный код?
- [Разработка игр, Управление продуктом, Учебный процесс в IT] Дивный новый мир: вручение дипломов в Аллодах Онлайн
Теги для поиска: #_razrabotka_igr (Разработка игр), #_c#, #_unity, #_unity, #_unity3d, #_c#, #_razrabotka_igr (разработка игр), #_razrabotka_igr (
Разработка игр
), #_c#, #_unity
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:49
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Вам когда-либо приходилось думать над тем, как сделать управление сценами в вашем проекте менее болезненным? Когда у вас достаточно простая игра, в которой всего несколько сцен идущих одна за другой, то, зачастую, всё проходит гладко. Но когда количество сцен растёт и усложняются переходы между ними — они могу загружаться в разном порядке и поведение некоторых из них должно зависеть от входящих параметров — задача становится менее тривиальной. Ниже несколько подходов к ее решению, которые мне приходилось видеть чаще всего:
Я хочу показать вам подход, который использую уже не один год. Он помогает сделать переходы более прозрачными для разработчика, становится проще разобраться где и что происходит, а также дебажить. В каждой сцене у меня есть SceneController. Он отвечает за проброс всех необходимых ссылок и инициализацию ключевых объектов. В некотором смысле, его можно считать точкой входа сцены. Для представления аргументов я использую класс SceneArgs и у каждой сцены есть свой класс, представляющий ее аргументы и являющийся наследником SceneArgs. public abstract class SceneArgs
{ public bool IsNull { get; private set; } } Как я уже написал выше, у каждой сцены есть свой контроллер, который наследуется от SceneController. public abstract class SceneController<TController, TArgs> : MonoBehaviour
where TController : SceneController<TController, TArgs> where TArgs : SceneArgs, new() { protected TArgs Args { get; private set; } private void Awake() { Args = SceneManager.GetArgs<Tcontroller, TArgs>(); OnAwake(); } protected virtual void OnAwake() {} } Я использую отдельный класс для представления аргументов по одной простой причине. Изначально, метод загрузки сцены принимал аргументы в виде массива объектов params object[] args. Это был унифицированный способ для загрузки любой сцены с возможностью передать аргументы. Когда контроллер сцены получал управление, он парсил этот массив объектов и получал все необходимые данные. Но, кроме банального боксинга, здесь была ещё одна проблема — ни для кого, кроме разработчика, который писал этот контроллер (а со временем и для него самого) не было очевидно, какого типа аргументы и в каком порядке нужно передать, чтобы потом не возникло ошибок кастинга. Когда мы создаём новый метод, то в его сигнатуре указываем порядок и типы аргументов и затем IDE может нам подсказать, если при его вызове мы допустили ошибку. Но с аргументом params object[] args мы видим лишь то, что нужно передать массив аргументов и каждый раз, чтобы понять их порядок и типы, разработчику нужно лезть в код контроллера и смотреть как же они парсятся. Мне хотелось сохранить метод запуска таким же унифицированным (один метод для запуска любой сцены), но при этом дать возможность жестко ограничить типы аргументов для каждой из сцен. И для этого нужны ограничения where, которые есть в SceneController. Как известно, мы должны каждый раз передавать name или buildIndex сцены, чтобы загрузить её через метод LoadScene() или LoadSceneAsync() в Unity API. Этого хотелось бы избежать, потому я использую кастомный атрибут SceneControllerAttribute, чтобы привязать конкретный контроллере к конкретной сцене. Здесь используется имя сцены, а не её buildIndex лишь по той причине, что, на моём опыте, оно реже подвергается изменениям. [AttributeUsage(AttributeTargets.Class)]
public sealed class SceneControllerAttribute : Attribute { public string SceneName { get; private set; } public SceneControllerAttribute(string name) { SceneName = name; } } Допустим, у нас есть сцена MainMenu. В таком случае, классы её аргументов и контроллера будут иметь следующий вид: public sealed class MainMenuArgs : SceneArgs
{ // args' properties } [SceneControllerAttribute]
public sealed class MainMenuController : SceneController<MainMenuController, MainMenuArgs> { protected override void OnAwake() { // scene initialization } } Собственно, это всё (из того, что касается контроллера сцены и её аргументов). Осталось лишь понять, как происходит переход от одной сцены к другой. Этим занимается внезапно статический класс SceneManager. Очень важно, чтобы он был как можно меньше, проще и понятнее. Чтобы он не превратился в один из тех ненавистных божественных объектов с тоннами зависимостей. У него всего лишь одна простая задача — передать управление от контроллера одной сцены к контроллеру следующей. За все последующие инициализации и прочее отвечает уже сам контроллер. public static class SceneManager
{ private static readonly Dictionary<Type, SceneArgs> args; static SceneManager() { args = new Dictionary<Type, SceneArgs>(); } private static T GetAttribute<T>(Type type) where T : Attribute { object[] attributes = type.GetCustomAttributes(true); foreach (object attribute in attributes) if (attribute is T targetAttribute) return targetAttribute; return null; } public static AsyncOperation OpenSceneWithArgs<TController, TArgs>(TArgs sceneArgs) where TController : SceneController<TController, TArgs> where TArgs : SceneArgs, new() { Type type = typeof(TController); SceneControllerAttribute attribute = GetAttribute<SceneControllerAttribute>(type); if (attribute == null) throw new NullReferenceException($"You're trying to load scene controller without {nameof(SceneControllerAttribute)}"); string sceneName = attribute.SceneName; if (sceneArgs == null) args.Add(type, new TArgs { IsNull = true }); return UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(sceneName); } public static TArgs GetArgs<TController, TArgs>() where TController : SceneController<TController, TArgs> where TArgs : SceneArgs, new() { Type type = typeof(TController); if (!args.ContainsKey(type) || args[type] == null) return new TArgs { IsNull = true }; TArgs sceneArgs = (TArgs)args[type]; args.Remove(type); return sceneArgs; } } Позвольте мне немного объяснить этот код. При вызове OpenSceneWithArgs() вы передаёте тип контроллера (TController) сцены, которую нужно загрузить, тип параметров (TArgs) и, собственно, сами параметры (sceneArgs). В первую очередь, SceneManager проверяет, есть ли у TController атрибут SceneControllerAttribute. Он должен быть, потому именно он определяет, к какой сцен привязан контроллер TController. Дальше мы просто добавляем аргументы sceneArgs в словарь. Если не было передано каких-либо аргументов, мы создаём экземпляр типа TArgs и присваиваем его свойству IsNull значение true. Если всё прошло гладко, то будет вызван метод из Unity API LoadSceneAsynс() и ему будет передано имя сцены, которое берётся из атрибута SceneControllerAttribute. Загружается следующая сцена и у её контроллера вызывается метод Awake(). Дальше, как видим в SceneController, TController вызывает SceneManager.GetArgs(), чтобы получить аргументы, которые были переданы и записаны в словарь, а затем производит все необходимые инициализации. В результате у нас каждая сцена сама отвечает сама за себя, а переходы происходят через небольшой класс SceneManager, отвечающий исключительно за передачу управления между ними. Просто попробуйте применить этот подход в одном из ваших пет проектов и вы заметите, на сколько прозрачнее и понятнее для вас и всех кто работает с вами станут переходы между сценами и их инициализации. Буду рад вашим комментариям. Успехов в разработке! =========== Источник: habr.com =========== Похожие новости:
Разработка игр ), #_c#, #_unity |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:49
Часовой пояс: UTC + 5