Hello everyone, my name is Dmitrii Ivashchenko, and I'm a Software Engineer at MY.GAMES. In this article, we'll talk about developing a user interface in Unity based on states and markup of elements. Intro First of all, it should be noted that we'll be talking in the context of the (uGUI) technology, which is still recommended for Runtime . The approach described is not applicable to , , or other UI building systems. Unity UI according to the documentation UI Toolkit IMGUI Most often in projects, you'll come across UI implementation built on View classes inherited from and peppered with a large number of fields. This approach provides full control over the behavior of the UI, but it also makes it necessary to write a large amount of code at the View and Presenter levels (depending on the architecture used). Unity MonoBehaviour SerializeField Often, as project development continues, these classes swell to incredible sizes, and the components on GameObject themselves are covered with a huge number of links to internal objects: Modifying components like this is also not enjoyable: to get a reference to a new element in a class, you need to add , recompile the code, find the new field in the prefab component, and drag the necessary object into it. As the project grows, the compile time, the number of fields, and the complexity of organizing prefabs also increase in turn. SerializeField As a result, we end up with bulky and overloaded subclasses of (or a large number of small ones, depending on your preference). MonoBehaviour It's also worth considering that any changes to the behavior of such a UI is a task for the programmer, and that task comes with all the associated costs: code review, resolving merge conflicts, code coverage with tests, and so on. I'd like to highlight the implementation of windows with multiple states. I have seen many variations, which can be divided into two approaches: First, any change in the window state occurs . To change the color of text, change an image, play an animation, move an object on the screen — all the involved objects and parameters require a corresponding , and then a large amount of code is written to make it work according to the requirements. Naturally, only a programmer can handle this, and the implementation turns out to be lengthy, expensive, and super-efficient (often much more efficient than anyone can notice). using code SerializeField Another approach can be described as the “ ”. In addition to the View class, an Animator Controller is created and controlled through parameters. A new Animator appears in the new window, and so on, until the FPS when displaying windows begins to drop. all-powerful Animator Now that we've highlighted some of the difficulties of working with uGUI, I would like to talk about a different approach to solving this problem. Stateful UI During my work on one of my pet-projects, I developed a library for structured UI development in Unity. Later, my team and I tested it on production and we were pleased with the results. . The source code for the library is available for download on GitHub Stateful Component The key element of the library is the component. This component is placed on the root GameObject of each screen and contains all the necessary references to internal elements, distributed across tabs: StatefulComponent Each link is named based on its . From a code perspective, the set of roles is a regular . Separate sets of roles are prepared for each type of UI element (buttons, images, texts, etc.): role enum public enum ButtonRole { ... } public enum ImageRole { ... } public enum TextRole { ... } ... Roles are generated directly from the component, and there is no need to manually edit the . Waiting for recompilation when creating a role is also not necessary, as these elements can be used immediately after creation. enum enum To simplify merge conflicts, enumeration values are calculated based on the names of the elements: [StatefulUI.Runtime.RoleAttributes.ButtonRoleAttribute] public enum ButtonRole { Unknown = 0, Start = -1436209294, // -1436209294 == "Start".GetHashCode() Settings = 681682073, Close = -1261564850, Quests = 1375430261, } This allows you to avoid breaking serialized values in prefabs if you and your colleagues happen to simultaneously create new roles for buttons in different branches. Each type of UI element (buttons, texts, images) is located on its own tab: By using roles, the complete markup of all elements inside the prefab is achieved. Sets of are no longer needed to access images and texts, and it is enough to have one reference to and know the role of the desired image in order to, for example, replace its sprite. SerializeField StatefulComponent The types of elements that are currently accessible are: Buttons, Images, Toggles, Sliders, Dropdowns, VideoPlayers, Animators Texts, including and UnityEngine.UI.Text TextMeshProUGUI TextInputs, including and UnityEngine.UI.InputField TMP_InputField Objects — for references to arbitrary objects. There are corresponding methods for working with annotated objects. In the code, you can use a reference to or inherit the class from : 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); } } Texts and Localization The tab with texts, in addition to the role and link to the object, contains the following columns: Code: a text key for localization Localize checkbox: an indicator that the text field is subject to localization Value: the current text content of the object Localized: the current text found by the key from the Code field The library does not include a built-in subsystem for working with translations. To connect your localization system, you'll need to create an implementation of the interface. This can be constructed, for example, based on your Back-end, ScriptableObjects, or 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; } } By clicking on the Copy Localization button, the contents of the Code and Value columns will be copied to the clipboard in a format suitable for pasting into Google Sheets. Internal Components Often, in order to facilitate reuse, separate parts of the UI are extracted into separate prefabs. also allows us to create a hierarchy of components, where each component only works with its own child interface elements. StatefulComponent On the Inner Comps tab, you can assign roles to internal components: Configured roles can be used in code similarly to other types of elements: 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"); Containers To create a list of similar elements, you can use the component. You need to specify the prefab for instantiation and the root object (optional). In Edit-mode, you can add and remove elements using : ContainerView StatefulComponent It's convenient to use for marking up the content of instantiated prefabs. In Runtime, you can use the methods , , or to populate the container: 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); }); If the standard does not suit you for creating objects, you can override this behavior, for example, for instantiation using Zenject: Object.Instantiate() StatefulUiManager.Instance.CustomInstantiateMethod = prefab => { return _diContainer.InstantiatePrefab(prefab); }; Internal Components and Containers provide static and dynamic nesting for , respectively. StatefulComponent We've considered the markup of prefabs, localization, and instantiation. Now it's time to move on to the most interesting part — developing UIs based on states. States We'll consider the concept of state as a named set of changes to a prefab. The name in this case is a role from the enum, and examples of changes to the prefab can be: StateRole Enabling and disabling a GameObject Replacing sprites or materials for Image objects Moving objects on the screen Changing texts and their appearance Playing animations And so on — you can add your own types of object manipulations A set of changes (State Description) can be configured on the States tab. A configured state can be applied directly from the inspector: A configured state can be applied from code using the method: SetState switch (colorScheme) { case ColorScheme.Orange: SetState(StateRole.Orange); break; case ColorScheme.Red: SetState(StateRole.Red); break; case ColorScheme.Purple: SetState(StateRole.Purple); break; } On the Tools tab, when the parameter is enabled, you can configure the State that will be applied immediately upon object instantiation. Apply Initial State On Enable Using states allows for a significant reduction in the amount of code required at the View class level. Simply describe each state of your screen as a set of changes in the and apply the necessary State from the code depending on the game situation. StatefulComponent State Tree Actually, developing a UI based on states is incredibly convenient. So much so that, over time, it leads to another problem — as the project evolves, the list of states for a single window can grow to an unruly length, and thus becomes difficult to navigate. In addition, there are states that only make sense in the context of some other states. To solve this problem, Statful UI has another tool: State Tree. You can access it by clicking on the State Tree Editor button in the States tab. Let's suppose we need to create a reward window for a chest. The window has 3 phases: Animated introduction of the chest (state ) Intro Looping appearance of three different types of rewards from the chest (states , , and , depending on the state, which triggers an animation of the reward appearing from the chest) Money Emoji Cards Reward Display of all awarded rewards in a single list (state ) Results Parent states (in this example ) are applied every time child states are called: Reward Managing a configured comes down to a minimal amount of simple and understandable code that populates the components with necessary data and switches states at the right moment: 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)); } Stateful API & Documentation Roles are intended to provide a convenient and unambiguous way to name links and states for later use in the code. However, there are situations where describing a state would require a name that is too long, and it would be more convenient to leave a small comment about what this link points to, or what behavior the state reflects. For such cases, each link and state in a allows you to add a description: StatefulComponent You may have already noticed the Copy API and Copy Docs buttons on each tab — these copy information for the selected section. In addition to those, there are similar buttons in the Tools tab — these copy information for all sections at once. When you click the Copy API button, the generated code for managing this object will be copied to the clipboard. Here's an example for our rewards window: 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); When you click the Copy Docs button, the documentation for this prefab will be copied to the clipboard in Markdown format: ### 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 It's obvious that making a mistake when implementing this screen with such detailed instructions is quite difficult. You can easily maintain up-to-date information about your UI organization in the project's knowledge base. At the same time, Stateful UI allows for delegating the creation of UI prefabs. In fact, state-based markup allows for fully testing the prefab's behavior before passing it on to programmers. This means that , technical designers, or even separate UI developers can prepare prefabs. Furthermore, since an API is created between the code and prefab, programming and configuring prefabs can be done in parallel! All that is required is to formulate the API in advance. But, even if the task of configuring prefabs remains with programmers, using Stateful UI significantly speeds up this work. game designers Conclusion As we have seen, Stateful UI significantly simplifies working with UI element states. Long cycles are no longer needed to create SerializeFields, recompile code, and search for references amongst a huge number of View class fields. In the View classes themselves, it's no longer necessary to write a large amount of code for repetitive operations such as turning objects on and off or changing text color. The library allows for a consistent approach to organizing layouts in a project, marking objects within prefabs, creating states, linking them to UI elements, and providing an API and documentation for UI management. It also allows for delegating the creation of UI prefabs and speeds up work with them. Moving forward, the project roadmap includes the following items: Expanding the capabilities of States, supporting new types of UI changes in Description, such as new types of animations, playing sounds in states, and so on Adding support for color palettes for coloring text and images Adding support for lists of items with GameObjects reuse Supporting a larger number of Unity UI elements Automating the unloading of added texts for localization Implementing a Test Framework. Since we have exhaustive markup of our prefabs, we can create easy-to-set-up ScriptableObject-based scenarios in the following format: Click the button ButtonRole.Settings Check the text in to be equal to "some value" TextRole.SomeText Check the image in to ensure it is equal to a certain sprite ImageRole.SomeImage A tutorial system. Similarly to testing, marked layout allows creating ScriptableObject-based tutorial scenarios in the form of instructions like "Show pointer on the button ". ButtonRole.UpgradeHero . You're welcome to create issues or contribute to the library! The project source code is available on GitHub