From 760cc719850325668bf45f67eacb777aa7ef47ed Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 20 Jan 2026 08:32:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=AE=9E=E7=8E=B0UI=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E6=B1=A0=E5=8C=96=E7=AE=A1=E7=90=86=E5=92=8C=E7=94=9F?= =?UTF-8?q?=E5=91=BD=E5=91=A8=E6=9C=9F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加UI实例管理策略枚举(AlwaysCreate、Reuse、Pooled) - 在GodotUiFactory中实现缓存池和实例回收机制 - 扩展IUiFactory接口支持预加载、回收和缓存管理功能 - 更新UiRouterBase支持实例策略参数传递 - 重构Pop策略将Hide重命名为Cache以明确语义 - 移除项目文件中的冗余文件夹引用 - 添加日志记录便于调试和监控实例状态 - 实现批量预加载和全量缓存清理功能 - 优化页面替换逻辑支持实例复用和池化管理 --- .../enums/UiTransitionPolicy.cs | 26 +-- GFramework.Game.Abstractions/ui/IUiFactory.cs | 45 +++- GFramework.Game.Abstractions/ui/IUiRouter.cs | 36 ++- .../ui/UiInstancePolicy.cs | 23 ++ .../ui/UiPopPolicy.cs | 8 +- GFramework.Game/GFramework.Game.csproj | 3 - GFramework.Game/ui/UiRouterBase.cs | 91 +++++--- GFramework.Godot/ui/GodotUiFactory.cs | 215 ++++++++++++++++-- 8 files changed, 354 insertions(+), 93 deletions(-) create mode 100644 GFramework.Game.Abstractions/ui/UiInstancePolicy.cs diff --git a/GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs b/GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs index 8bc837e..87abe83 100644 --- a/GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs +++ b/GFramework.Game.Abstractions/enums/UiTransitionPolicy.cs @@ -6,27 +6,13 @@ /// public enum UiTransitionPolicy { - /// - /// 出栈即销毁(一次性页面) - /// 页面从栈中移除时会完全销毁实例 - /// - Destroy, - - /// - /// 出栈隐藏,保留实例 - /// 页面从栈中移除时仅隐藏显示,保留实例以便后续重用 - /// - Hide, - - /// - /// 覆盖显示(不影响下层页面) - /// 当前页面覆盖在其他页面之上显示,不影响下层页面的状态 - /// - Overlay, - /// /// 独占显示(下层页面 Pause + Hide) - /// 当前页面独占显示区域,下层页面会被暂停并隐藏 /// - Exclusive + Exclusive, + + /// + /// 覆盖显示(下层页面仅 Pause,不隐藏) + /// + Overlay } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiFactory.cs b/GFramework.Game.Abstractions/ui/IUiFactory.cs index 56f78e1..b7e1199 100644 --- a/GFramework.Game.Abstractions/ui/IUiFactory.cs +++ b/GFramework.Game.Abstractions/ui/IUiFactory.cs @@ -8,9 +8,48 @@ namespace GFramework.Game.Abstractions.ui; public interface IUiFactory : IContextUtility { /// - /// 根据UI键值创建对应的UI页面实例 + /// 创建或获取UI页面实例 + /// + /// UI标识键 + /// 实例管理策略 + /// UI页面实例 + IUiPageBehavior GetOrCreate(string uiKey, UiInstancePolicy policy = UiInstancePolicy.AlwaysCreate); + + /// + /// 仅创建新实例(不使用缓存) /// - /// UI标识键,用于确定要创建的具体UI页面类型 - /// 创建的UI页面实例,实现IUiPage接口 IUiPageBehavior Create(string uiKey); + + /// + /// 预加载UI资源到缓存池 + /// + /// UI标识键 + /// 预加载数量,默认1个 + void Preload(string uiKey, int count = 1); + + /// + /// 批量预加载 + /// + void PreloadBatch(params string[] uiKeys); + + /// + /// 回收实例到缓存池 + /// + /// 要回收的页面实例 + void Recycle(IUiPageBehavior page); + + /// + /// 清理指定UI的缓存实例 + /// + void ClearCache(string uiKey); + + /// + /// 清理所有缓存 + /// + void ClearAllCache(); + + /// + /// 检查是否有缓存的实例 + /// + bool HasCached(string uiKey); } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/IUiRouter.cs b/GFramework.Game.Abstractions/ui/IUiRouter.cs index aeefefc..2910c8a 100644 --- a/GFramework.Game.Abstractions/ui/IUiRouter.cs +++ b/GFramework.Game.Abstractions/ui/IUiRouter.cs @@ -25,7 +25,10 @@ public interface IUiRouter : ISystem /// UI界面的唯一标识符 /// 进入界面的参数,可为空 /// 界面切换策略,默认为Exclusive(独占) - void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive); + /// 实例管理策略,默认为Reuse(复用) + void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive, + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse); + /// /// 将已存在的UI页面压入路由栈 @@ -37,6 +40,7 @@ public interface IUiRouter : ISystem void Push(IUiPageBehavior page, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive); + /// /// 弹出路由栈顶的UI界面,返回到上一个界面 /// @@ -44,21 +48,33 @@ public interface IUiRouter : ISystem void Pop(UiPopPolicy policy = UiPopPolicy.Destroy); /// - /// 替换当前UI界面为指定的新界面 + /// 替换当前所有页面为新页面(基于uiKey) /// - /// 新UI界面的唯一标识符 - /// 进入界面的参数,可为空 - /// 界面弹出策略,默认为销毁当前界面 - /// 界面过渡策略,默认为独占模式 - void Replace( + /// 新UI页面标识符 + /// 页面进入参数,可为空 + /// 弹出页面时的销毁策略,默认为销毁 + /// 推入页面时的过渡策略,默认为独占 + /// 实例管理策略 + public void Replace( string uiKey, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, - UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive - ); - + UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive, + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse); /// + /// 替换当前所有页面为已存在的页面(基于实例) + /// + /// 已创建的UI页面行为实例 + /// 页面进入参数,可为空 + /// 弹出页面时的销毁策略,默认为销毁 + /// 推入页面时的过渡策略,默认为独占 + public void Replace( + IUiPageBehavior page, + IUiPageEnterParam? param = null, + UiPopPolicy popPolicy = UiPopPolicy.Destroy, + UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive); + /// /// 清空所有UI界面,重置路由状态 /// void Clear(); diff --git a/GFramework.Game.Abstractions/ui/UiInstancePolicy.cs b/GFramework.Game.Abstractions/ui/UiInstancePolicy.cs new file mode 100644 index 0000000..eab977c --- /dev/null +++ b/GFramework.Game.Abstractions/ui/UiInstancePolicy.cs @@ -0,0 +1,23 @@ + +namespace GFramework.Game.Abstractions.ui; + +/// +/// UI页面实例管理策略(控制实例的生命周期) +/// +public enum UiInstancePolicy +{ + /// + /// 总是创建新实例 + /// + AlwaysCreate, + + /// + /// 复用已存在的实例(如果有) + /// + Reuse, + + /// + /// 从预加载池中获取或创建 + /// + Pooled +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/ui/UiPopPolicy.cs b/GFramework.Game.Abstractions/ui/UiPopPolicy.cs index 34607db..3ac7bec 100644 --- a/GFramework.Game.Abstractions/ui/UiPopPolicy.cs +++ b/GFramework.Game.Abstractions/ui/UiPopPolicy.cs @@ -6,12 +6,12 @@ public enum UiPopPolicy { /// - /// 销毁模式:关闭时完全销毁UI对象 + /// 销毁实例 /// Destroy, - + /// - /// 隐藏模式:关闭时仅隐藏UI对象,保留实例 + /// 隐藏但保留实例(下次Push可复用) /// - Hide + Cache } \ No newline at end of file diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj index 77d1ec0..b81965b 100644 --- a/GFramework.Game/GFramework.Game.csproj +++ b/GFramework.Game/GFramework.Game.csproj @@ -13,7 +13,4 @@ - - - diff --git a/GFramework.Game/ui/UiRouterBase.cs b/GFramework.Game/ui/UiRouterBase.cs index aa9738e..d7df837 100644 --- a/GFramework.Game/ui/UiRouterBase.cs +++ b/GFramework.Game/ui/UiRouterBase.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using GFramework.Core.Abstractions.logging; using GFramework.Core.extensions; using GFramework.Core.logging; @@ -62,18 +58,17 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter _uiRoot = root; Log.Debug("Bind UI Root: {0}", root.GetType().Name); } + /// - /// 将指定UI页面压入栈顶并显示 + /// 将指定的UI界面压入路由栈,显示新的UI界面 /// - /// UI页面标识符 - /// 页面进入参数,可为空 - /// 页面切换策略 - public void Push( - string uiKey, - IUiPageEnterParam? param = null, - UiTransitionPolicy policy = UiTransitionPolicy.Exclusive - ) + /// UI界面的唯一标识符 + /// 进入界面的参数,可为空 + /// 界面切换策略,默认为Exclusive(独占) + /// 实例管理策略,默认为Reuse(复用) + public void Push(string uiKey, IUiPageEnterParam? param = null, UiTransitionPolicy policy = UiTransitionPolicy.Exclusive, + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse) { if (IsTop(uiKey)) { @@ -84,15 +79,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param); Log.Debug( - "Push UI Page: key={0}, policy={1}, stackBefore={2}", - uiKey, policy, _stack.Count + "Push UI Page: key={0}, policy={1}, instancePolicy={2}, stackBefore={3}", + uiKey, policy, instancePolicy, _stack.Count ); BeforeChange(@event); - // 先创建页面,然后使用统一的Push逻辑 - var page = _factory.Create(uiKey); - Log.Debug("Create UI Page instance: {0}", page.GetType().Name); + // 使用工厂的增强方法获取实例 + var page = _factory.GetOrCreate(uiKey, instancePolicy); + Log.Debug("Get/Create UI Page instance: {0}", page.GetType().Name); DoPushPageInternal(page, param, policy); @@ -144,8 +139,8 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter } var nextUiKey = _stack.Count > 1 - ? _stack.ElementAt(1).View.GetType().Name - : string.Empty; + ? _stack.ElementAt(1).Key // 使用 Key 而不是 View.GetType().Name + : throw new InvalidOperationException("Stack is empty"); var @event = CreateEvent(nextUiKey, UiTransitionType.Pop); BeforeChange(@event); @@ -156,33 +151,67 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter } /// - /// 替换当前所有页面为新页面 + /// 替换当前所有页面为新页面(基于uiKey) /// /// 新UI页面标识符 /// 页面进入参数,可为空 /// 弹出页面时的销毁策略,默认为销毁 /// 推入页面时的过渡策略,默认为独占 + /// 实例管理策略 public void Replace( string uiKey, IUiPageEnterParam? param = null, UiPopPolicy popPolicy = UiPopPolicy.Destroy, - UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive - ) + UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive, + UiInstancePolicy instancePolicy = UiInstancePolicy.Reuse) { var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param); Log.Debug( - "Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}", + "Replace UI Stack with page: key={0}, popPolicy={1}, pushPolicy={2}, instancePolicy={3}", + uiKey, popPolicy, pushPolicy, instancePolicy + ); + + BeforeChange(@event); + + // 使用内部方法清空栈,避免触发额外的Pipeline + DoClearInternal(popPolicy); + + // 使用工厂的增强方法获取实例 + var page = _factory.GetOrCreate(uiKey, instancePolicy); + Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name); + + DoPushPageInternal(page, param, pushPolicy); + + AfterChange(@event); + } + /// + /// 替换当前所有页面为已存在的页面(基于实例) + /// + /// 已创建的UI页面行为实例 + /// 页面进入参数,可为空 + /// 弹出页面时的销毁策略,默认为销毁 + /// 推入页面时的过渡策略,默认为独占 + public void Replace( + IUiPageBehavior page, + IUiPageEnterParam? param = null, + UiPopPolicy popPolicy = UiPopPolicy.Destroy, + UiTransitionPolicy pushPolicy = UiTransitionPolicy.Exclusive) + { + var uiKey = page.Key; + var @event = CreateEvent(uiKey, UiTransitionType.Replace, pushPolicy, param); + + Log.Debug( + "Replace UI Stack with existing page: key={0}, popPolicy={1}, pushPolicy={2}", uiKey, popPolicy, pushPolicy ); BeforeChange(@event); - // 使用内部方法,避免触发额外的Pipeline + // 清空栈 DoClearInternal(popPolicy); - var page = _factory.Create(uiKey); - Log.Debug("Create UI Page instance for Replace: {0}", page.GetType().Name); + Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name); DoPushPageInternal(page, param, pushPolicy); AfterChange(@event); @@ -372,11 +401,13 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter { Log.Debug("Destroy UI Page: {0}", top.GetType().Name); _uiRoot.RemoveUiPage(top); + // 不回收,直接销毁 } - else + else // UiPopPolicy.Cache { - Log.Debug("Hide UI Page: {0}", top.GetType().Name); - top.OnHide(); + Log.Debug("Cache UI Page: {0}", top.GetType().Name); + _uiRoot.RemoveUiPage(top); + _factory.Recycle(top); // 回收到池中 } if (_stack.Count > 0) diff --git a/GFramework.Godot/ui/GodotUiFactory.cs b/GFramework.Godot/ui/GodotUiFactory.cs index 8794e34..a5f57dd 100644 --- a/GFramework.Godot/ui/GodotUiFactory.cs +++ b/GFramework.Godot/ui/GodotUiFactory.cs @@ -1,6 +1,10 @@ -using GFramework.Core.extensions; +using GFramework.Core.Abstractions.logging; +using GFramework.Core.extensions; +using GFramework.Core.logging; using GFramework.Core.utility; using GFramework.Game.Abstractions.ui; +using GFramework.Godot.extensions; +using Godot; namespace GFramework.Godot.ui; @@ -10,39 +14,204 @@ namespace GFramework.Godot.ui; /// public class GodotUiFactory : AbstractContextUtility, IUiFactory { - /// - /// UI注册表,用于存储和获取PackedScene类型的UI资源 - /// + private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger("GodotUiFactory"); + private IGodotUiRegistry _registry = null!; + + /// + /// 缓存池:uiKey -> 实例队列 + /// + private readonly Dictionary> _cachedInstances = new(); + + /// + /// 追踪所有创建的实例(用于清理) + /// + private readonly Dictionary> _allInstances = new(); /// - /// 根据指定的UI键创建UI页面实例 + /// 创建或获取UI页面实例 /// - /// UI资源的唯一标识键 - /// 实现IUiPage接口的UI页面实例 - /// 当UI场景没有继承IUiPage接口时抛出 - public IUiPageBehavior Create(string uiKey) + public IUiPageBehavior GetOrCreate(string uiKey, UiInstancePolicy policy = UiInstancePolicy.AlwaysCreate) { - // 从注册表中获取对应的场景资源 - var scene = _registry.Get(uiKey); - - // 实例化场景节点 - var node = scene.Instantiate(); - - // 验证节点是否实现了IUiPageProvider接口 - if (node is not IUiPageBehaviorProvider provider) - throw new InvalidCastException( - $"UI scene {uiKey} must implement IUiPageBehaviorProvider"); - - // 获取并返回页面行为实例 - return provider.GetPage(); + return policy switch + { + UiInstancePolicy.Reuse => GetCachedOrCreate(uiKey), + UiInstancePolicy.Pooled => GetFromPoolOrCreate(uiKey), + _ => Create(uiKey) + }; } /// - /// 初始化方法,获取UI注册表实例 + /// 仅创建新实例 /// + public IUiPageBehavior Create(string uiKey) + { + var scene = _registry.Get(uiKey); + var node = scene.Instantiate(); + + if (node is not IUiPageBehaviorProvider provider) + throw new InvalidCastException($"UI scene {uiKey} must implement IUiPageBehaviorProvider"); + + var page = provider.GetPage(); + + // 追踪实例 + if (!_allInstances.ContainsKey(uiKey)) + _allInstances[uiKey] = new HashSet(); + _allInstances[uiKey].Add(page); + + Log.Debug("Created new UI instance: {0}", uiKey); + return page; + } + + /// + /// 预加载UI资源 + /// + public void Preload(string uiKey, int count = 1) + { + Log.Debug("Preloading UI: {0}, count={1}", uiKey, count); + + if (!_cachedInstances.ContainsKey(uiKey)) + _cachedInstances[uiKey] = new Queue(); + + var queue = _cachedInstances[uiKey]; + + for (int i = 0; i < count; i++) + { + var instance = Create(uiKey); + // 预加载的实例初始状态为隐藏 + instance.OnHide(); + queue.Enqueue(instance); + } + + Log.Debug("Preloaded {0} instances of {1}", count, uiKey); + } + + /// + /// 批量预加载 + /// + public void PreloadBatch(params string[] uiKeys) + { + foreach (var uiKey in uiKeys) + { + Preload(uiKey); + } + } + + /// + /// 回收实例到缓存池 + /// + public void Recycle(IUiPageBehavior page) + { + var uiKey = page.Key; + + if (!_cachedInstances.ContainsKey(uiKey)) + _cachedInstances[uiKey] = new Queue(); + + // 确保实例处于隐藏状态 + page.OnHide(); + + _cachedInstances[uiKey].Enqueue(page); + Log.Debug("Recycled UI instance to pool: {0}, poolSize={1}", uiKey, _cachedInstances[uiKey].Count); + } + + /// + /// 清理指定UI的缓存 + /// + public void ClearCache(string uiKey) + { + if (!_cachedInstances.TryGetValue(uiKey, out var queue)) + return; + + int count = queue.Count; + while (queue.Count > 0) + { + var instance = queue.Dequeue(); + DestroyInstance(instance); + } + + _cachedInstances.Remove(uiKey); + Log.Debug("Cleared cache for UI: {0}, destroyed {1} instances", uiKey, count); + } + + /// + /// 清理所有缓存 + /// + public void ClearAllCache() + { + foreach (var uiKey in _cachedInstances.Keys) + { + ClearCache(uiKey); + } + + Log.Debug("Cleared all UI caches"); + } + + /// + /// 检查是否有缓存的实例 + /// + public bool HasCached(string uiKey) + { + return _cachedInstances.TryGetValue(uiKey, out var queue) && queue.Count > 0; + } + protected override void OnInit() { _registry = this.GetUtility()!; } + + #region Private Methods + + /// + /// 获取缓存实例或创建新实例(Reuse策略) + /// + private IUiPageBehavior GetCachedOrCreate(string uiKey) + { + // 优先从缓存池获取 + if (_cachedInstances.TryGetValue(uiKey, out var queue) && queue.Count > 0) + { + var cached = queue.Dequeue(); + Log.Debug("Reused cached UI instance: {0}, remainingInPool={1}", uiKey, queue.Count); + return cached; + } + + // 没有缓存则创建新实例 + Log.Debug("No cached instance, creating new: {0}", uiKey); + return Create(uiKey); + } + + /// + /// 从池中获取或创建(Pooled策略) + /// 如果池为空,自动创建并填充 + /// + private IUiPageBehavior GetFromPoolOrCreate(string uiKey) + { + // 如果池为空,先预加载一个 + if (HasCached(uiKey)) return GetCachedOrCreate(uiKey); + Log.Debug("Pool empty, preloading instance: {0}", uiKey); + Preload(uiKey); + + return GetCachedOrCreate(uiKey); + } + + /// + /// 销毁实例 + /// + private void DestroyInstance(IUiPageBehavior page) + { + var uiKey = page.Key; + + // 从追踪列表移除 + if (_allInstances.TryGetValue(uiKey, out var set)) + { + set.Remove(page); + } + + // 销毁Godot节点 + if (page.View is Node node) + { + node.QueueFreeX(); + } + } + + #endregion } \ No newline at end of file