I noticed other people using AddListener to add onClick listeners for Unity UI buttons, so I tried doing the same. I also noticed that hiding/showing my multiple menu GameObjects was causing the listeners to stop functioning.

What went wrong? What’s the real difference between setting the listeners on the inspector vs in code?

From what I found the main difference between connecting OnClick in Unity inspector vs code is how it is set up: statically vs dynamically. Dynamically setting at runtime supposedly has some advantages in that things can be done dynamically (haha) all the normal runtime advantages of being able to control logic of how and when the listeners are attached for example. Supposedly this comes with the disadvantage of possibly leaking memory when you do not unregister the listeners.

Since we need to clean up after ourselves, it’s recommended to set these listeners up on OnEnable and OnDisable to have a balanced init/cleanup lifecycle. However, this didn’t really seem to fix the issue at all.

This Unity discussion suggests enabling/disabling the Canvas component itself rather than the entire game object:

CanvasObject.GetComponent<Canvas> ().enabled = false;

this is to avoid null references. This would also require us to restructure our menus into multiple canvases.

I got the original idea for view swapping using a single canvas with multiple game object children (not sub-canvases) from this FishNet multiplayer youtube video.

Which uses a simple loop to disable/enable game objects, not canvases:

public class UiManager : MonoBehaviour
{
    public static UiManager Instance { get; private set; }

    [SerializeField] View[] views;

    void Awake()
    {
        Instance = this;
    }

    public void Init()
    {
        foreach (var view in views)
        {
            view.Init();
        }
    }

    public void Show<T>() where T: View
    {
        foreach (var view in views)
        {
            view.gameObject.SetActive(view is T);
        }
    }
}

but makes use of its own Init() method which is called the very first time the singleton is used:

UiManage.Instance.Init()

which serves to bootstrap the subviews after they are instantiated by the engine.

A couple of different Unite talks discuss advantages of splitting up the UI into multiple canvases:

  1. Unite Europe 2017
  2. Unite 2016

So it seems like using multiple canvases is probably the way to go. Unity has a post about Optimization Tips For Unity UI which also mentions splitting UI into multiple canvases. Changing a single element on your canvas causes it to become “dirtied” which will cause a redraw that requires recalculations.

Fix Part 2: Reassigning the EventSystem’s action asset

Apparently when enabling/disabling PlayerInput components, it’s necessary to reassign the EventSystem’s actionAsset field, which I gave its own helper method:

void FixEventSystem()  
{  
    var eventSystem = FindObjectOfType<InputSystemUIInputModule>();  
    var player1Input = player1SelectCard.GetComponent<PlayerInput>();  
    eventSystem.actionsAsset = player1Input.actions;  
}

You can tell this is happening by checking the PlayerInput component in the view hierarchy at runtime:

Clicking the Fix UI Input Module has the same effect as the above code.

Helpful Resources

  1. Unity thread on programmatic vs inspector listeners
  2. Unity GameObject life cycle diagram (2023.3)
  3. Optimization Tips For Unity UI