Unity6 Game Development · Lesson 17

uGUI buttons, labels and listener lifetime

Install Unity UI and configure the EventSystem for the project’s input backend.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

The UI pipeline has several independent boundaries

A visible button is only one part of an interactive screen. This lesson uses uGUI from com.unity.ugui, with UnityEngine.UI.Button and the legacy UnityEngine.UI.Text component. It does not use UI Toolkit or automatically accept a label from a different text package. A Canvas needs its raycasting path, and an EventSystem needs an input module matching the project's configured backend. Treat package availability, assigned references, pointer delivery and the button's gameplay listener as separate boundaries when investigating a screen that looks correct but does not respond.

CounterPanel keeps the numerical rule small and makes listener ownership explicit. Awake validates the two serialized references; OnEnable attaches its named AddOne method and renders the current count; OnDisable removes that runtime listener. The count belongs to the panel instance and is not reset merely because it is disabled. Re-enabling therefore renders the existing value. This arrangement lets another button listener or presentation system coexist without requiring the panel to remove every callback owned by someone else or assume that showing a screen creates a brand-new counter.

Portable C# companion: count and label

The SharpRune companion instantiates an ordinary CounterModel. Its initial integer value is zero, so Label first prints Count: 0. Each Add call increments once, producing Count: 1 and Count: 2 on the next two lines. Invariant formatting stabilizes the console representation. This trace checks model state and derived text only: there is no Canvas, EventSystem, pointer event or Unity Button involved. If this companion passes while the Editor button does nothing, changing the model's addition logic is unlikely to repair a missing raycaster or mismatched input module.

var model = new CounterModel(); Console.WriteLine(model.Label); model.Add(); Console.WriteLine(model.Label); model.Add(); Console.WriteLine(model.Label);
sealed class CounterModel { private int count; public string Label => "Count: " + count.ToString(CultureInfo.InvariantCulture); public void Add() { count++; } }

Unity Editor: subscribe while the panel is enabled

Use CounterPanel.cs in the Unity Editor with explicitly assigned uGUI Button and Text references. In an Input System-only project, configure InputSystemUIInputModule and its required actions rather than leaving an incompatible legacy module. The excerpt below shows the enabled-lifetime subscription and rendering path. Click twice, then disable and re-enable the panel and click once more. With the original state retained, the next value is three. This is an engine integration observation that must be performed in the actual configured UI hierarchy, separate from the portable model's three printed lines.

private void OnEnable()
    {
        if (addButton == null || countLabel == null) return;
        addButton.onClick.AddListener(AddOne);
        Render();
    }
    private void OnDisable() { if (addButton != null) addButton.onClick.RemoveListener(AddOne); }
    private void AddOne() { count++; Render(); }
    private void Render() { countLabel.text = "Count: " + count; }

Listener cleanup is not input configuration

Do not diagnose every extra update as identical-listener duplication. UnityEvent documents that adding an identical runtime listener repeatedly results in one call to that listener; newly created closures or different targets can still be distinct listeners. Pairing ownership remains valuable because a disabled panel should not retain an unwanted callback. RemoveListener removes runtime listeners, not persistent Inspector callbacks. Avoid RemoveAllListeners as a casual cleanup shortcut, since the panel does not own other systems' runtime registrations. Also inspect overlapping raycast targets, interactability and focus before blaming a valid listener method.

Two independent UI diagnosis exercises

First, independently test the listener lifetime by reaching Count: 2, disabling the panel, re-enabling it and clicking once. Predict Count: 3 with no reset. Hint: OnEnable calls Render but never assigns count, so rendering existing state is different from constructing a new CounterModel. Repeat the cycle several times and record increments. Inspect the Button's Inspector callbacks separately so an intentionally configured second gameplay action is not mistaken for a subscription leak or removed by a cleanup routine that does not own it.

Second, in a disposable UI scene, leave the model and listener intact while deliberately assigning the wrong EventSystem input module for the active backend. Diagnose the stopped click pipeline in order: package configuration, Game-view focus, active EventSystem, module actions, raycasting and interactability. Hint: a direct call to AddOne can establish that rendering works without proving that a real pointer event reaches the button. Restore the correct module and repeat an actual click. This experiment tests wiring independently of count retention and does not require changing the arithmetic rule.

Review model, rendering and click delivery

Before moving on, explain what the model knows, what Render does and what delivers a click. Identify which object owns each listener and how that ownership ends. Keep the portable Count: 0, Count: 1, Count: 2 trace distinct from the Editor's input evidence. A working panel needs correct packages and references as well as correct code; a passing model trace establishes none of those scene conditions. Record the chosen UI system and input backend so future maintenance does not mix incompatible setup instructions into an otherwise consistent example.

  • Assign actual uGUI Button and legacy Text references.
  • Match the EventSystem module to the active input backend.
  • Pair the named runtime listener with its owning lifetime.
  • Preserve other systems' listeners during cleanup.
Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Ensure com.unity.ugui is present in Package Manager for the Unity6.0 project.
  2. Create a Screen Space Overlay Canvas, a uGUI Button, a legacy uGUI Text label and an EventSystem.
  3. For an Input System-only project, install com.unity.inputsystem and configure EventSystem with InputSystemUIInputModule and its actions; use the appropriate module for a legacy-input project.
  4. Attach CounterPanel.cs to an active panel and assign its Button and UnityEngine.UI.Text references explicitly.
  5. Enter Play mode, click twice, then disable/re-enable the panel and verify one increment per click.
using UnityEngine;
using UnityEngine.UI;
public sealed class CounterPanel : MonoBehaviour
{
    [SerializeField] private Button addButton;
    [SerializeField] private Text countLabel;
    private int count;
    private void Awake()
    {
        if (addButton == null || countLabel == null)
        { Debug.LogError("Assign a uGUI Button and Text.", this); enabled = false; }
    }
    private void OnEnable()
    {
        if (addButton == null || countLabel == null) return;
        addButton.onClick.AddListener(AddOne);
        Render();
    }
    private void OnDisable() { if (addButton != null) addButton.onClick.RemoveListener(AddOne); }
    private void AddOne() { count++; Render(); }
    private void Render() { countLabel.text = "Count: " + count; }
}

What to observe in Unity

The assigned label starts at Count:0 and increments once per click while enabled. Missing references are reported, and a mismatched EventSystem backend must be corrected in setup.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var model = new CounterModel(); Console.WriteLine(model.Label); model.Add(); Console.WriteLine(model.Label); model.Add(); Console.WriteLine(model.Label);
sealed class CounterModel { private int count; public string Label => "Count: " + count.ToString(CultureInfo.InvariantCulture); public void Add() { count++; } }
Open C# practice

C# practice output

Count: 0
Count: 1
Count: 2
Try this
A Button needs both assigned references and an EventSystem configured for the active input backend.

Further reading