Хватит блокировать вертикальную или горизонтальную ориентацию экрана в своих проектах на 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.
Изображение компонента в инспекторе (Внутри спойлера, т.к. я не умею скейлить изображения на хабре :P )
И мем связанный с моим неумением скейлить изображения:
Что бы изменять ориентацию в Unity Editor, нужно либо добавить свою (в выпадающий список на скриншоте ниже, через плюсик в конце), либо выбрать существующий. Чтобы получить тот же список, что и у меня, необходимо в качестве Platform выбрать iOS (или Android).
Спойлер
Да, это я использую Unity 2019.2 в 2022 году :)
На более новых версиях все будет выглядеть и работать так же, проверено до Unity 2021.3.x
И вот как выглядит результат, меняем ориентации - меняется Layout, так же он меняется сразу, если подгружать новую сцену:
Скрипт можно усложнять до бесконечности: добавить плавные переходы UI элементов на новые позиции, добавить управление AnimatorController для переключения паков анимаций для Landscape на Portrait и обратно и т.д.; подстроив его под любые нужны проекта.
Буду рад дополнениям и критике в комментариях, код продублирован на git'е:
AlexMorOR/Unity-UIOrientation (github.com)
Комментарии (6)
DezmontDeXa
10.08.2022 16:09Когда посмотрел первый кусок кода и ушел. Инкапсуляция где?
AlexMorOR Автор
10.08.2022 16:17Здравствуйте, если я правильно вас понял, что вы посмотрели код типа SavedRect, то инкапсулировать поля будет лишним, т.к. их изменение не сломает логику, и позволяет дизайнеру править данные без необходимости менять ориентацию в редакторе, и к тому же делает данные открытыми и следовательно простыми для проверки. Если вас неустраивает большой контроллер с кучей значений, то Unity предоставляет возможность свернуть эти поля типа SavedRect.
DezmontDeXa
10.08.2022 17:07Если хотите оставить открытии для изменений из инспектора, тогда следует использовать аттрибут SerializeField.
В вашем виде внутренние данные объекта полностью открыты для всех, в том числе для других скриптов, которые вроде как не должны изменять SavedRect, иначе какой из него Saved.
AlexMorOR Автор
10.08.2022 17:37Да, Вы правы, но видимой проблемы или преимущества это изменение не дает.
Tr0sT
Это работает для такого простого случая, но что случится, если кто-то перевернёт экран во время анимации, или после перемещения какого-нибудь элемента со своей стартовой позиции?
Обычно для поддержки нескольких ориентаций экрана, или более точного лэйаута (например, отдельно для 4:3 планшета и 21:9 смартфона) создают под каждый случай свою вьюшку. И при ивенте смене ориентации пересоздают и переинициализируют окошки.
AlexMorOR Автор
Здравствуйте, это хорошее замечание, для поддержки разных соотношений можно использовать подобный скрипт, дублировать интерфейс нецелесообразно, т.к. это удваивает работу, если только, конечно, речь не идет о кардинально другом лэйауте, в этом случае сама задача подразумевает "дублирование" работы. Но если такой задачи не стоит, то правильная верстка и этот скрипт будет достаточным решением, пример можно увидеть в приложении, которое стало основой статьи, оно поддерживает как планшеты, так и мобильные устройства, без дополнительных скриптов, кроме представленного, с небольшой доработкой на смену анимации. Ссылка на приложение есть в начале статьи и в конце этого ответа.
Насчет анимаций: в большинстве случае анимации перемещения интерфейса длятся <1 сек, а при смене ориентации проигрывается анимация поворота длиной 0.5 сек, за это время можно сменить набор анимаций и перевести элемент в конечную точку запущенной анимации, пример можно увидеть в приложении.
Насчет перемещения: если задача подразумевает перемещение без анимаций, то необходимо доработать скрипт, согласно вашему случаю, универсальное решение тут сложно придумать.
Ссылки на приложение:
https://apps.apple.com/de/app/россети-центр-книга-достижений/id1498965479
https://play.google.com/store/apps/details?id=com.NexusStudio.mrskglorybook&hl=af&gl=US