ECS UI 窗口
用 ECS System 当窗口,而不是 MonoBehaviour 窗口管理器。每个界面 = 一个 SystemBase 子类 + 一个 Addressables 预制体。
命名空间 HYC.Framework.UI。一句话:窗口是 System,界面是 GameObject,两者由 UIManager 在运行时缝合。
设计要点
| 传统 MonoBehaviour UI | HYC ECS UI |
|---|---|
UIManager 单例持有所有窗口实例 | UIManager 是一个 SystemBase,只持有层节点 + 视图状态表 |
窗口是 MonoBehaviour + 预制体 | 窗口是 SystemBase(AbsUISystem)+ 预制体 |
生命周期靠 Awake/OnEnable/OnDestroy | 生命周期靠 OnViewOpen / OnViewUpdate / OnViewClose / OnViewFocus / OnViewLost |
打开 = Instantiate | 打开 = Addressables.InstantiateAsync + group.AddSystemToUpdateList |
| 层级靠 Canvas sortOrder 手工维护 | 层级由 WindowLayer × 1000 + 层内序号 自动算 |
核心类
| 类 | 职责 |
|---|---|
UIManager | [DisableAutoCreation] 的 SystemBase。根节点、层节点、视图状态、焦点、UI 场景缓存、分辨率适配、点击检测 |
AbsUISystem | 所有窗口 / HUD / 弹窗的抽象基类(本身也是 SystemBase) |
BaseWindowSystem<T> | 窗口层基类(Focusable = true) |
BaseDialogSystem<T> | 弹窗层基类 |
BaseHudSystem<T> | HUD 基类(Focusable = false) |
UILayerTable | UI 层表的运行期门面(FrameworkSettings.uiLayers) |
层级(层表,原 WindowLayer 枚举)
层集合是数据:FrameworkSettings.uiLayers(Resources 资产),在 HYC Settings → UI/层级 增删改名、拖动排序。下表是默认 10 层,顺序即层级(数值越大越靠前):
| # | 层 | 打开 API | 说明 |
|---|---|---|---|
| 0 | HUD | OpenHud<T>() | 常驻 HUD(不可聚焦) |
| 1 | HUDNotice | OpenHudNotice<T>() | HUD 上的飘字 / 提示 |
| 2 | Window | OpenWindow<T>(...) | 功能窗口 |
| 3 | WindowNotice | OpenWindowNotice<T>() | 窗口之上的提示 |
| 4 | Dialog | OpenDialog<T>() | 对话框 |
| 5 | TopWindow | OpenTopWindow<T>() | 最顶层窗口 |
| 6 | ToolTip | OpenToolTip<T>() | Tooltip |
| 7 | Loading | OpenLoading<T>() | 加载遮罩 |
| 8 | Cursor | OpenCursor<T>() | 光标 |
| 9 | Mask | OpenMask<T>() | 最上层遮罩 |
sortingOrder = i * 1000 + j(i 从 1 起,j 是层内按 Order 排序后的序号)。所以单层窗口不要超过 999 个,否则会串到下一层号段。
打开一个窗口
public class BagWindow : BaseWindowSystem<BagBinder>
{
public override string PrefabKey => "UI/BagWindow.prefab"; // 必须实现
// public override string SceneKey => "UI/Scene/Bag"; // 可选
public override void OnViewOpen(params object[] args)
{
base.OnViewOpen();
Binder.CloseButton.onClick.AddListener(() => UIManager.Close<BagWindow>());
}
}
// 打开(异步!View 在 OnViewOpen 里才就绪)
UIManager.OpenWindow<BagWindow>(1001);
✕
打开是异步的:
Open<T>() 返回时 View == null。所有依赖 View 的初始化必须写在 OnViewOpen,不能写在 OnCreate。
⚠
UIManager 带 [DisableAutoCreation] —— 宿主必须在引导期手动创建它(World.GetOrCreateSystemManaged<UIManager>() 并加进更新列表),否则所有窗口 API 都拿不到层节点(静态字段为 null)。关闭窗口
UIManager.Close<MyWindow>(); // 泛型,默认播放退出动画
UIManager.Close(typeof(MyWindow), false); // 跳过动画
UIManager.CloseBy(w => w is IBattleUI); // 按选择器批量关
退出动画依赖 Animator 里名为 Exit 的 Trigger:找到就先 SetTrigger 并本帧不关,等 OnAnimationExitEvent 回调里再真正关闭;没有 Exit Trigger 就立即关闭。
焦点与导航
ResetWindowFocus():遍历所有层,最后一个Focusable == true的窗口获得焦点(越靠后的层越优先)。- 切换时旧窗口
OnViewLost()(热键Silence = true),新窗口OnViewFocus()(Silence = false)。 - 手柄 / 键盘导航由
AbsUISystem.UpdateByFocusManager()驱动,只在Focused && HotkeyManager.IsNavigateMode时跑。
分辨率适配
OnUpdate 里检测 Screen.width/height 变化 → ResetScreenData + ResetUIFitData + ResetWindowFit。最终每个窗口 Canvas:
canvasRect.localPosition = new Vector3(0, 0, ZInCamera);
canvasRect.localScale = Vector3.one * ScaleFactor;
canvasRect.sizeDelta = BestResolution;
root.anchorMin = root.anchorMax = new Vector2(0.5f, 0.5f);
root.anchoredPosition = Vector2.zero;
root.sizeDelta = BestResolution;
常见坑(必看)
UIManager是[DisableAutoCreation],必须手动创建;忘了建会LayerNodes == null空引用。- 打开是异步的——
Open<T>()返回时View还是 null,初始化写在OnViewOpen。 PrefabKey是抽象方法必须实现,SceneKey可选。- 单层窗口别超过 999 个(sortingOrder 号段只有 1000)。
- HUD 会被自动隐藏:有 Window/Dialog 或 UI 场景时 HUD 的 Canvas 直接
enabled = false,但OnViewUpdate仍在跑。 Close的动画分支依赖 Animator 里有名为Exit的 Trigger;没有就立即关闭。- UI 场景卸载有 0.1s 延迟(
mSceneClearDelayTime),期间又有窗口打开会取消卸载。 UICamera没设会 fallback 到Camera.main;两个都没有会LogError("UIManager.ResetUIFitData : UICamera 未设置!")。IGameFunctionWindow是空标记接口,语义是「场景切换时强制关闭」,但需要宿主自己实现扫描逻辑(框架内没有自动关闭代码)。MoveDirection枚举不在本包内(来自外部 Dots/Input 包)。