Version: 5.4
Создание элементов интерфейса путём скриптинга
Immediate Mode GUI (IMGUI)

Создание экранных переходов (Screen Transitons)

Довольно часто бывает необходимо переключаться между разными окнами интерфейса и мы изучим на простых примерах как создавать и управлять этими переходами. Мы покажем вам как создавать переходы, используя анимацию и State Machines для управления и контроля над каждым из этих окон.

Обзор

Главной идея здесь заключается в том, что каждое из наших окон будет иметь Animation Controller с двумя states (открытыми-Open and закрытыми-Closed) и boolean Paramenter (открытый-Open). Для перехода между окнами вам нужно всего лишь закрыть открытое в данный момент окно и открыть другое. Чтобы упростить этот процесс мы создадим небольшой класс ScreenManager, который будет следить за тем, чтобы любое уже открытое в данный момент окно закрывалось. Кнопка, отвечающая за переход будет выполнять всего лишь роль триггера, который будет запрашивать у менеджера экрана (ScreenManager) разрешение на открытие нужного окна.

Размышляя о навигации

Если вы планируете ввести поддержку других контроллеров/клавиатуры для навигации по элементам интерфейса, нужно помнить о нескольких важных моментах. Важно не допустить наличия выделяемых (Selectable) элементов вне экрана, так как тогда у игрока(-ов) появится возможность выделять их. И подобную возможность можно отключить путём деактивации вне экранной (off-screen) иерархии. Мы также должны убедиться, что, когда отображается новый экран, нами уже заранее установлен элемент от него, который можно выбрать, в противном случае игрок не сможет перейти к новому экрану. Обо всём этом мы позаботимся в классе ScreenManager чуть ниже.

Настраивая контроллер анимации (Animatior Controller)

Давайте взглянем на наиболее распространённые и минимальные настройки для Animation Controller-а для создания перехода экрана (Screen). Контроллеру понадобиться boolean параметр (открытый) и два состояния (открытое и закрытое), каждое состояние должно иметь анимацию всего лишь с одним ключевым кадром, таким образом мы позволим State Machine осуществлять для нас плавный переход с одного экрана на другой.

Открытое состояние и анимация
Открытое состояние и анимация
Закрытое состояние и анимация
Закрытое состояние и анимация

Теперь нам нужно создать transition между обоими состояниями, давайте начнём с перехода с открытого (Open) на закрытое (Closed) состояние и установим нужным образом наше состояние. Нам нужно перейти с открытого на закрытое и затем установить состояние с закрытого на открытое когда параметр (Open) примет значение равное true.

Переход между закрытым (Closed) и открытым (Open) состоянием
Переход между закрытым (Closed) и открытым (Open) состоянием
Переход между открытым (Open) и закрытым (Closed) состоянием
Переход между открытым (Open) и закрытым (Closed) состоянием

Управление окнами (Screens)

Со всей перечисленной выше настройкой, нам не хватает только одной вещи, и этой вещью является установка параметра Open в значение true в Animator-е экрана, к которому мы хотим применить переход и установка Open в значение false для текущего Animator-а экрана. С этой целью мы создадим скрипт, который поможет нам в этом.

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;

public class ScreenManager : MonoBehaviour {

    //Screen to open automatically at the start of the Scene
    public Animator initiallyOpen;

    //Currently Open Screen
    private Animator m_Open;

    //Hash of the parameter we use to control the transitions.
    private int m_OpenParameterId;

    //The GameObject Selected before we opened the current Screen.
    //Used when closing a Screen, so we can go back to the button that opened it.
    private GameObject m_PreviouslySelected;

    //Animator State and Transition names we need to check against.
    const string k_OpenTransitionName = "Open";
    const string k_ClosedStateName = "Closed";

    public void OnEnable()
    {
        //We cache the Hash to the "Open" Parameter, so we can feed to Animator.SetBool.
        m_OpenParameterId = Animator.StringToHash (k_OpenTransitionName);

        //If set, open the initial Screen now.
        if (initiallyOpen == null)
            return;
        OpenPanel(initiallyOpen);
    }

    //Closes the currently open panel and opens the provided one.
    //It also takes care of handling the navigation, setting the new Selected element.
    public void OpenPanel (Animator anim)
    {
        if (m_Open == anim)
            return;

        //Activate the new Screen hierarchy so we can animate it.
        anim.gameObject.SetActive(true);
        //Save the currently selected button that was used to open this Screen. (CloseCurrent will modify it)
        var newPreviouslySelected = EventSystem.current.currentSelectedGameObject;
        //Move the Screen to front.
        anim.transform.SetAsLastSibling();

        CloseCurrent();

        m_PreviouslySelected = newPreviouslySelected;

        //Set the new Screen as then open one.
        m_Open = anim;
        //Start the open animation
        m_Open.SetBool(m_OpenParameterId, true);

        //Set an element in the new screen as the new Selected one.
        GameObject go = FindFirstEnabledSelectable(anim.gameObject);
        SetSelected(go);
    }

