Friends, food, and flourishing

Unity UI code structure

Another in my remind-myself-how-to-do-things-in-Unity series. This one is about handling UI display, with the conditions:

  • Works with Easy Character Movement 2 (ECM2), a character controller asset.
  • Works with multiple devices. In this case, keyboard/mouse, and gamepad.
  • Avoids mutual dependencies, where game object (GO) A is aware of GO B, and GO B is aware of GO A. One-way dependencies are better.
 
Here’s a video of what I wanted to happen:

Game objects and components

Here’s the hierarchy:

Hierarchy

(The strange stuff on the right is added by Inspector Gadgets Pro.)

The first GO is a prefab from ECM2. Nothing customized.

The last three have custom stuff. The cube has a simple script to write text on one of its faces.

The PlayerController has:

Player controller

Player Input is part of the “new” input system (it’s not really new anymore). The most important thing is its input action asset.

Input action asset

It’s copied from a standard Unity one (DefaultInputActions). DefaultInputActions has two maps, one for player movement, and the other for UI. After copying DefaultInputActions to my own folder, I deleted the movement map, leaving the UI bit.

I added the last two items. Ignore the Test one. ToggleMenu is bound to a button on a gamepad, and the Tab key.

Here are the GOs for the UI:

UI GOs

ClickBlockingPanel covers the entire screen, and blocks clicks from getting through to the game world when the UI is showing.

The others contain the two menus. MainMenuManager has no visible components, just a script, MainMenuController. The widget container underneath it has the visual stuff. I do this to conveniently change UI visibility. MainMenuManager is always active, so its Awake and Start methods run predictably when the game starts. The widget container can be turned on and off as desired, without having Awakes and Starts running at unexpected times.

(In the screenshot, the widget containers are active, to make them easy to see. When the program starts, they are both deactivated.)

OptionsManager has the script OptionsController, using the same widget container pattern. Yeah, I know it doesn’t look like an options menu, but just go with it. It works for my purposes.

The widget containers have buttons in them, tied to methods of MainMenuManager and OptionsManager, respectively. They use strange colors for button states, so they’d be obvious:

Button state colors

The bottom GO is a cube, with a canvas in world space attached.

Cube with world-space canvas and text widget

There’s a TMP_Text on a GO on the canvas, to show messages on the cube.

Code

There are six scripts. The simplest is attached to the cube, to show text:

				
					using TMPro;
using UnityEngine;

namespace Game.Scripts
{
    /// <summary>
    /// Writes text on a TMP_Text GO attached to a cube.
    /// </summary>
    public class CubeTextController : MonoBehaviour
    {
        private TMP_Text _message;
        
        private void Awake()
        {
            // Cache the text component.
            _message = transform.GetComponentInChildren<TMP_Text>();
        }

        private void Start()
        {
            // Show this text when the game starts.
            ShowText("(Nothing)");
        }

        /// <summary>
        /// Show text.
        /// </summary>
        /// <param name="messageIn">The text to show.</param>
        public void ShowText(string messageIn)
        {
            _message.text = messageIn;
        }
    }
}
				
			

The rest of the scripts are:

  • PlayerController: shows and hides menus, switches input context (more later).
  • MainMenuController: main menu actions
  • OptionsController: second menu options
  • UiDisplayBase: base class for the previous two
  • Utils: a static class with useful stuff

Here are the references (solid lines) and inheritance (dashed lines) between the classes (just a casual drawing):

wpg_div_wp_graphviz_1


There are no cycles. Yay!

Let’s start with PlayerController. It does two things:

  • Manages the showing and hiding of menus
  • Switching input context

Menus are shown and hidden by either the player directly pressing a button or key, or indirectly by a method somewhere (in this game’s case, one menu opening another menu).

I don’t know if “input context” is a real term or not, but it’s useful. There are two input contexts here:

  • World: when the player’s character is running around the world, there is no mouse cursor, no menus, and navigation inputs (WASD, DPad, joysticks, etc) move the character around.
  • UI: there’s a mouse cursor, menus, and the navigation inputs move the focus on the menu. The player’s character does not move in response to input.

PlayerController makes sure the game uses the UI context when any menu is showing, or the world context otherwise.

				
					using InspectorGadgets.Attributes;
using UnityEngine;

namespace Game.Scripts
{
    /// <summary>
    /// This class manages input not managed by the ECM2 character.
    /// </summary>
    public class PlayerController : MonoBehaviour
    {
        // The menu controllers.
        public MainMenuController mainMenuController;
        public OptionsController optionsController;
        
        // The panel blocking clicks on the UI background.
        [Required]
        public GameObject blockingPanel;

        // Is any menu UI showing?
        private bool IsAnyUiShowing => mainMenuController.IsUiShowing || optionsController.IsUiShowing;

        private void Awake()
        {
            // Subscribe to events from the menus.
            mainMenuController.uiDisplayShownSubscribers += UiShown;
            mainMenuController.uiDisplayHiddenSubscribers += UiHidden;
            mainMenuController.showOptionButtonSubscribers += ShowOptionsTriggered;
            optionsController.uiDisplayShownSubscribers += UiShown;
            optionsController.uiDisplayHiddenSubscribers += UiHidden;
        }

        void Start()
        {
            // Show the main menu to start.
            optionsController.HideUi();
            mainMenuController.ShowUi();
        }
    
        /// <summary>
        /// The Options button on the main menu was pressed.
        /// </summary>
        public void ShowOptionsTriggered()
        {
            mainMenuController.HideUi();
            optionsController.ShowUi();
        }

        /// <summary>
        /// A UI was shown.
        /// </summary>
        /// <param name="uiDisplayBase"></param>
        public void UiShown(UiDisplayBase uiDisplayBase)
        {
            Utils.SwitchToUiInteraction();
            blockingPanel.SetActive(true);
        }

        /// <summary>
        /// A UI was hidden.
        /// </summary>
        /// <param name="uiDisplayBase"></param>
        public void UiHidden(UiDisplayBase uiDisplayBase)
        {
            if (!IsAnyUiShowing)
            {
                blockingPanel.SetActive(false);
                Utils.SwitchToWorldInteraction();
            }
        }
        
        /// <summary>
        /// Player wants to see/hide the main menu. 
        /// </summary>
        public void OnToggleMenu()
        {
            if (!IsAnyUiShowing)
            {
                // Nothing showing, so show the main menu.
                mainMenuController.ShowUi();
            }
            else
            {
                // Hide any menus showing.
                mainMenuController.HideUi();
                optionsController.HideUi();
            }
        }

    }
}

				
			

The class is aware of the two menus, the blocking panel, and the utilities class. It communicates with them mainly through events, so the menu controllers do not depend on the PlayerController.

PlayerController calls the menus’ ShowUi and HideUi methods in Start. When the player clicks the main menu’s Options button, PlayerController handles menu switching, in the ShowOptionsTriggered method.

PlayerController watches for any UI showing itself, by registering for events. It then runs UiShown. which changes the input context.

UiHidden is run when any menu is hidden. If no menus are showing, it switches to the world input context. 

The input system runs OnToggleMenu when the player presses a menu button on the keyboard or gamepad. The method shows/hides the UI.

OnToggleMenu doesn’t switch input contexts itself. Remember that PlayerController is watching UI show and hide events. When PlayerController.OnToggleMenu runs, a UI is shown or hidden, PlayerController sees that, and sets the input context.

Let’s look at Utils.

				
					using EasyCharacterMovement;
using UnityEngine;

namespace Game.Scripts
{
    /// <summary>
    /// Useful Stuff.
    /// </summary>
    public static class Utils
    {
        // An ECM2 component.
        private static FirstPersonCharacter _firstPersonCharacter;
        
        /// <summary>
        /// Get an ECM2 component, finding and caching it if it isn't known. 
        /// </summary>
        private static FirstPersonCharacter FirstPersonCharacter
        {
            get
            {
                if (_firstPersonCharacter == null)
                {
                    _firstPersonCharacter = Object.FindObjectOfType<FirstPersonCharacter>();
                }
                return _firstPersonCharacter;
            }
        }

        /// <summary>
        /// Is the game in UI input context?
        /// </summary>
        /// <returns>True if the game is in UI input context</returns>
        private static bool IsUsingUiInteraction()
        {
            return Cursor.visible;
        }

        /// <summary>
        /// Is the game in world (game play, not UI) input context?
        /// </summary>
        /// <returns>True if the game is in world input context</returns>
        private static bool IsUsingWorldInteraction()
        {
            return !Cursor.visible;
        }
            
        /// <summary>
        /// Switch to UI input context.
        /// </summary>
        public static void SwitchToUiInteraction()
        {
            if (!IsUsingUiInteraction())
            {
                // Unlock mouse cursor.
                Cursor.visible = true;
                Cursor.lockState = CursorLockMode.None;
                // Lock the player's FP GO.
                FirstPersonCharacter.GetCharacterLook().lockCursor = false;
                FirstPersonCharacter.enabled = false;
                FirstPersonCharacter.SetMovementMode(MovementMode.None);
            }
        }

        public static void SwitchToWorldInteraction()
        {
            if (!IsUsingWorldInteraction())
            {
                // Lock mouse cursor.
                Cursor.visible = false;
                Cursor.lockState = CursorLockMode.Locked;
                // Unlock the player's FP GO.
                FirstPersonCharacter.GetCharacterLook().lockCursor = true;
                FirstPersonCharacter.enabled = true;
                FirstPersonCharacter.SetMovementMode(MovementMode.Walking);
            }
        }

    }
}
				
			

