Хватит блокировать вертикальную или горизонтальную ориентацию экрана в своих проектах на Unity! В этой статье мы рассмотрим небольшой скрипт, который я использовал в своем проекте.
При разработке приложения «Россети Книга Достижений», в прибавку к непростой задаче об оптимизации интерфейса на разные экрана, добавилась задача о поддержке разных ориентаций устройства, и вот как я ее решил:
Для начала необходимо реализовать скрипт, который поможет нам в сохранении и выгрузке значений RectTransform:
Код модели для хранения значений RectTransform
[Serializable] // Аттрибут, чтобы Unity сохранял наш объект public class SavedRect { // Присутствуют ли полезные данные в этом объекте public bool isInitialized = false; // Поля для хранения данных из RectTransform public Vector3 anchoredPosition; public Vector2 sizeDelta; public Vector2 minAnchor; public Vector2 maxAnchor; public Vector2 pivot; public Vector3 rotation; public Vector3 scale; /// <summary> /// Сохраняет данные из RectTransform в этот объект. /// </summary> /// <param name="rect"></param> public void SaveDataFromRectTransform(RectTransform rect) { if (rect == null) // null игнорируем return; isInitialized = true; // теперь полезные данные есть // сохраняем данные anchoredPosition = rect.anchoredPosition3D; sizeDelta = rect.sizeDelta; minAnchor = rect.anchorMin; maxAnchor = rect.anchorMax; pivot = rect.pivot; rotation = rect.localEulerAngles; scale = rect.localScale; } /// <summary> /// Выгружает данные из этого объекта в RectTransform /// </summary> /// <param name="rect"></param> public void PutDataToRectTransform(RectTransform rect) { // игнорируем null или пустые данные if (rect == null || !isInitialized) return; // выгружаем данные rect.anchoredPosition3D = anchoredPosition; rect.sizeDelta = sizeDelta; rect.anchorMin = minAnchor; rect.anchorMax = maxAnchor; rect.pivot = pivot; rect.localEulerAngles = rotation; rect.localScale = scale; } }
И теперь мы можем реализовать скрипт, который будет хранить данные для двух ориентаций экрана:
Код контроллера положения UI элемента
[ExecuteAlways] // выполняем скрипт и в Play, и в Edit моде [RequireComponent(typeof(RectTransform))] // нужен RectTransform на GameObject public sealed class OrientationController : MonoBehaviour { // Хранилище для вертикальной ориентации public SavedRect verticalRect = new SavedRect(); // Хранилище для горизонтальной ориентации public SavedRect horizontalRect = new SavedRect(); // Закэшированный RectTransform private RectTransform _rect; private void Awake() { _rect = GetComponent<RectTransform>(); // Подписываемся на событие OrientationChanged += OnOrientationChanged; // Проводим инициализационное принудительное обновление OnOrientationChanged(this, isVertical); } public void SaveCurrentState() { if (isVertical) verticalRect.SaveDataFromRectTransform(_rect); else horizontalRect.SaveDataFromRectTransform(_rect); } public void PutCurrentState() { OnOrientationChanged(this, isVertical); } private void OnOrientationChanged(object sender, bool isVertical) { if (isVertical) verticalRect.PutDataToRectTransform(_rect); else horizontalRect.PutDataToRectTransform(_rect); } private void OnDestroy() { // отписываемся от события OrientationChanged -= OnOrientationChanged; } // Static public static bool isVertical; // событие смены ориентации private static event EventHandler<bool> OrientationChanged; // метод для вызова события извне public static void FireOrientationChanged(object s, bool isVertical) => OrientationChanged?.Invoke(s, isVertical); /// статический конструктор нестатического класса вызывается 1 раз /// до первой инициализации объекта этого типа static OrientationController() { // обновляем isVertical при срабатывании события OrientationChanged += (s, e) => isVertical = e; } }
В этом скрипте присутствует OrientationChanged ивент, его можно вызывать через FireOrientationChanged(…), для обновления UI. Напишем тестовый скрипт, который будет этим управлять:
Код скрипта, проверяющего изменения ориентации
[ExecuteAlways] // скрипт работает всегда /// этот аттрибут нужен, чтобы быть уверенным, в том, что /// этот компонент будет инициализирован до OrientationController [DefaultExecutionOrder(-10)] public class OrientationChecker : MonoBehaviour { private void Awake() { // принудительное инициализационной обновление HandleOrientation(); } void Update() { // проверяем ориентацию каждый кадр HandleOrientation(); } private void HandleOrientation() { /// Если последняя ориентация была вертикальной /// и ширина экрана больше высоты ... if (OrientationController.isVertical && Screen.width > Screen.height) { // ... то вызываем событие, и передаем isVertical = false OrientationController.FireOrientationChanged(this, false); } else /// Иначе, если последняя ориентация была горизонтальной /// и ширина экрана меньше высоты ... if (!OrientationController.isVertical && Screen.width < Screen.height) { // ... то вызываем событие, и передаем isVertical = true OrientationController.FireOrientationChanged(this, true); } } }
Представленный вариант так же обрабатывает ориентацию на ПК, если вдруг вы не ограничиваетесь приложениями на мобильные устройства, он может определять книжный (Portrait) и альбомный (Landscape) режимы отображения монитора.
Теперь напишем скрипт для инспектора, что бы сохранять и выгружать позицию в режиме Edit, а не только Play. Вы могли уже заметить, что в скриптах используется аттрибут ExecuteAlways, что заставляет скрипт быть активным всегда. И так, код кастомного инспектора:
Код скрипта, изменяющего отображение компонента в инспекторе
/// Код внутри этой инструкции будет удален из билда /// это нужно, т.к. в билде нет библиотеки UnityEditor /// что вызовет ошибку компиляции #if UNITY_EDITOR using UnityEditor; // Помечаем, что это кастомный инспектор для OrientationController [CustomEditor(typeof(OrientationController))] // Помечаем, что этот компонент поддерживает редактирование сразу нескольких объектов [CanEditMultipleObjects] public class OrientationControllerEditor : Editor { public override void OnInspectorGUI() { serializedObject.Update(); // Выводим текущую ориентацию в самом верху ... GUILayout.Label("Current orientation: " + (OrientationController.isVertical ? "Vertical" : "Horizontal")); // ... рисуем стандартный испектор ... base.DrawDefaultInspector(); var controllers = targets; // ... после отрисовываем кнопку Save ... if (GUILayout.Button("Save values")) // этот цикл тут для поддержки редактирования нескольких объектов foreach(var controller in controllers) ((OrientationController)controller).SaveCurrentState(); // ... и после кнопку Put if (GUILayout.Button("Put values")) // этот цикл тут для поддержки редактирования нескольких объектов foreach (var controller in controllers) ((OrientationController)controller).PutCurrentState(); // сохраняем изменения serializedObject.ApplyModifiedProperties(); } } #endif
В этом скрипте используется инструкция компилятору «#if UNITY_EDITOR», это сделано с расчетом на незнакомых с спец. папками пользователей, если вы умеете создавать и использовать папку Editor в проекте, то можете удалить эти инструкции 🙂
И так, у нас все есть, добавляем на сцену OrientationChecker и на все необходимые UI элемнты OrientationController, при нажатии на кнопку «Save values» — компонент возьмет данные из RectTransform и сохранит у себя в нужную модель, согласно ориентации, при нажатии на «Put values» — вставит из соответствующего контейнера данные в RectTransform.
Изображение компонента в инспекторе (Внутри спойлера, т.к. я не умею скейлить изображения на хабре 😛 )
И мем связанный с моим неумением скейлить изображения:
Что бы изменять ориентацию в Unity Editor, нужно либо добавить свою (в выпадающий список на скриншоте ниже, через плюсик в конце), либо выбрать существующий. Чтобы получить тот же список, что и у меня, необходимо в качестве Platform выбрать iOS (или Android).
Спойлер
Да, это я использую Unity 2019.2 в 2022 году 🙂
На более новых версиях все будет выглядеть и работать так же, проверено до Unity 2021.3.x
И вот как выглядит результат, меняем ориентации — меняется Layout, так же он меняется сразу, если подгружать новую сцену:
Скрипт можно усложнять до бесконечности: добавить плавные переходы UI элементов на новые позиции, добавить управление AnimatorController для переключения паков анимаций для Landscape на Portrait и обратно и т.д.; подстроив его под любые нужны проекта.
Буду рад дополнениям и критике в комментариях, код продублирован на git’е:
AlexMorOR/Unity-UIOrientation (github.com)
ссылка на оригинал статьи https://habr.com/ru/post/681582/