UIVisibilityBlocker 可见性阻塞器

本文最后更新于 2026年7月21日 下午

UIVisibilityBlocker

源码:

1
Runtime/UIFramework/UIVisibilityBlocker.cs

这个组件用来处理多个地方同时控制一个 UI 的情况,其实很类似之前写的Capability中的发起者模式。

比如 Loading 和新手引导都隐藏了按钮。Loading 先结束时不能直接 SetActive(true),因为新手引导还没结束。

UIVisibilityBlocker 会记录每个隐藏来源,只有相关来源全部解除后才恢复。

UIVisibilityBlocker 流程

数据结构

VisibilityMode 决定怎么应用状态:

1
2
3
4
5
public enum VisibilityMode
{
GameObject,
CanvasGroup,
}

GameObject 模式用 SetActive 控制显示,CanvasGroup 模式修改透明度和交互。

具体阻塞哪些内容由一个 Flags 枚举控制:

1
2
3
4
5
6
7
8
9
[Flags]
public enum VisibilityModeEnum
{
Alpha = 1 << 0,
Interactable = 1 << 1,
BlockRaycasts = 1 << 2,

All = Alpha | Interactable | BlockRaycasts
}

所以可以只隐藏、只禁用点击,也可以全部阻塞。

每个来源和它的阻塞内容保存在字典里:

1
private readonly Dictionary<string, VisibilityModeEnum> _blockSources;

Block 和 Unblock

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Block(string source, VisibilityModeEnum mode = VisibilityModeEnum.All)
{
_blockSources[source] = mode;
UpdateVisibility();
UpdateDebugInfo();
}

public void Unblock(string source)
{
_blockSources.Remove(source);
UpdateVisibility();
UpdateDebugInfo();
}

同一个 source 再次调用 Block 会覆盖旧的 mode,不是增加一次引用计数。解除时也必须传同一个字符串,最好直接定义常量:

1
2
3
4
5
const string SourceLoading = "Loading";
const string SourceGuide = "Guide";

blocker.Block(SourceLoading);
blocker.Unblock(SourceLoading);

所有来源的 mode 通过按位 OR 合并:

1
2
3
4
5
6
7
8
9
10
private VisibilityModeEnum GetBlockedMode()
{
var mode = 0;
foreach (var blockSource in _blockSources)
{
mode |= (int)blockSource.Value;
}

return (VisibilityModeEnum)mode;
}

例如:

1
2
3
4
Loading -> Alpha
Guide -> Interactable | BlockRaycasts

最终状态 = Alpha | Interactable | BlockRaycasts

只要还有一个来源包含某个 flag,这部分状态就不会恢复。

两种应用方式

GameObject

GameObject 模式下,Alpha 对应 Target.SetActive

1
2
if (ShouldApplyMode(blockedMode, VisibilityModeEnum.Alpha))
Target.SetActive(!blockedMode.HasFlag(VisibilityModeEnum.Alpha));

如果节点上还有 CanvasGroupInteractableBlockRaycasts 仍然会应用到它。

这种方式会真的禁用节点。UI 隐藏后不需要继续参与布局、Update 和子节点逻辑时可以用它。

CanvasGroup

CanvasGroup 模式不关闭 GameObject:

1
2
3
4
5
6
7
8
9
10
11
private void ApplyCanvasGroupMode(CanvasGroup cg, VisibilityModeEnum blockedMode)
{
if (ShouldApplyMode(blockedMode, VisibilityModeEnum.Alpha))
cg.alpha = blockedMode.HasFlag(VisibilityModeEnum.Alpha) ? 0f : 1f;

if (ShouldApplyMode(blockedMode, VisibilityModeEnum.Interactable))
cg.interactable = !blockedMode.HasFlag(VisibilityModeEnum.Interactable);

if (ShouldApplyMode(blockedMode, VisibilityModeEnum.BlockRaycasts))
cg.blocksRaycasts = !blockedMode.HasFlag(VisibilityModeEnum.BlockRaycasts);
}

需要保留布局和脚本生命周期,或者只想禁用点击时,用这个模式比较合适。

如果找不到 CanvasGroup,当前实现会输出 Warning,然后退回 GameObject 模式。

为什么需要 _appliedBlockMode

ShouldApplyMode 不只检查当前状态:

1
2
3
4
private bool ShouldApplyMode(VisibilityModeEnum currentMode, VisibilityModeEnum mode)
{
return currentMode.HasFlag(mode) || _appliedBlockMode.HasFlag(mode);
}

例如上一帧的状态是 Alpha,UI 已经被隐藏。下一帧来源解除,当前 mode 变成 0。

如果只检查当前 mode,代码就不会再处理 Alpha,UI 也就不会恢复。_appliedBlockMode 记录上一次应用过的 flag,让解除阻塞时对应字段再执行一次。

使用

全部隐藏:

1
2
blocker.Block("Loading");
blocker.Unblock("Loading");

只禁用点击:

1
2
3
blocker.Block(
"Guide",
VisibilityModeEnum.Interactable | VisibilityModeEnum.BlockRaycasts);

只隐藏画面:

1
blocker.Block("FadeOut", VisibilityModeEnum.Alpha);

组件还提供了 ClearAll(),用于面板关闭或状态整体重置。这个方法会清掉其他系统加进来的来源,不能当成普通的 Unblock 使用。

调试

打开 showDebugInfo 后,Inspector 会显示当前的来源:

1
2
Loading: All
Guide: Interactable, BlockRaycasts

UpdateDebugInfo 带有 Conditional("UNITY_EDITOR"),不会进入 Player 逻辑。UI 没有恢复时,先看还有谁没 Unblock

注意

  • source 不能为空,也不要用时间戳、随机数或临时 hash 拼接。
  • Unblock 删除不存在的来源时不会报错,清理阶段可以直接调用。
  • CanvasGroup 模式恢复的是 alpha = 1interactable = trueblocksRaycasts = true,不会保存阻塞前的值。
  • OnDestroy 只清理内部字典,不会再恢复目标状态。Blocker 和目标生命周期不一致时需要自己处理。
  • 只有一个地方控制显隐时直接处理即可,没必要再套一层 Blocker。

UIVisibilityBlocker 可见性阻塞器
https://nicoier.github.io/2026/07/09/UIVisibilityBlocker可见性阻塞器/
作者
NicoIer
发布于
2026年7月9日
许可协议