    //Finds the first Selectable element in the providade hierarchy.
    static GameObject FindFirstEnabledSelectable (GameObject gameObject)
    {
        GameObject go = null;
        var selectables = gameObject.GetComponentsInChildren<Selectable> (true);
        foreach (var selectable in selectables) {
            if (selectable.IsActive () && selectable.IsInteractable ()) {
                go = selectable.gameObject;
                break;
            }
        }
        return go;
    }

    //Closes the currently open Screen
    //It also takes care of navigation.
    //Reverting selection to the Selectable used before opening the current screen.
    public void CloseCurrent()
    {
        if (m_Open == null)
            return;

        //Start the close animation.
        m_Open.SetBool(m_OpenParameterId, false);

        //Reverting selection to the Selectable used before opening the current screen.
        SetSelected(m_PreviouslySelected);
        //Start Coroutine to disable the hierarchy when closing animation finishes.
        StartCoroutine(DisablePanelDeleyed(m_Open));
        //No screen open.
        m_Open = null;
    }

    //Coroutine that will detect when the Closing animation is finished and it will deactivate the
    //hierarchy.
    IEnumerator DisablePanelDeleyed(Animator anim)
    {
        bool closedStateReached = false;
        bool wantToClose = true;
        while (!closedStateReached && wantToClose)
        {
            if (!anim.IsInTransition(0))
                closedStateReached = anim.GetCurrentAnimatorStateInfo(0).IsName(k_ClosedStateName);

            wantToClose = !anim.GetBool(m_OpenParameterId);

            yield return new WaitForEndOfFrame();
        }

        if (wantToClose)
            anim.gameObject.SetActive(false);
    }

    //Make the provided GameObject selected
    //When using the mouse/touch we actually want to set it as the previously selected and 
    //set nothing as selected for now.
    private void SetSelected(GameObject go)
    {
        //Select the GameObject.
        EventSystem.current.SetSelectedGameObject(go);

        //If we are using the keyboard right now, that's all we need to do.
        var standaloneInputModule = EventSystem.current.currentInputModule as StandaloneInputModule;
        if (standaloneInputModule != null && standaloneInputModule.inputMode == StandaloneInputModule.InputMode.Buttons)
            return;

        //Since we are using a pointer device, we don't want anything selected. 
        //But if the user switches to the keyboard, we want to start the navigation from the provided game object.
        //So here we set the current Selected to null, so the provided gameObject becomes the Last Selected in the EventSystem.
        EventSystem.current.SetSelectedGameObject(null);
    }
}

Давайте подключим этот скрипт. Мы сделаем это путём создания нового игрового объекта (GameObject), который мы можем переименовать например в “ScreenManager” и добавить к нему компонент. К нему можно привязать стартовый экран, который будет появляться когда будет запускаться ваша сцена.

Наконец, в финале давайте сделаем так, чтобы UI buttons заработали. Выберите кнопку, которая должна реагировать на переход экрана и добавьте новое действие для события On Click () из выпадающего списка Inspector-а. Перетащите ScreenManager GameObject, который мы только что создали в ObjectField, в выпадающем списке выберите ScreenManager->OpenPanel (Animator) и перетащите ту панель, которую вы хотите чтобы открывалась при нажатии пользователя на эту кнопку в ObjectField.

Button Inspector
Button Inspector

Замечания

Преимущество этого метода заключается в том, что единственным требованием для полноценной работы здесь является то, что каждый экран должен иметь AnimatorController с Open параметром и Closed состоянием. Здесь совершенно неважно то, как построен ваш Screen или ваша State Machine. И всё это прекрасно работает с вложенными экранами, для работы которых вам понадобится всего один ScreenManager для каждого вложенного уровня.

Машина состояний (State Machine) которую мы настраивали выше по-умолчанию находится в состоянии Closed, поэтому все экраны, которые используют этот контроллер будут начинать свою работу в закрытом (closed) состоянии. Вот почему мы назначаем ScreenManager-у свойство initiallyOpen. Чтобы вы могли указать какой экран должен появляться первым.

Создание элементов интерфейса путём скриптинга
Immediate Mode GUI (IMGUI)