Довольно часто бывает необходимо переключаться между разными окнами интерфейса и мы изучим на простых примерах как создавать и управлять этими переходами. Мы покажем вам как создавать переходы, используя анимацию и State Machines для управления и контроля над каждым из этих окон.
Главной идея здесь заключается в том, что каждое из наших окон будет иметь Animation Controller с двумя states (открытыми-Open and закрытыми-Closed) и boolean Paramenter (открытый-Open). Для перехода между окнами вам нужно всего лишь закрыть открытое в данный момент окно и открыть другое. Чтобы упростить этот процесс мы создадим небольшой класс ScreenManager, который будет следить за тем, чтобы любое уже открытое в данный момент окно закрывалось. Кнопка, отвечающая за переход будет выполнять всего лишь роль триггера, который будет запрашивать у менеджера экрана (ScreenManager) разрешение на открытие нужного окна.
Если вы планируете ввести поддержку других контроллеров/клавиатуры для навигации по элементам интерфейса, нужно помнить о нескольких важных моментах. Важно не допустить наличия выделяемых (Selectable) элементов вне экрана, так как тогда у игрока(-ов) появится возможность выделять их. И подобную возможность можно отключить путём деактивации вне экранной (off-screen) иерархии. Мы также должны убедиться, что, когда отображается новый экран, нами уже заранее установлен элемент от него, который можно выбрать, в противном случае игрок не сможет перейти к новому экрану. Обо всём этом мы позаботимся в классе ScreenManager чуть ниже.
Давайте взглянем на наиболее распространённые и минимальные настройки для Animation Controller-а для создания перехода экрана (Screen). Контроллеру понадобиться boolean параметр (открытый) и два состояния (открытое и закрытое), каждое состояние должно иметь анимацию всего лишь с одним ключевым кадром, таким образом мы позволим State Machine осуществлять для нас плавный переход с одного экрана на другой.
Теперь нам нужно создать transition между обоими состояниями, давайте начнём с перехода с открытого (Open) на закрытое (Closed) состояние и установим нужным образом наше состояние. Нам нужно перейти с открытого на закрытое и затем установить состояние с закрытого на открытое когда параметр (Open) примет значение равное true.
Со всей перечисленной выше настройкой, нам не хватает только одной вещи, и этой вещью является установка параметра 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, в выпадающем списке выберите и перетащите ту панель, которую вы хотите чтобы открывалась при нажатии пользователя на эту кнопку в ObjectField.
Преимущество этого метода заключается в том, что единственным требованием для полноценной работы здесь является то, что каждый экран должен иметь AnimatorController с Open параметром и Closed состоянием. Здесь совершенно неважно то, как построен ваш Screen или ваша State Machine. И всё это прекрасно работает с вложенными экранами, для работы которых вам понадобится всего один ScreenManager для каждого вложенного уровня.
Машина состояний (State Machine) которую мы настраивали выше по-умолчанию находится в состоянии Closed, поэтому все экраны, которые используют этот контроллер будут начинать свою работу в закрытом (closed) состоянии. Вот почему мы назначаем ScreenManager-у свойство initiallyOpen. Чтобы вы могли указать какой экран должен появляться первым.