Всем привет. Прямо сейчас в OTUS открыт набор на курс «Unity Game Developer. Basic». Предлагаем вам посмотреть запись дня открытых дверей по курсу, а также традиционно делимся интересным переводом.
Большинство игр подразумевают множество уровней, а уровни зачастую содержат более одной сцены. В играх, где сцены относительно небольшие, вы можете разбить их на разные части с помощью префабов (Prefab). Однако, чтобы подключать или инстанцировать их во время игры, вам необходимо сослаться на все эти префабы. Это означает, что по мере того, как ваша игра становится больше и эти ссылки занимают больше места в памяти, более эффективным решением становится использование сцен.
Вы можете разбить уровни на одну или несколько сцен (Scene) Unity. Ключевым моментом становится поиск оптимального способа управления ими. Вы можете открывать сразу несколько сцен в редакторе и во время выполнения с помощью функции редактирования множества сцен (Multi-Scene editing). Разделение уровней на несколько сцен также упрощает работу в команде, поскольку позволяет избежать конфликтов слияния в таких инструментах совместной работы, как Git, SVN, Unity Collaborate и т. д.
В видео ниже мы покажем, как загружать уровень более эффективно, разбив логику игры и различные части уровня на несколько отдельных сцен Unity. Затем, используя режим аддитивной загрузки сцен (Additive Scene-loading mode) при загрузке этих сцен, мы загружаем и выгружаем необходимые части вместе с игровой логикой, которая никуда не пропадает. Мы используем префабы в качестве «якорей» для сцен, что также обеспечивает большую гибкость при работе в команде, поскольку каждая сцена представляет собой часть уровня и может редактироваться отдельно.
Вы по-прежнему можете загрузить эти сцены в режиме редактирования и в любой момент нажать Play, чтобы визуализировать их все вместе при работе над левел дизайном.
Мы покажем два разных метода загрузки этих сцен. Первый основан на расстоянии, что хорошо подходит для не интерьерных уровней, таких как открытый мир. Этот метод также полезен для некоторых визуальных эффектов (например, тумана), чтобы скрыть процесс загрузки и выгрузки.
Второй метод использует Trigger для проверки того, какие сцены необходимо загружать, что более эффективно при работе с интерьерами.
Теперь, когда мы разобрались со всем внутри уровня, мы можем добавить поверх него дополнительный слой, чтобы эффективнее управлять самими уровнями.
Мы хотим отслеживать различные сцены на каждом уровне, а также все уровни в течение всего игрового процесса. Один из возможных способов добиться этого — использовать статические переменные и синглтоны в скриптах MonoBehaviour, но с этим решением не все так гладко. Использование синглтона подразумевает жесткие связи между вашими системами, поэтому он не является в строгом понимании модульным. Системы не могут существовать отдельно и всегда будут зависеть друг от друга.
Другая проблема связана с использованием статических переменных. Поскольку вы не можете увидеть их в инспекторе (Inspector), вам необходимо определять их через код, что усложняет художникам или левел дизайнерам тестирование игры. Когда вам нужно, чтобы данные были разделены между разными сценами, вы используете статические переменные в сочетании с DontDestroyOnLoad, но последнего следует по возможности избегать.
Для хранения информации о различных сценах можно использовать ScriptableObject, сериализуемый класс, который в основном используется для хранения данных. В отличие от скриптов MonoBehaviour, которые используются в качестве компонентов, привязанных к GameObjects, ScriptableObjects не привязаны к каким-либо GameObject и, таким образом, могут использоваться разными сценами всего проекта.
Было бы неплохо иметь возможность использовать эту структуру для уровней, а также для сцен меню в вашей игре. Для этого создайте класс GameScene, содержащий различные общие свойства уровней и меню.
Обратите внимание, что класс наследуется от ScriptableObject, а не от MonoBehaviour. Вы можете добавить столько свойств, сколько нужно для вашей игры. После этого шага вы можете создать классы Level и Menu, которые наследуются от только что созданного класса GameScene, поэтому они также являются ScriptableObjects.
Добавление атрибута CreateAssetMenu вверху позволяет создать новый уровень из меню Assets в Unity. Вы можете сделать то же самое для класса Menu. Вы также можете добавить перечисление, чтобы иметь возможность выбирать тип меню из инспектора.
Теперь, когда вы можете создавать уровни и меню, давайте для удобства добавим базу данных, которая их перечисляет (уровни и меню). Вы также можете добавить индекс для отслеживания текущего уровня игрока. Затем вы можете добавить методы для загрузки новой игры (в этом случае будет загружен первый уровень), для повтора текущего уровня и для перехода на следующий уровень. Обратите внимание, что в этих трех методах изменяется только индекс, поэтому вы можете создать метод, который загружает уровень по индексу, чтобы повторно его использовать.
Существуют также методы меню, и вы можете использовать тип перечисления, который вы создали ранее, для загрузки конкретного меню, которое вам нужно — просто убедитесь, что порядок в перечислении и порядок в списке меню одинаковы.
Наконец, теперь вы можете создать уровень, меню или объект ScriptableObject базы данных из меню Assets, щелкнув правой кнопкой мыши в окне Project.
Дальше просто продолжайте добавлять нужные уровни и меню, настраивая параметры, а затем добавляя их в базу данных сцен. В приведенном ниже примере показано, как выглядят данные Level1, MainMenu и Scenes.
Настало время вызывать эти методы. В этом примере кнопка Next Level в пользовательском интерфейсе (UI), которая появляется, когда игрок достигает конца уровня, вызывает метод NextLevel. Чтобы привязать метод к кнопке, нажмите кнопку с плюсом события On Click компонента Button, чтобы добавить новое событие, затем перетащите объект ScriptableObject данных сцены в поле объекта и выберите метод NextLevel из ScenesData, как показано ниже.
Теперь вы можете проделать тот же процесс для других кнопок — переиграть уровень или перейти в главное меню и так далее. Вы также можете ссылаться на ScriptableObject из любого другого скрипта, чтобы получить доступ к различным свойствам, таким как AudioClip для фоновой музыки или профилю постобработки, и использовать их на уровне.
Минимизация загрузки/выгрузки
В скрипте ScenePartLoader, показанном на видео, вы можете видеть, что игрок может продолжать входить и выходить из коллайдера несколько раз, вызывая повторную загрузку и выгрузку сцены. Чтобы избежать этого, вы можете добавить корутину перед вызовом методов загрузки и выгрузки сцены в скрипте и останавливать корутину, если игрок покидает триггер.
Соглашения об именовании
Еще один глобальный совет — использовать в проекте твердые соглашения об именовании. Команда должна заранее договориться о том, как называть различные типы ассетов — от скриптов и сцен до материалов и других вещей в проекте. Это упростит работу над проектом и его поддержку не только вам, но и вашим товарищам по команде. Это всегда хорошая идея, но в данном конкретном случае она очень важна для управления сценами с помощью ScriptableObjects. В нашем примере использовался простой подход, основанный на имени сцены, но есть много различных решений, которые меньше полагаются на имя сцены. Вам следует избегать подхода на основе строк, потому что если вы переименуете сцену Unity в данном контексте, в другой части игры эта сцена не загрузится.
Специальные инструменты
Один из способов избежать зависимости от имен в рамках всей игры — настроить скрипт так, чтобы он ссылался на сцены как на тип Object. Это позволяет вам перетаскивать ресурс сцены в инспекторе, а затем спокойно получать его имя в скрипте. Однако, поскольку это класс Editor, у вас нет доступа к классу AssetDatabase во время выполнения, поэтому вам необходимо объединить обе части данных для решения, которое работает в редакторе, предотвращает человеческий фактор и по-прежнему работает во время выполнения. Вы можете обратиться к интерфейсу ISerializationCallbackReceiver для примера того, как реализовать объект, который после сериализации может извлекать строковый путь из ассета Scene и сохранять его для использования во время выполнения.
Кроме того, вы также можете создать собственный инспектор, чтобы упростить быстрое добавление сцен в Build Settings с помощью кнопок, вместо того, чтобы добавлять их вручную через это меню и поддерживать их синхронизацию.
В качестве примера этого типа инструмента посмотрите эту замечательную реализацию с открытым исходным кодом от разработчика JohannesMP (это не официальный ресурс Unity).
Этот пост показывает только один способ, с помощью которого ScriptableObjects может улучшить ваш рабочий процесс при работе с несколькими сценами в сочетании с префабами. В разных играх используются совершенно разные способы управления сценами — ни одно типовое решение не подходит сразу для всех игровых структур. Имеет смысл реализовать собственные инструменты, соответствующие организации вашего проекта.
Мы надеемся, что эта информация поможет вам в вашем проекте или, возможно, вдохновит вас на создание собственных инструментов управления сценами.
Сообщите нам в комментариях, если у вас возникнут вопросы. Мы хотели бы услышать, какие методы вы используете для управления сценами в вашей игре. И не стесняйтесь предлагать другие варианты использования, которые вы хотели бы предложить для рассмотрения в будущих публикациях.
Работа сразу с несколькими сценами в Unity может быть сложной задачей, и оптимизация этого рабочего процесса очень сильно влияет как на производительность вашей игры, так и на продуктивность вашей команды. Сегодня мы поделимся с вами советами по настройке рабочих процессов со Scene, которые можно масштабировать на более крупные проекты.
Большинство игр подразумевают множество уровней, а уровни зачастую содержат более одной сцены. В играх, где сцены относительно небольшие, вы можете разбить их на разные части с помощью префабов (Prefab). Однако, чтобы подключать или инстанцировать их во время игры, вам необходимо сослаться на все эти префабы. Это означает, что по мере того, как ваша игра становится больше и эти ссылки занимают больше места в памяти, более эффективным решением становится использование сцен.
Вы можете разбить уровни на одну или несколько сцен (Scene) Unity. Ключевым моментом становится поиск оптимального способа управления ими. Вы можете открывать сразу несколько сцен в редакторе и во время выполнения с помощью функции редактирования множества сцен (Multi-Scene editing). Разделение уровней на несколько сцен также упрощает работу в команде, поскольку позволяет избежать конфликтов слияния в таких инструментах совместной работы, как Git, SVN, Unity Collaborate и т. д.
Управление множеством сцен для создания уровня
В видео ниже мы покажем, как загружать уровень более эффективно, разбив логику игры и различные части уровня на несколько отдельных сцен Unity. Затем, используя режим аддитивной загрузки сцен (Additive Scene-loading mode) при загрузке этих сцен, мы загружаем и выгружаем необходимые части вместе с игровой логикой, которая никуда не пропадает. Мы используем префабы в качестве «якорей» для сцен, что также обеспечивает большую гибкость при работе в команде, поскольку каждая сцена представляет собой часть уровня и может редактироваться отдельно.
Вы по-прежнему можете загрузить эти сцены в режиме редактирования и в любой момент нажать Play, чтобы визуализировать их все вместе при работе над левел дизайном.
Мы покажем два разных метода загрузки этих сцен. Первый основан на расстоянии, что хорошо подходит для не интерьерных уровней, таких как открытый мир. Этот метод также полезен для некоторых визуальных эффектов (например, тумана), чтобы скрыть процесс загрузки и выгрузки.
Второй метод использует Trigger для проверки того, какие сцены необходимо загружать, что более эффективно при работе с интерьерами.
Теперь, когда мы разобрались со всем внутри уровня, мы можем добавить поверх него дополнительный слой, чтобы эффективнее управлять самими уровнями.
Управление множеством игровых уровней с помощью ScriptableObjects
Мы хотим отслеживать различные сцены на каждом уровне, а также все уровни в течение всего игрового процесса. Один из возможных способов добиться этого — использовать статические переменные и синглтоны в скриптах MonoBehaviour, но с этим решением не все так гладко. Использование синглтона подразумевает жесткие связи между вашими системами, поэтому он не является в строгом понимании модульным. Системы не могут существовать отдельно и всегда будут зависеть друг от друга.
Другая проблема связана с использованием статических переменных. Поскольку вы не можете увидеть их в инспекторе (Inspector), вам необходимо определять их через код, что усложняет художникам или левел дизайнерам тестирование игры. Когда вам нужно, чтобы данные были разделены между разными сценами, вы используете статические переменные в сочетании с DontDestroyOnLoad, но последнего следует по возможности избегать.
Для хранения информации о различных сценах можно использовать ScriptableObject, сериализуемый класс, который в основном используется для хранения данных. В отличие от скриптов MonoBehaviour, которые используются в качестве компонентов, привязанных к GameObjects, ScriptableObjects не привязаны к каким-либо GameObject и, таким образом, могут использоваться разными сценами всего проекта.
Было бы неплохо иметь возможность использовать эту структуру для уровней, а также для сцен меню в вашей игре. Для этого создайте класс GameScene, содержащий различные общие свойства уровней и меню.
public class GameScene : ScriptableObject
{
[Header("Information")]
public string sceneName;
public string shortDescription;
[Header("Sounds")]
public AudioClip music;
[Range(0.0f, 1.0f)]
public float musicVolume;
[Header("Visuals")]
public PostProcessProfile postprocess;
}
Обратите внимание, что класс наследуется от ScriptableObject, а не от MonoBehaviour. Вы можете добавить столько свойств, сколько нужно для вашей игры. После этого шага вы можете создать классы Level и Menu, которые наследуются от только что созданного класса GameScene, поэтому они также являются ScriptableObjects.
[CreateAssetMenu(fileName = "NewLevel", menuName = "Scene Data/Level")]
public class Level : GameScene
{
// Настройки, относящиеся только к уровню
[Header("Level specific")]
public int enemiesCount;
}
Добавление атрибута CreateAssetMenu вверху позволяет создать новый уровень из меню Assets в Unity. Вы можете сделать то же самое для класса Menu. Вы также можете добавить перечисление, чтобы иметь возможность выбирать тип меню из инспектора.
public enum Type
{
Main_Menu,
Pause_Menu
}
[CreateAssetMenu(fileName = "NewMenu", menuName = "Scene Data/Menu")]
public class Menu : GameScene
{
// Настройки, относящиеся только к меню
[Header("Menu specific")]
public Type type;
}
Теперь, когда вы можете создавать уровни и меню, давайте для удобства добавим базу данных, которая их перечисляет (уровни и меню). Вы также можете добавить индекс для отслеживания текущего уровня игрока. Затем вы можете добавить методы для загрузки новой игры (в этом случае будет загружен первый уровень), для повтора текущего уровня и для перехода на следующий уровень. Обратите внимание, что в этих трех методах изменяется только индекс, поэтому вы можете создать метод, который загружает уровень по индексу, чтобы повторно его использовать.
[CreateAssetMenu(fileName = "sceneDB", menuName = "Scene Data/Database")]
public class ScenesData : ScriptableObject
{
public List<Level> levels = new List<Level>();
public List<Menu> menus = new List<Menu>();
public int CurrentLevelIndex=1;
/*
* Уровни
*/
// Загружаем сцену с заданным индексом
public void LoadLevelWithIndex(int index)
{
if (index <= levels.Count)
{
// Загружаем сцену геймплея для уровня
SceneManager.LoadSceneAsync("Gameplay" + index.ToString());
// Загружаем первую часть уровня в аддитивном режиме
SceneManager.LoadSceneAsync("Level" + index.ToString() + "Part1", LoadSceneMode.Additive);
}
// сбрасываем индекс, если у нас больше нет уровней
else CurrentLevelIndex =1;
}
// Запуск следующего уровня
public void NextLevel()
{
CurrentLevelIndex++;
LoadLevelWithIndex(CurrentLevelIndex);
}
// Перезапускаем текущий уровень
public void RestartLevel()
{
LoadLevelWithIndex(CurrentLevelIndex);
}
// Новая игра, загрузка первого уровня
public void NewGame()
{
LoadLevelWithIndex(1);
}
/*
* Меню
*/
// Загрузить главное меню
public void LoadMainMenu()
{
SceneManager.LoadSceneAsync(menus[(int)Type.Main_Menu].sceneName);
}
// Загрузить меню паузы
public void LoadPauseMenu()
{
SceneManager.LoadSceneAsync(menus[(int)Type.Pause_Menu].sceneName);
}
Существуют также методы меню, и вы можете использовать тип перечисления, который вы создали ранее, для загрузки конкретного меню, которое вам нужно — просто убедитесь, что порядок в перечислении и порядок в списке меню одинаковы.
Наконец, теперь вы можете создать уровень, меню или объект ScriptableObject базы данных из меню Assets, щелкнув правой кнопкой мыши в окне Project.
Дальше просто продолжайте добавлять нужные уровни и меню, настраивая параметры, а затем добавляя их в базу данных сцен. В приведенном ниже примере показано, как выглядят данные Level1, MainMenu и Scenes.
Настало время вызывать эти методы. В этом примере кнопка Next Level в пользовательском интерфейсе (UI), которая появляется, когда игрок достигает конца уровня, вызывает метод NextLevel. Чтобы привязать метод к кнопке, нажмите кнопку с плюсом события On Click компонента Button, чтобы добавить новое событие, затем перетащите объект ScriptableObject данных сцены в поле объекта и выберите метод NextLevel из ScenesData, как показано ниже.
Теперь вы можете проделать тот же процесс для других кнопок — переиграть уровень или перейти в главное меню и так далее. Вы также можете ссылаться на ScriptableObject из любого другого скрипта, чтобы получить доступ к различным свойствам, таким как AudioClip для фоновой музыки или профилю постобработки, и использовать их на уровне.
Советы по минимизации ошибок в ваших процессах
Минимизация загрузки/выгрузки
В скрипте ScenePartLoader, показанном на видео, вы можете видеть, что игрок может продолжать входить и выходить из коллайдера несколько раз, вызывая повторную загрузку и выгрузку сцены. Чтобы избежать этого, вы можете добавить корутину перед вызовом методов загрузки и выгрузки сцены в скрипте и останавливать корутину, если игрок покидает триггер.
Соглашения об именовании
Еще один глобальный совет — использовать в проекте твердые соглашения об именовании. Команда должна заранее договориться о том, как называть различные типы ассетов — от скриптов и сцен до материалов и других вещей в проекте. Это упростит работу над проектом и его поддержку не только вам, но и вашим товарищам по команде. Это всегда хорошая идея, но в данном конкретном случае она очень важна для управления сценами с помощью ScriptableObjects. В нашем примере использовался простой подход, основанный на имени сцены, но есть много различных решений, которые меньше полагаются на имя сцены. Вам следует избегать подхода на основе строк, потому что если вы переименуете сцену Unity в данном контексте, в другой части игры эта сцена не загрузится.
Специальные инструменты
Один из способов избежать зависимости от имен в рамках всей игры — настроить скрипт так, чтобы он ссылался на сцены как на тип Object. Это позволяет вам перетаскивать ресурс сцены в инспекторе, а затем спокойно получать его имя в скрипте. Однако, поскольку это класс Editor, у вас нет доступа к классу AssetDatabase во время выполнения, поэтому вам необходимо объединить обе части данных для решения, которое работает в редакторе, предотвращает человеческий фактор и по-прежнему работает во время выполнения. Вы можете обратиться к интерфейсу ISerializationCallbackReceiver для примера того, как реализовать объект, который после сериализации может извлекать строковый путь из ассета Scene и сохранять его для использования во время выполнения.
Кроме того, вы также можете создать собственный инспектор, чтобы упростить быстрое добавление сцен в Build Settings с помощью кнопок, вместо того, чтобы добавлять их вручную через это меню и поддерживать их синхронизацию.
В качестве примера этого типа инструмента посмотрите эту замечательную реализацию с открытым исходным кодом от разработчика JohannesMP (это не официальный ресурс Unity).
Дайте нам знать, что вы думаете
Этот пост показывает только один способ, с помощью которого ScriptableObjects может улучшить ваш рабочий процесс при работе с несколькими сценами в сочетании с префабами. В разных играх используются совершенно разные способы управления сценами — ни одно типовое решение не подходит сразу для всех игровых структур. Имеет смысл реализовать собственные инструменты, соответствующие организации вашего проекта.
Мы надеемся, что эта информация поможет вам в вашем проекте или, возможно, вдохновит вас на создание собственных инструментов управления сценами.
Сообщите нам в комментариях, если у вас возникнут вопросы. Мы хотели бы услышать, какие методы вы используете для управления сценами в вашей игре. И не стесняйтесь предлагать другие варианты использования, которые вы хотели бы предложить для рассмотрения в будущих публикациях.
Читать ещё