Как стать автором
Обновить

Unity: Небольшой скрипт = все ориентации экрана

Время на прочтение6 мин
Количество просмотров5.3K

Хватит блокировать вертикальную или горизонтальную ориентацию экрана в своих проектах на 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, так же он меняется сразу, если подгружать новую сцену:

Landscape ориентация, с настроенным OrientationController на квадратах слева, и без OrientationController на квадрате справа и стрелке.
Landscape ориентация, с настроенным OrientationController на квадратах слева, и без OrientationController на квадрате справа и стрелке.
Portrait ориентация, здесь можно увидеть преимущества скрипта над системой RectTransform из коробки Unity.
Portrait ориентация, здесь можно увидеть преимущества скрипта над системой RectTransform из коробки Unity.

Скрипт можно усложнять до бесконечности: добавить плавные переходы UI элементов на новые позиции, добавить управление AnimatorController для переключения паков анимаций для Landscape на Portrait и обратно и т.д.; подстроив его под любые нужны проекта.

Буду рад дополнениям и критике в комментариях, код продублирован на git'е:
AlexMorOR/Unity-UIOrientation (github.com)

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии6

Публикации

Истории

Работа

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань