◆ 持续开发中 · 当前为非稳定版本,如需抢先体验请通过 GitHub Issues 联系作者

ECS UI 窗口

用 ECS System 当窗口,而不是 MonoBehaviour 窗口管理器。每个界面 = 一个 SystemBase 子类 + 一个 Addressables 预制体。

命名空间 HYC.Framework.UI。一句话:窗口是 System,界面是 GameObject,两者由 UIManager 在运行时缝合。

设计要点

传统 MonoBehaviour UIHYC 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)
UILayerTableUI 层表的运行期门面(FrameworkSettings.uiLayers)

层级(层表,原 WindowLayer 枚举)

层集合是数据:FrameworkSettings.uiLayers(Resources 资产),在 HYC Settings → UI/层级 增删改名、拖动排序。下表是默认 10 层,顺序即层级(数值越大越靠前):

#层打开 API说明
0HUDOpenHud<T>()常驻 HUD(不可聚焦)
1HUDNoticeOpenHudNotice<T>()HUD 上的飘字 / 提示
2WindowOpenWindow<T>(...)功能窗口
3WindowNoticeOpenWindowNotice<T>()窗口之上的提示
4DialogOpenDialog<T>()对话框
5TopWindowOpenTopWindow<T>()最顶层窗口
6ToolTipOpenToolTip<T>()Tooltip
7LoadingOpenLoading<T>()加载遮罩
8CursorOpenCursor<T>()光标
9MaskOpenMask<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 包)。