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