Hola a todos, mi nombre es Dmitrii Ivashchenko y soy ingeniero de software en MY.GAMES. En este artículo, hablaremos sobre el desarrollo de una interfaz de usuario en Unity basada en estados y marcado de elementos. Introducción En primer lugar, debe tenerse en cuenta que hablaremos en el contexto de la tecnología (uGUI), que aún se recomienda para Runtime . El enfoque descrito no se aplica a , u otros sistemas de creación de UI. Unity UI según la documentación UI Toolkit IMGUI La mayoría de las veces, en los proyectos , se encontrará con la implementación de la interfaz de usuario basada en las clases View heredadas de y salpicada con una gran cantidad de campos . Este enfoque proporciona un control total sobre el comportamiento de la interfaz de usuario, pero también hace que sea necesario escribir una gran cantidad de código en los niveles de Vista y Presentador (según la arquitectura utilizada). de Unity MonoBehaviour SerializeField A menudo, a medida que continúa el desarrollo del proyecto, estas clases aumentan a tamaños increíbles, y los componentes de GameObject en sí mismos están cubiertos con una gran cantidad de enlaces a objetos internos: Modificar componentes como este tampoco es divertido: para obtener una referencia a un nuevo elemento en una clase, debe agregar , volver a compilar el código, encontrar el nuevo campo en el componente prefabricado y arrastrar el objeto necesario hacia él. A medida que crece el proyecto, el tiempo de compilación, la cantidad de campos y la complejidad de organizar prefabricados también aumentan a su vez. SerializeField Como resultado, terminamos con subclases voluminosas y sobrecargadas de (o una gran cantidad de subclases pequeñas, según sus preferencias). MonoBehaviour También vale la pena considerar que cualquier cambio en el comportamiento de dicha interfaz de usuario es una tarea para el programador, y esa tarea conlleva todos los costos asociados: revisión de código, resolución de conflictos de fusión, cobertura de código con pruebas, etc. Me gustaría destacar la implementación de ventanas con múltiples estados. He visto muchas variaciones, que se pueden dividir en dos enfoques: Primero, cualquier cambio en el estado de la ventana ocurre . Para cambiar el color del texto, cambiar una imagen, reproducir una animación, mover un objeto en la pantalla: todos los objetos y parámetros involucrados requieren un correspondiente, y luego se escribe una gran cantidad de código para que funcione de acuerdo con los requisitos. . Naturalmente, solo un programador puede manejar esto, y la implementación resulta ser larga, costosa y súper eficiente (a menudo mucho más eficiente de lo que nadie puede notar). usando el código SerializeField Otro enfoque puede describirse como el “ ”. Además de la clase View, se crea y controla un Animator Controller a través de parámetros. Aparece un nuevo animador en la nueva ventana, y así sucesivamente, hasta que el FPS cuando se muestran las ventanas comienza a disminuir. animador todopoderoso Ahora que hemos resaltado algunas de las dificultades de trabajar con uGUI, me gustaría hablar sobre un enfoque diferente para resolver este problema. IU con estado Durante mi trabajo en uno de mis proyectos favoritos, desarrollé una biblioteca para el desarrollo de una interfaz de usuario estructurada en Unity. Más tarde, mi equipo y yo lo probamos en producción y quedamos satisfechos con los resultados. . El código fuente de la biblioteca está disponible para su descarga en GitHub Componente con estado El elemento clave de la biblioteca es el componente . Este componente se ubica en el GameObject raíz de cada pantalla y contiene todas las referencias necesarias a los elementos internos, distribuidas en pestañas: StatefulComponent Cada enlace recibe un nombre en función de su . Desde la perspectiva del código, el conjunto de roles es una regular. Se preparan conjuntos separados de roles para cada tipo de elemento de la interfaz de usuario (botones, imágenes, textos, etc.): función enum public enum ButtonRole { ... } public enum ImageRole { ... } public enum TextRole { ... } ... Los roles se generan directamente desde el componente y no es necesario editar manualmente la . Tampoco es necesario esperar la recompilación al crear un rol, ya que estos elementos se pueden usar inmediatamente después de la creación. enum enum Para simplificar los conflictos de combinación, los valores de enumeración se calculan en función de los nombres de los elementos: [StatefulUI.Runtime.RoleAttributes.ButtonRoleAttribute] public enum ButtonRole { Unknown = 0, Start = -1436209294, // -1436209294 == "Start".GetHashCode() Settings = 681682073, Close = -1261564850, Quests = 1375430261, } Esto le permite evitar romper valores serializados en prefabricados si usted y sus colegas crean simultáneamente nuevos roles para botones en diferentes ramas. Cada tipo de elemento de la interfaz de usuario (botones, textos, imágenes) se encuentra en su propia pestaña: Mediante el uso de roles, se logra el marcado completo de todos los elementos dentro de la casa prefabricada. Ya no se necesitan conjuntos de para acceder a imágenes y textos, y basta con tener una referencia a y saber el rol de la imagen deseada para, por ejemplo, reemplazar su sprite. SerializeField StatefulComponent Los tipos de elementos a los que se puede acceder actualmente son: Botones, imágenes, conmutadores, controles deslizantes, menús desplegables, reproductores de video, animadores Textos, incluidos y UnityEngine.UI.Text TextMeshProUGUI TextInputs, incluidos y UnityEngine.UI.InputField TMP_InputField Objetos: para referencias a objetos arbitrarios. Existen métodos correspondientes para trabajar con objetos anotados. En el código, puede usar una referencia a o heredar la clase de : StatefulComponent StatefulView public class ExamplePresenter { private StatefulComponent _view; public void OnOpen() { _view.GetButton(ButtonRole.Settings).onClick.AddListener(OnSettingsClicked); _view.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); _view.GetSlider(SliderRole.Volume).onValueChanged.AddListener(OnVolumeChanged); } } public class ExampleScreen : StatefulView { private void Start() { SetText(TextRole.Title, "Hello World"); SetTextValues(TextRole.Timer, hours, minutes, seconds); SetImage(ImageRole.UserAvatar, avatarSprite); } } Textos y Localización La pestaña con textos, además del rol y enlace al objeto, contiene las siguientes columnas: Código: una clave de texto para la localización Casilla de verificación Localizar: un indicador de que el campo de texto está sujeto a localización Valor: el contenido de texto actual del objeto Localizado: el texto actual encontrado por la clave del campo Código La biblioteca no incluye un subsistema incorporado para trabajar con traducciones. Para conectar su sistema de localización, deberá crear una implementación de la interfaz . Esto se puede construir, por ejemplo, en función de su back-end, ScriptableObjects o Google Sheets. ILocalizationProvider public class HardcodeLocalizationProvider : ILocalizationProvider { private Dictionary<string, string> _dictionary = new Dictionary<string, string> { { "timer" , "{0}h {1}m {2}s" }, { "title" , "Título do Jogo" }, { "description" , "Descrição longa do jogo" }, }; public string GetPhrase(string key, string defaultValue) { return _dictionary.TryGetValue(key, out var value) ? value : defaultValue; } } Al hacer clic en el botón Copiar localización, el contenido de las columnas Código y Valor se copiará en el portapapeles en un formato adecuado para pegarlo en Hojas de cálculo de Google. Componentes internos A menudo, para facilitar la reutilización, se extraen partes separadas de la interfaz de usuario en prefabricados separados. también nos permite crear una jerarquía de componentes, donde cada componente solo funciona con sus propios elementos de interfaz secundarios. StatefulComponent En la pestaña Componentes internos, puede asignar funciones a los componentes internos: Los roles configurados se pueden usar en el código de manera similar a otros tipos de elementos: var header = GetInnerComponent(InnerComponentRole.Header); header.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); header.SetText(TextRole.Title, "Header Title"); var footer = GetInnerComponent(InnerComponentRole.Footer); footer.GetButton(ButtonRole.Continue).onClick.AddListener(OnContinueClicked); footer.SetText(TextRole.Message, "Footer Message"); Contenedores Para crear una lista de elementos similares, puede utilizar el componente . Debe especificar el prefabricado para la creación de instancias y el objeto raíz (opcional). En el modo de edición, puede agregar y eliminar elementos usando : ContainerView StatefulComponent Es conveniente usar para marcar el contenido de los prefabricados instanciados. En Runtime, puede usar los métodos , o para llenar el contenedor: StatefulComponent AddInstance<T> AddStatefulComponent FillWithItems var container = GetContainer(ContainerRole.Players); container.Clear(); container.FillWithItems(_player, (StatefulComponent view, PlayerData data) => { view.SetText(TextRole.Name, data.Name); view.SetText(TextRole.Level, data.Level); view.SetImage(ImageRole.Avatar, data.Avatar); }); Si el estándar no le conviene para crear objetos, puede anular este comportamiento, por ejemplo, para la creación de instancias con Zenject: Object.Instantiate() StatefulUiManager.Instance.CustomInstantiateMethod = prefab => { return _diContainer.InstantiatePrefab(prefab); }; Los componentes internos y los contenedores proporcionan anidamiento estático y dinámico para , respectivamente. StatefulComponent Hemos considerado el marcado de los prefabricados, la localización y la creación de instancias. Ahora es el momento de pasar a la parte más interesante: desarrollar interfaces de usuario basadas en estados. estados Consideraremos el concepto de estado como un conjunto de cambios con nombre en una casa prefabricada. El nombre en este caso es un rol de la enumeración , y los ejemplos de cambios en el prefabricado pueden ser: StateRole Habilitar y deshabilitar un GameObject Reemplazo de sprites o materiales por objetos de Imagen Mover objetos en la pantalla Cambio de textos y su apariencia. Reproducir animaciones Y así sucesivamente: puede agregar sus propios tipos de manipulaciones de objetos Se puede configurar un conjunto de cambios (Descripción del estado) en la pestaña Estados. Un estado configurado se puede aplicar directamente desde el inspector: Se puede aplicar un estado configurado desde el código usando el método : SetState switch (colorScheme) { case ColorScheme.Orange: SetState(StateRole.Orange); break; case ColorScheme.Red: SetState(StateRole.Red); break; case ColorScheme.Purple: SetState(StateRole.Purple); break; } En la pestaña Herramientas, cuando el parámetro está habilitado, puede configurar el estado que se aplicará inmediatamente después de la instanciación del objeto. Aplicar estado inicial al habilitar El uso de estados permite una reducción significativa en la cantidad de código requerido en el nivel de clase de Vista. Simplemente describa cada estado de su pantalla como un conjunto de cambios en y aplique el estado necesario del código según la situación del juego. StatefulComponent Árbol de estado En realidad, desarrollar una interfaz de usuario basada en estados es increíblemente conveniente. Tanto es así que, con el tiempo, conduce a otro problema: a medida que el proyecto evoluciona, la lista de estados para una sola ventana puede crecer hasta una longitud ingobernable y, por lo tanto, se vuelve difícil de navegar. Además, hay estados que solo tienen sentido en el contexto de algunos otros estados. Para resolver este problema, Statful UI tiene otra herramienta: State Tree. Puede acceder a él haciendo clic en el botón Editor de árbol de estado en la pestaña Estados. Supongamos que necesitamos crear una ventana de recompensa para un cofre. La ventana tiene 3 fases: Introducción animada del cofre (estado ) Intro Aparición en bucle de tres tipos diferentes de recompensas del cofre (indica , y , según el estado , lo que activa una animación de la recompensa que aparece en el cofre) Dinero Emoji Tarjetas de la Recompensa Visualización de todas las recompensas otorgadas en una sola lista ( estatales) resultados Los estados principales (en este ejemplo ) se aplican cada vez que se llama a los estados secundarios: , Recompensa Administrar un configurado se reduce a una cantidad mínima de código simple y comprensible que llena los componentes con los datos necesarios y cambia de estado en el momento adecuado: StatefulComponent public void ShowIntro() { SetState(StateRole.Intro); } public void ShowReward(IReward reward) { // Update the inner view with the reward reward.UpdateView(GetInnerComponent(InnerComponentRole.Reward)); // Switch on the type of reward switch (reward) { case ICardsReward cardsReward: SetState(StateRole.Cards); break; case IMoneyReward moneyReward: SetState(StateRole.Money); break; case IEmojiReward emojiReward: SetState(StateRole.Emoji); break; } } public void ShowResults(IEnumerable<IReward> rewards) { SetState(StateRole.Results); // Fill the container with the rewards GetContainer(ContainerRole.TotalReward) .FillWithItems(rewards, (view, reward) => reward.UpdateView(view)); } API con estado y documentación Los roles están destinados a proporcionar una forma conveniente e inequívoca de nombrar enlaces y estados para su uso posterior en el código. Sin embargo, hay situaciones en las que describir un estado requeriría un nombre demasiado largo y sería más conveniente dejar un pequeño comentario sobre a qué apunta este enlace o qué comportamiento refleja el estado. Para tales casos, cada enlace y estado en un le permite agregar una descripción: StatefulComponent Es posible que ya haya notado los botones Copiar API y Copiar documentos en cada pestaña; estos copian información para la sección seleccionada. Además de esos, hay botones similares en la pestaña Herramientas: estos copian información para todas las secciones a la vez. Al hacer clic en el botón Copiar API, el código generado para administrar este objeto se copiará en el portapapeles. Aquí hay un ejemplo para nuestra ventana de recompensas: StatfulComponent // Insert the name of the chest here SetText(TextRole.Title, "Lootbox"); // Button to proceed to the reward issuance phase GetButton(ButtonRole.Overlay); // Button to display information about the card GetButton(ButtonRole.Info); // Container for displaying the complete list of awarded rewards GetContainer(ContainerRole.TotalReward); // Insert the card image here SetImage(ImageRole.Avatar, null); // Animated appearance of a chest SetState(StateRole.Intro); Al hacer clic en el botón Copiar documentos, la documentación de este prefabricado se copiará en el portapapeles en formato Markdown: ### RewardScreen Buttons: - Overlay - Button to proceed to the reward issuance phase - Info - Button to display information about the card Texts: - Title - Insert the name of the chest here Containers: - TotalReward - Container for displaying the complete list of awarded rewards Images: - Avatar - Insert the card image here States: - Intro - Animated appearance of a chest - Cards - Displaying rewards in the form of a card - Money - Displaying rewards in the form of game currency - Emoji - Displaying rewards in the form of an emoji - Results - Displaying a complete list of issued rewards Es obvio que cometer un error al implementar esta pantalla con instrucciones tan detalladas es bastante difícil. Puede mantener fácilmente información actualizada sobre su organización de interfaz de usuario en la base de conocimientos del proyecto. Al mismo tiempo, Stateful UI permite delegar la creación de prefabricados de UI. De hecho, el marcado basado en el estado permite probar completamente el comportamiento del prefabricado antes de pasarlo a los programadores. Esto significa que , los diseñadores técnicos o incluso los desarrolladores de UI independientes pueden preparar prefabricados. Además, dado que se crea una API entre el código y el prefabricado, ¡la programación y la configuración de los prefabricados se pueden hacer en paralelo! Todo lo que se requiere es formular la API por adelantado. Pero, incluso si la tarea de configurar prefabricados sigue siendo de los programadores, el uso de Stateful UI acelera significativamente este trabajo. los diseñadores de juegos Conclusión Como hemos visto, Stateful UI simplifica significativamente el trabajo con los estados de los elementos de la UI. Ya no se necesitan ciclos largos para crear SerializeFields, recompilar código y buscar referencias entre una gran cantidad de campos de clase View. En las propias clases de Vista, ya no es necesario escribir una gran cantidad de código para operaciones repetitivas, como encender y apagar objetos o cambiar el color del texto. La biblioteca permite un enfoque coherente para organizar diseños en un proyecto, marcar objetos dentro de prefabricados, crear estados, vincularlos a elementos de la interfaz de usuario y proporcionar una API y documentación para la gestión de la interfaz de usuario. También permite delegar la creación de prefabricados de interfaz de usuario y acelera el trabajo con ellos. En el futuro, la hoja de ruta del proyecto incluye los siguientes elementos: Expandir las capacidades de los estados, admitir nuevos tipos de cambios en la interfaz de usuario en la descripción, como nuevos tipos de animaciones, reproducir sonidos en los estados, etc. Agregar soporte para paletas de colores para colorear texto e imágenes Agregar soporte para listas de elementos con la reutilización de GameObjects Compatibilidad con una mayor cantidad de elementos de la interfaz de usuario de Unity Automatización de la descarga de textos añadidos para su localización Implementación de un marco de prueba. Dado que tenemos un marcado exhaustivo de nuestros prefabricados, podemos crear escenarios basados en ScriptableObject fáciles de configurar en el siguiente formato: Haga clic en el botón ButtonRole.Settings Verifique que el texto en sea igual a "algún valor" TextRole.SomeText Verifique la imagen en para asegurarse de que sea igual a un cierto sprite ImageRole.SomeImage Un sistema tutorial. De manera similar a las pruebas, el diseño marcado permite crear escenarios de tutoriales basados en ScriptableObject en forma de instrucciones como "Mostrar puntero en el botón ". ButtonRole.UpgradeHero . ¡Le invitamos a crear problemas o contribuir a la biblioteca! El código fuente del proyecto está disponible en GitHub