Utils does one thing: switch between the two interaction states, UI or game play. This class should be called something else, now that I think about it.

The other three programs are for the menus. First, there’s a base class for the main and options menu, since most of their code is the same.

				
					using UnityEngine;
using UnityEngine.UI;

namespace Game.Scripts
{
    /// <summary>
    /// Base class for both menus.
    /// </summary>
    public abstract class UiDisplayBase : MonoBehaviour
    {
        // The visible UI is in the widget container.
        public GameObject widgetContainer;
        
        // What button should be selected initially?
        public Button initiallySelectedButton;
        
        // Other objects can ask to be informed when this UI is shown.
        public delegate void UiDisplayHiddenSubscribers(UiDisplayBase uiDisplayBase);
        public event UiDisplayHiddenSubscribers uiDisplayHiddenSubscribers;    

        // Other objects can ask to be informed when this UI is hidden
        public delegate void UiDisplayShownSubscribers(UiDisplayBase uiDisplayBase);
        public event UiDisplayShownSubscribers uiDisplayShownSubscribers;    

        // Is the UI showing?
        public bool IsUiShowing => widgetContainer.activeSelf;

        /// <summary>
        /// Show this UI.
        /// </summary>
        public void ShowUi()
        {
            if (!IsUiShowing)
            {
                // Show the widgets.
                widgetContainer.SetActive(true);
                // Select one of the buttons.
                initiallySelectedButton.Select();
                // Tell watchers about it.
                uiDisplayShownSubscribers?.Invoke(this);
            }
        }

        public virtual void HideUi()
        {
            if (IsUiShowing)
            {
                // Hide the widgets.
                widgetContainer.SetActive(false);
                // Tell watchers about it.
                uiDisplayHiddenSubscribers?.Invoke(this);
            }
        }

    }
}
				
			

All the class does is show and hide its widgets, and tells watchers about it.

Here’s the main menu controller. The menu looks like this, with selection visualization shown:

Main menu

Close is linked to a method in the base class. MainMenuController handles the doggos and options buttons. It handles the doggo button directly. When the options button is pressed, the class tells watchers about it.

				
					using UnityEngine;

namespace Game.Scripts
{
    /// <summary>
    /// Handles player actions on the main menu.
    /// </summary>
    public class MainMenuController : UiDisplayBase
    {
        [SerializeField]
        private CubeTextController cubeTextController; 
        
        public delegate void ShowOptionButtonSubscribers();
        // ReSharper disable once InconsistentNaming
        public event ShowOptionButtonSubscribers showOptionButtonSubscribers;    

        /// <summary>
        /// Doggos are wonderful!
        /// </summary>
        public void DoggosButtonPressed()
        {
            cubeTextController.ShowText("Doggos rock!");
        }

        /// <summary>
        /// Tell watchers the player wants to see the options menu.
        /// </summary>
        public void ShowOptions()
        {
            showOptionButtonSubscribers?.Invoke();
        }
        
    }
}

				
			

<BTW>

Here, the base class UiDisplayBase does the Invoke. Say you want to override the method with the Invoke, and have the child class do the Invoke instead. You can’t. C# only allows Invokes in the class that declares the delegate.

Never fear! There’s an easy solution: make a new protected method in the base class. The method just does the Invoke. Call the method in the child class. I got this from a Microsoft tip.

</BTW>

The last program is OptionsController. Here’s that menu:

Options menu

				
					using UnityEngine;

namespace Game.Scripts
{
    /// <summary>
    /// Handles player actions on the options menu.
    /// </summary>
    public class OptionsController : UiDisplayBase
    {
        [SerializeField]
        private CubeTextController cubeTextController; 

        /// <summary>
        /// Goattos are cool!
        /// </summary>
        public void GoattosButtonPressed()
        {
            cubeTextController.ShowText("Goattos are cool!");
        }
    }
}
				
			

There’s not much to this one. It knows about CubeTextController, and nothing else.

Ack! A race condition

A problem came up, though. When the game started, you’d get the main menu, but no cursor. I noodled this might be because the ECM2 GO initialized after PlayerController, and turned the cursor off during its initialization.

So, I made sure PlayerController ran after the rest:

Script execution order

ECM2 isn’t listed, so it runs somewhere in Default Time.

It makes me happy

A few dozen lines of code makes a nice UI management system for a Unity game. The classes are simple, decoupled, and do what I want.

Huzzah!

Leave a Reply

Your email address will not be published. Required fields are marked *

css.php