PAYATHON 2026

Unity UI 按钮首次点击无法打开新 Panel

排障赵师傅

结论

最可能的原因是 UnitPanel 的脚本在 Awake() 中调用了 gameObject.SetActive(false),而该对象在场景开始时已经被禁用。

第一次调用 SetActive(true) 时,Unity 才会执行这个组件的 Awake()。Awake() 随即把对象再次禁用,于是第一次点击看起来没有反应。由于 Awake() 只执行一次,第二次调用 SetActive(true) 时,Panel 就能正常显示。

Hierarchy 中的顺序变化通常是 transform.SetAsLastSibling() 引起的。这个方法会把对象移到同级节点的最后。按照 Unity UI 默认的绘制顺序,靠后的兄弟节点会显示在前面。由此可以判断,打开 Panel 的代码很可能已经执行,只是对象随后又被隐藏了。

为什么第二次才能打开

问题代码中有这一段:

void Awake()
{
    gameDatabase = FindAnyObjectByType<GameDatabase>();
    uiManager = FindAnyObjectByType<UIManager>();

    gameObject.SetActive(false);
}

如果对象在场景加载时处于激活状态,Unity 会立即执行 Awake(),然后把它隐藏。这种写法可以运行,但很容易受到对象初始状态的影响。

如果对象一开始就在 Inspector 中取消了勾选,执行过程如下:

  1. 游戏启动时,Panel 处于禁用状态,Awake() 尚未执行。
  2. 第一次点击调用 SetActive(true)。
  3. Panel 激活后,Unity 开始执行它的 Awake()。
  4. Awake() 中的 gameObject.SetActive(false) 再次关闭 Panel。
  5. 第二次点击重新调用 SetActive(true)。
  6. Awake() 不会重复执行,因此 Panel 保持显示。

这正好对应第一次点击失败、第二次点击正常的现象。需要检查 UnitPanelUI 中是否也有相同的初始化代码。

推荐修改

不要让 Panel 在自己的 Awake() 中关闭自己。这里可以只获取引用并初始化数据:

void Awake()
{
    gameDatabase = FindAnyObjectByType<GameDatabase>();
    uiManager = FindAnyObjectByType<UIManager>();
}

初始显示状态交给 UIManager 统一设置:

using UnityEngine;

public class UIManager : MonoBehaviour
{
    [SerializeField] private ProvincePanelUI provincePanel;
    [SerializeField] private UnitPanelUI unitPanel;

    private void Awake()
    {
        provincePanel.gameObject.SetActive(false);
        unitPanel.gameObject.SetActive(false);
    }

    public void ShowUnit(Unit unit)
    {
        unitPanel.gameObject.SetActive(true);
        unitPanel.ShowUnit(unit);
        unitPanel.transform.SetAsLastSibling();

        provincePanel.gameObject.SetActive(false);
    }
}

这段代码假设打开 UnitPanel 时需要关闭 ProvincePanel。如果两个 Panel 需要同时显示,删除最后一行即可。

同时检查 UnitPanelUI.ShowUnit(),确认其中没有再次调用:

gameObject.SetActive(false);

处理 Panel 的显示顺序

在同一个 Canvas 下,兄弟节点的顺序通常会影响 UI 的绘制顺序。下面的调用会把 Panel 移到同级节点的最后:

unitPanel.transform.SetAsLastSibling();

所以,Hierarchy 中的 UnitPanel 移到最下面属于正常现象。按照默认绘制规则,它通常也会出现在其他同级 UI 的上方。

可以先显示 Panel,再调整顺序:

public void ShowUnit(Unit unit)
{
    unitPanel.gameObject.SetActive(true);
    unitPanel.ShowUnit(unit);
    unitPanel.transform.SetAsLastSibling();
}

如果几个 Panel 分别位于不同的 Canvas 中,兄弟节点顺序可能无法决定最终的显示层级。这时还需要检查:

  • Canvas.overrideSorting
  • Canvas.sortingOrder
  • 父级 CanvasGroup.alpha
  • 父级 CanvasGroup.interactable
  • Animator 是否在激活后隐藏了 Panel
  • Panel 的 RectTransform 是否位于屏幕外

确认是否由 Awake() 引起

在 UnitPanelUI 中加入日志:

private void Awake()
{
    Debug.Log("UnitPanel Awake");
}

private void OnEnable()
{
    Debug.Log("UnitPanel OnEnable");
}

private void OnDisable()
{
    Debug.Log("UnitPanel OnDisable");
}

在 UIManager.ShowUnit() 中也记录一次:

public void ShowUnit(Unit unit)
{
    Debug.Log("UIManager.ShowUnit");

    unitPanel.gameObject.SetActive(true);
    unitPanel.transform.SetAsLastSibling();
}

如果第一次点击时,Console 依次输出:

UIManager.ShowUnit
UnitPanel Awake
UnitPanel OnEnable
UnitPanel OnDisable

说明 Panel 在首次激活后又被代码关闭了。

当前代码中的其他注意点

按钮监听器捕获局部变量 unit 的写法没有问题:

unitButton.onClick.AddListener(() =>
{
    uiManager.ShowUnit(unit);
});

Destroy(child.gameObject) 会在当前帧结束时才真正删除对象,但一般不会导致 Panel 必须点击两次。

下面的代码默认驻军列表中至少有一个元素:

Unit unit = province.Garrisons[0];

如果列表可能为空,应先检查:

if (province.Garrisons == null || province.Garrisons.Count == 0)
{
    Debug.LogWarning("Province has no garrison.");
    return;
}

仅凭现有的 ProvincePanelUI 代码,还无法完全排除 UIManager.ShowUnit()、UnitPanelUI、Animator 或 CanvasGroup 的影响。可以先检查 UnitPanelUI.Awake() 中是否存在自我禁用的代码,因为它与首次点击失败、第二次点击正常,以及 Hierarchy 顺序已经改变这几个现象最为吻合。

备注:内容仅供参考。