Es bastante común la necesidad de transicionar entre varias pantallas UI. En esta página exploraremos una manera de crear y manejar esas transiciones utilizando animaciones y State Machines (maquinas de estado) para manejar y controlar cada pantalla.
La idea de alto nivel es que cada una de nuestras pantallas tendrá un Animator Controller con dos states (estado) (Open y Closed (Abierto y cerrado) ) y un Parámetro boolean (Open (abierto)). Para transicionar entre pantallas usted solo va a necesitar cerrar la pantalla actualmente abierta y abrir la deseada. Para hacer este proceso más fácil, nosotros vamos a crear una pequeña Clase ScreenManager que mantendrá un seguimiento y se encargará de cerrar cualquier pantalla abierta para nosotros. El botón que triggers (activa/desactiva) la transición solo necesitará preguntar al ScreenManager para abrir la pantalla deseada.
Si usted planea soportar una navegación con controlador/teclado de elementos UI, entonces es importante tener unas cosas en mente. Es importante evitar tener elementos seleccionables afuera de la pantalla ya que esto permitiría a lo jugadores en seleccionar elementos que no estén en la pantalla, nosotros podemos hacer esto al desactivar cualquier jerarquía que sea afuera de la pantalla. Nosotros también necesitamos asegurarnos que cuando una nueva pantalla sea mostrada, nosotros configuremos un elemento de este como seleccionado, de lo contrario el jugador no sería capaz de navegar a una nueva pantalla. Nosotros nos aseguraremos de todo eso en la clase ScreenManager abajo.
Echemos un vistazo a la configuración más común y minima para que haga el Animation Controller en una transición de pantalla. El controller va a necesitar un parámetro boolean (Open-abierto) y dos estados (Open y Closed (Abierto y Cerrado)), cada estado debería tener una animación con un solo keyframe, de esta manera nosotros dejamos que la State Machine (Maquina de estado) haga la mezcla de la transición por nosotros.
Ahora nosotros necesitamos crear la transition entre ambos estados, comencemos con la transición de Open (abierto) a Closed (cerrado) y configuremos su condición apropiadamente, nosotros queremos ir de Open (abierto) a Closed (cerrado) cuando el parámetro Open sea configurado a falso. Ahora nosotros creamos la transición de Closed (cerrado) a Open (abierto) y configure la condición para ir de Closed (cerrado) a Open (abierto) cuando el parámetro Open (abierto) sea verdad.
Con todo lo de arriba configurado, la única cosa que falta para nosotros es configurar el parámetro Open (abierto) a verdad en la pantalla del Animator nosotros queremos que haga una transición y Open (abra) en falso en las pantallas Animator actuales. Para hacer esto nosotros vamos a crear un pequeño Script que se va a encargar de eso para nosotros.
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);
}
}
Ahora añadamos ese script, nosotros hacemos esto al crear un nuevo GameObject, nosotros le cambiamos el nombre a “ScreenManager” por ejemplo, y agregamos el componente de arriba a él. Usted puede asignar una pantalla inicial a él, esta pantalla va a abrir al principio de su escena.
Ahora para la parte final, hagamos que los UI buttons funcionen. Seleccione el botón que debería trigger (Activar/desactivar) la transición de la pantalla y agreguemos una nueva acción bajo la lista On Click () en el Inspector. Arrastre el GameObject ScreenManage que acabamos de crear al ObjectField (Campo de objeto), en el despegable seleccione ScreenManager->OpenPanel (Animator) y arrastre el panel que usted quiere abrir cuando el usuario hace click en el botón del campo de objeto.
Esta técnica tiene la ventaja de que el único requerimiento para que funcione es que cada pantalla debería tener un AnimatorController con un parámetro Open y un estado Closed (cerrado). Es completamente despreocupado con los detalles de cómo su Pantalla o su State Machine (Maquina de estado) es construida. Y funciona bien con pantallas anidadas, usted solamente necesitaría un ScreenManager para cada nivel anidado.
La State Machine (Maquina de estado) que hemos configurado arriba tienen el estado predeterminado como Closed (cerrado), por lo que todas las pantallas que utilizan este controller comenzarían como closed (Cerradas). Esa es la razón por la cual en el ScreenManager nosotros proporcionamos una propiedad initiallyOpen, para que usted pueda especificar cuál es la primera pantalla que será mostrada.