From c9f01f587797d44eb7c6f878f78a0dda7ba75067 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:14:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=B7=BB=E5=8A=A0UI=E5=B1=82?= =?UTF-8?q?=E7=BA=A7=E7=AE=A1=E7=90=86=E5=92=8CGodot=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在IUiRoot接口中添加Z-order控制和页面层级管理功能 - 实现Godot平台的UiRoot,支持UI页面的添加、移除和层级排序 - 添加UiLayer枚举定义不同UI层级(Page、Overlay、Modal、Toast、Topmost) - 在IUiRouter中扩展层级管理方法,支持指定层级显示UI - 实现UiRouterBase中的层级管理逻辑,包括显示、隐藏、清空等操作 - 添加对GodotSharp包的引用以支持Godot平台功能 --- GFramework.Game.Abstractions/enums/UiLayer.cs | 33 +++++ GFramework.Game.Abstractions/ui/IUiRoot.cs | 27 ++++ GFramework.Game.Abstractions/ui/IUiRouter.cs | 54 +++++++ GFramework.Game/ui/UiRouterBase.cs | 137 ++++++++++++++++++ GFramework.Godot/GFramework.Godot.csproj | 3 +- GFramework.Godot/ui/GodotUiRoot.cs | 123 ++++++++++++++++ 6 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 GFramework.Game.Abstractions/enums/UiLayer.cs create mode 100644 GFramework.Godot/ui/GodotUiRoot.cs diff --git a/GFramework.Game.Abstractions/enums/UiLayer.cs b/GFramework.Game.Abstractions/enums/UiLayer.cs new file mode 100644 index 0000000..1a91f33 --- /dev/null +++ b/GFramework.Game.Abstractions/enums/UiLayer.cs @@ -0,0 +1,33 @@ +namespace GFramework.Game.Abstractions.enums; + +/// +/// UI层级枚举,定义UI界面的显示层级 +/// 用于管理不同类型的UI在屏幕上的显示顺序 +/// +public enum UiLayer +{ + /// + /// 页面层,使用栈管理UI的切换 + /// + Page = 0, + + /// + /// 浮层,用于覆盖层、对话框等 + /// + Overlay = 10, + + /// + /// 模态层,会阻挡下层交互,带有遮罩效果 + /// + Modal = 20, + + /// + /// 提示层,用于轻量提示如toast消息、loading指示器等 + /// + Toast = 30, + + /// + /// 顶层,用于系统级弹窗、全屏加载等 + /// + Topmost = 40 +} diff --git a/GFramework.Game.Abstractions/ui/IUiRoot.cs b/GFramework.Game.Abstractions/ui/IUiRoot.cs index f3f74cf..078caa8 100644 --- a/GFramework.Game.Abstractions/ui/IUiRoot.cs +++ b/GFramework.Game.Abstractions/ui/IUiRoot.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace GFramework.Game.Abstractions.ui; /// @@ -11,9 +13,34 @@ public interface IUiRoot /// 要添加的UI页面子节点 void AddUiPage(IUiPageBehavior child); + /// + /// 向UI根节点添加子页面到指定层级 + /// + /// 要添加的UI页面子节点 + /// Z-order值,用于控制UI显示层级 + void AddUiPage(IUiPageBehavior child, int zOrder = 0); + /// /// 从UI根节点移除子页面 /// /// 要移除的UI页面子节点 void RemoveUiPage(IUiPageBehavior child); + + /// + /// 设置页面的Z-order(层级顺序) + /// + /// UI页面 + /// Z-order值 + void SetZOrder(IUiPageBehavior page, int zOrder); + + /// + /// 获取当前所有显示的页面 + /// + /// 所有显示的页面列表 + IReadOnlyList GetVisiblePages(); + + /// + /// 强制刷新UI层级排序 + /// + void RefreshLayerOrder(); } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiRouter.cs b/GFramework.Game.Abstractions/ui/IUiRouter.cs index 2910c8a..fde8763 100644 --- a/GFramework.Game.Abstractions/ui/IUiRouter.cs +++ b/GFramework.Game.Abstractions/ui/IUiRouter.cs @@ -114,4 +114,58 @@ public interface IUiRouter : ISystem /// 判断指定UI是否存在于UI栈中 /// bool Contains(string uiKey); + + #region 层级管理 + + /// + /// 在指定层级显示UI(非栈管理) + /// + /// UI标识符 + /// UI层级 + /// 进入参数 + /// 实例策略 + void Show( + string uiKey, + UiLayer layer, + IUiPageEnterParam? param = null, + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse); + + /// + /// 在指定层级显示UI(基于实例) + /// + /// UI页面实例 + /// UI层级 + void Show(IUiPageBehavior page, UiLayer layer); + + /// + /// 隐藏指定层级的UI + /// + /// UI标识符 + /// UI层级 + /// 是否销毁实例 + void Hide(string uiKey, UiLayer layer, bool destroy = false); + + /// + /// 清空指定层级的所有UI + /// + /// UI层级 + /// 是否销毁实例 + void ClearLayer(UiLayer layer, bool destroy = false); + + /// + /// 获取指定层级的UI实例 + /// + /// UI标识符 + /// UI层级 + /// UI实例,不存在则返回null + IUiPageBehavior? GetFromLayer(string uiKey, UiLayer layer); + + /// + /// 判断指定层级是否有UI显示 + /// + /// UI层级 + /// 是否有UI显示 + bool HasVisibleInLayer(UiLayer layer); + + #endregion } \ No newline at end of file diff --git a/GFramework.Game/ui/UiRouterBase.cs b/GFramework.Game/ui/UiRouterBase.cs index d7df837..b6bffc8 100644 --- a/GFramework.Game/ui/UiRouterBase.cs +++ b/GFramework.Game/ui/UiRouterBase.cs @@ -4,6 +4,7 @@ using GFramework.Core.logging; using GFramework.Core.system; using GFramework.Game.Abstractions.enums; using GFramework.Game.Abstractions.ui; +using System.Linq; namespace GFramework.Game.ui; @@ -24,6 +25,11 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// private readonly Stack _stack = new(); + /// + /// 层级管理(非栈层级),用于Overlay、Modal、Toast等浮层 + /// + private readonly Dictionary> _layers = new(); + /// /// UI工厂实例,用于创建UI相关的对象 /// @@ -434,4 +440,135 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter while (_stack.Count > 0) DoPopInternal(policy); } + + #region 层级管理 + + /// + /// 在指定层级显示UI(非栈管理) + /// + public void Show( + string uiKey, + Game.Abstractions.enums.UiLayer layer, + IUiPageEnterParam? param = null, + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse) + { + if (layer == Game.Abstractions.enums.UiLayer.Page) + { + throw new ArgumentException("Use Push() for Page layer"); + } + + // 初始化层级字典 + if (!_layers.ContainsKey(layer)) + _layers[layer] = new Dictionary(); + + var layerDict = _layers[layer]; + + // 检查是否已存在 + if (layerDict.TryGetValue(uiKey, out var existing)) + { + Log.Debug("UI already visible in layer: {0}, layer={1}", uiKey, layer); + existing.OnEnter(param); + existing.OnShow(); + return; + } + + // 获取或创建实例 + var page = _factory.GetOrCreate(uiKey, instancePolicy); + layerDict[uiKey] = page; + + // 添加到UiRoot,传入层级Z-order + _uiRoot.AddUiPage(page, (int)layer); + + page.OnEnter(param); + page.OnShow(); + + Log.Debug("Show UI in layer: {0}, layer={1}", uiKey, layer); + } + + /// + /// 在指定层级显示UI(基于实例) + /// + public void Show(IUiPageBehavior page, UiLayer layer) + { + if (layer == UiLayer.Page) + throw new ArgumentException("Use Push() for Page layer"); + + var uiKey = page.Key; + + if (!_layers.ContainsKey(layer)) + _layers[layer] = new Dictionary(); + + _layers[layer][uiKey] = page; + _uiRoot.AddUiPage(page, (int)layer); + page.OnShow(); + + Log.Debug("Show existing UI instance in layer: {0}, layer={1}", uiKey, layer); + } + + /// + /// 隐藏指定层级的UI + /// + public void Hide(string uiKey, Game.Abstractions.enums.UiLayer layer, bool destroy = false) + { + if (!_layers.TryGetValue(layer, out var layerDict)) + return; + + if (!layerDict.TryGetValue(uiKey, out var page)) + return; + + page.OnExit(); + page.OnHide(); + + if (destroy) + { + _uiRoot.RemoveUiPage(page); + layerDict.Remove(uiKey); + Log.Debug("Hide & Destroy UI from layer: {0}, layer={1}", uiKey, layer); + } + else + { + _uiRoot.RemoveUiPage(page); + _factory.Recycle(page); + layerDict.Remove(uiKey); + Log.Debug("Hide & Cache UI from layer: {0}, layer={1}", uiKey, layer); + } + } + + /// + /// 清空指定层级的所有UI + /// + public void ClearLayer(Game.Abstractions.enums.UiLayer layer, bool destroy = false) + { + if (!_layers.TryGetValue(layer, out var layerDict)) + return; + + var keys = layerDict.Keys.ToArray(); + foreach (var key in keys) + { + Hide(key, layer, destroy); + } + + Log.Debug("Cleared layer: {0}, destroyed={1}", layer, destroy); + } + + /// + /// 获取指定层级的UI实例 + /// + public IUiPageBehavior? GetFromLayer(string uiKey, UiLayer layer) + { + return _layers.TryGetValue(layer, out var layerDict) && + layerDict.TryGetValue(uiKey, out var page) + ? page + : null; + } + + /// + /// 判断指定层级是否有UI显示 + /// + public bool HasVisibleInLayer(UiLayer layer) + { + return _layers.TryGetValue(layer, out var layerDict) && layerDict.Count > 0; + } + + #endregion } \ No newline at end of file diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj index fd5c89c..a77478d 100644 --- a/GFramework.Godot/GFramework.Godot.csproj +++ b/GFramework.Godot/GFramework.Godot.csproj @@ -1,4 +1,4 @@ - + GeWuYou.$(AssemblyName) @@ -9,6 +9,7 @@ + diff --git a/GFramework.Godot/ui/GodotUiRoot.cs b/GFramework.Godot/ui/GodotUiRoot.cs new file mode 100644 index 0000000..7bcdaba --- /dev/null +++ b/GFramework.Godot/ui/GodotUiRoot.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System; +using GFramework.Game.Abstractions.ui; +using Godot; + +namespace GFramework.Godot.ui; + +/// +/// Godot平台的UI根节点实现 +/// 用于管理UI页面的添加、移除和层级排序 +/// +public partial class GodotUiRoot : Node, IUiRoot +{ + /// + /// UI节点的父容器,所有UI页面都添加到这个节点下 + /// + private Node? _uiContainer; + + /// + /// UI页面的追踪字典,记录每个页面的节点 + /// + private readonly Dictionary _pageNodes = new(); + + public override void _Ready() + { + // 创建UI容器节点 + _uiContainer = new Node + { + Name = "UiContainer" + }; + + AddChild(_uiContainer); + } + + /// + /// 向UI根节点添加子页面 + /// + public void AddUiPage(IUiPageBehavior child) + { + if (_uiContainer == null) + throw new InvalidOperationException("UiContainer is not initialized"); + + var node = GetNodeFromPage(child); + if (node == null) + throw new InvalidOperationException($"Page node is null: {child.Key}"); + + if (_pageNodes.ContainsKey(child)) + return; + + _pageNodes[child] = node; + + if (node.GetParent() != _uiContainer) + { + _uiContainer.AddChild(node); + } + } + + /// + /// 向UI根节点添加子页面到指定层级 + /// + public void AddUiPage(IUiPageBehavior child, int zOrder = 0) + { + AddUiPage(child); + SetZOrder(child, zOrder); + } + + /// + /// 从UI根节点移除子页面 + /// + public void RemoveUiPage(IUiPageBehavior child) + { + if (!_pageNodes.TryGetValue(child, out var node)) + return; + + _pageNodes.Remove(child); + + if (_uiContainer != null && node.GetParent() == _uiContainer) + { + _uiContainer.RemoveChild(node); + } + } + + /// + /// 设置页面的Z-order(层级顺序) + /// + public void SetZOrder(IUiPageBehavior page, int zOrder) + { + if (!_pageNodes.TryGetValue(page, out var node)) + return; + + if (node is CanvasItem canvasItem) + { + canvasItem.ZIndex = zOrder; + } + } + + /// + /// 获取当前所有显示的页面 + /// + public IReadOnlyList GetVisiblePages() + { + return _pageNodes.Keys.ToList().AsReadOnly(); + } + + /// + /// 强制刷新UI层级排序 + /// + public void RefreshLayerOrder() + { + if (_uiContainer == null) + return; + + _uiContainer.MoveChild(_uiContainer.GetChild(0), 0); + } + + /// + /// 从页面行为获取对应的节点 + /// + private static Node? GetNodeFromPage(IUiPageBehavior page) + { + return page.View as Node; + } +}