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;
+ }
+}