mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
feat(pause): 添加暂停栈管理系统
- 实现了 PauseStackManager 核心管理器,支持嵌套暂停和分组管理 - 添加了 PauseToken 暂停令牌和 PauseGroup 暂停组枚举 - 创建了 PauseScope 作用域类,支持 using 语法自动管理暂停生命周期 - 实现了线程安全的暂停栈操作,包括 Push、Pop 和状态查询 - 添加了暂停处理器接口 IPauseHandler 和 Godot 平台具体实现 - 提供了完整的单元测试覆盖基础功能、嵌套暂停、分组管理和线程安全场景
This commit is contained in:
parent
aa13760748
commit
7734fba56f
19
GFramework.Core.Abstractions/pause/IPauseHandler.cs
Normal file
19
GFramework.Core.Abstractions/pause/IPauseHandler.cs
Normal file
@ -0,0 +1,19 @@
|
||||
namespace GFramework.Core.Abstractions.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停处理器接口,由引擎层实现具体的暂停/恢复逻辑
|
||||
/// </summary>
|
||||
public interface IPauseHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理器优先级(数值越小优先级越高)
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当某个组的暂停状态变化时调用
|
||||
/// </summary>
|
||||
/// <param name="group">暂停组</param>
|
||||
/// <param name="isPaused">是否暂停</param>
|
||||
void OnPauseStateChanged(PauseGroup group, bool isPaused);
|
||||
}
|
||||
81
GFramework.Core.Abstractions/pause/IPauseStackManager.cs
Normal file
81
GFramework.Core.Abstractions/pause/IPauseStackManager.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using GFramework.Core.Abstractions.utility;
|
||||
|
||||
namespace GFramework.Core.Abstractions.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停栈管理器接口,管理嵌套暂停状态
|
||||
/// </summary>
|
||||
public interface IPauseStackManager : IContextUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// 推入暂停请求
|
||||
/// </summary>
|
||||
/// <param name="reason">暂停原因(用于调试)</param>
|
||||
/// <param name="group">暂停组</param>
|
||||
/// <returns>暂停令牌(用于后续恢复)</returns>
|
||||
PauseToken Push(string reason, PauseGroup group = PauseGroup.Global);
|
||||
|
||||
/// <summary>
|
||||
/// 弹出暂停请求
|
||||
/// </summary>
|
||||
/// <param name="token">暂停令牌</param>
|
||||
/// <returns>是否成功弹出</returns>
|
||||
bool Pop(PauseToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定组是否暂停
|
||||
/// </summary>
|
||||
/// <param name="group">暂停组</param>
|
||||
/// <returns>是否暂停</returns>
|
||||
bool IsPaused(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定组的暂停深度(栈中元素数量)
|
||||
/// </summary>
|
||||
/// <param name="group">暂停组</param>
|
||||
/// <returns>暂停深度</returns>
|
||||
int GetPauseDepth(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定组的所有暂停原因
|
||||
/// </summary>
|
||||
/// <param name="group">暂停组</param>
|
||||
/// <returns>暂停原因列表</returns>
|
||||
IReadOnlyList<string> GetPauseReasons(PauseGroup group = PauseGroup.Global);
|
||||
|
||||
/// <summary>
|
||||
/// 创建暂停作用域(支持 using 语法)
|
||||
/// </summary>
|
||||
/// <param name="reason">暂停原因</param>
|
||||
/// <param name="group">暂停组</param>
|
||||
/// <returns>可释放的作用域对象</returns>
|
||||
IDisposable PauseScope(string reason, PauseGroup group = PauseGroup.Global);
|
||||
|
||||
/// <summary>
|
||||
/// 清空指定组的所有暂停请求
|
||||
/// </summary>
|
||||
/// <param name="group">暂停组</param>
|
||||
void ClearGroup(PauseGroup group);
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有暂停请求
|
||||
/// </summary>
|
||||
void ClearAll();
|
||||
|
||||
/// <summary>
|
||||
/// 注册暂停处理器
|
||||
/// </summary>
|
||||
/// <param name="handler">处理器实例</param>
|
||||
void RegisterHandler(IPauseHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 注销暂停处理器
|
||||
/// </summary>
|
||||
/// <param name="handler">处理器实例</param>
|
||||
void UnregisterHandler(IPauseHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// 暂停状态变化事件
|
||||
/// </summary>
|
||||
event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
}
|
||||
42
GFramework.Core.Abstractions/pause/PauseGroup.cs
Normal file
42
GFramework.Core.Abstractions/pause/PauseGroup.cs
Normal file
@ -0,0 +1,42 @@
|
||||
namespace GFramework.Core.Abstractions.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停组枚举,定义不同的暂停作用域
|
||||
/// </summary>
|
||||
public enum PauseGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局暂停(影响所有系统)
|
||||
/// </summary>
|
||||
Global = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 游戏逻辑暂停(不影响 UI)
|
||||
/// </summary>
|
||||
Gameplay = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 动画暂停
|
||||
/// </summary>
|
||||
Animation = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 音频暂停
|
||||
/// </summary>
|
||||
Audio = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 自定义组 1
|
||||
/// </summary>
|
||||
Custom1 = 10,
|
||||
|
||||
/// <summary>
|
||||
/// 自定义组 2
|
||||
/// </summary>
|
||||
Custom2 = 11,
|
||||
|
||||
/// <summary>
|
||||
/// 自定义组 3
|
||||
/// </summary>
|
||||
Custom3 = 12
|
||||
}
|
||||
43
GFramework.Core.Abstractions/pause/PauseToken.cs
Normal file
43
GFramework.Core.Abstractions/pause/PauseToken.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace GFramework.Core.Abstractions.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停令牌,唯一标识一个暂停请求
|
||||
/// </summary>
|
||||
public readonly struct PauseToken : IEquatable<PauseToken>
|
||||
{
|
||||
/// <summary>
|
||||
/// 令牌 ID
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为有效令牌
|
||||
/// </summary>
|
||||
public bool IsValid => Id != Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 创建暂停令牌
|
||||
/// </summary>
|
||||
/// <param name="id">令牌 ID</param>
|
||||
public PauseToken(Guid id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建无效令牌
|
||||
/// </summary>
|
||||
public static PauseToken Invalid => new(Guid.Empty);
|
||||
|
||||
public bool Equals(PauseToken other) => Id.Equals(other.Id);
|
||||
|
||||
public override bool Equals(object? obj) => obj is PauseToken other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
|
||||
public static bool operator ==(PauseToken left, PauseToken right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(PauseToken left, PauseToken right) => !left.Equals(right);
|
||||
|
||||
public override string ToString() => $"PauseToken({Id})";
|
||||
}
|
||||
473
GFramework.Core.Tests/pause/PauseStackManagerTests.cs
Normal file
473
GFramework.Core.Tests/pause/PauseStackManagerTests.cs
Normal file
@ -0,0 +1,473 @@
|
||||
using GFramework.Core.Abstractions.pause;
|
||||
using GFramework.Core.pause;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GFramework.Core.Tests.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停栈管理器单元测试
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class PauseStackManagerTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 在每个测试方法执行前设置测试环境
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_manager = new PauseStackManager();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在每个测试方法执行后清理资源
|
||||
/// </summary>
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_manager.DestroyAsync();
|
||||
}
|
||||
|
||||
private PauseStackManager _manager = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 验证Push方法返回有效的令牌
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Push_Should_ReturnValidToken()
|
||||
{
|
||||
var token = _manager.Push("Test pause");
|
||||
|
||||
Assert.That(token.IsValid, Is.True);
|
||||
Assert.That(token.Id, Is.Not.EqualTo(Guid.Empty));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Push方法设置暂停状态
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Push_Should_SetPausedState()
|
||||
{
|
||||
Assert.That(_manager.IsPaused(), Is.False);
|
||||
|
||||
_manager.Push("Test pause");
|
||||
|
||||
Assert.That(_manager.IsPaused(), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Pop方法清除暂停状态
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Pop_Should_ClearPausedState()
|
||||
{
|
||||
var token = _manager.Push("Test pause");
|
||||
Assert.That(_manager.IsPaused(), Is.True);
|
||||
|
||||
_manager.Pop(token);
|
||||
|
||||
Assert.That(_manager.IsPaused(), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Pop无效令牌返回false
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Pop_WithInvalidToken_Should_ReturnFalse()
|
||||
{
|
||||
var result = _manager.Pop(PauseToken.Invalid);
|
||||
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Pop过期令牌返回false
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Pop_WithExpiredToken_Should_ReturnFalse()
|
||||
{
|
||||
var token = _manager.Push("Test pause");
|
||||
_manager.Pop(token);
|
||||
|
||||
// 尝试再次 Pop 同一个 Token
|
||||
var result = _manager.Pop(token);
|
||||
|
||||
Assert.That(result, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证多次Push增加深度
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MultiplePush_Should_IncreaseDepth()
|
||||
{
|
||||
_manager.Push("First");
|
||||
_manager.Push("Second");
|
||||
_manager.Push("Third");
|
||||
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证嵌套暂停需要所有Pop才能恢复
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void NestedPause_Should_RequireAllPops()
|
||||
{
|
||||
var token1 = _manager.Push("First");
|
||||
var token2 = _manager.Push("Second");
|
||||
|
||||
_manager.Pop(token1);
|
||||
Assert.That(_manager.IsPaused(), Is.True, "Should still be paused after first pop");
|
||||
|
||||
_manager.Pop(token2);
|
||||
Assert.That(_manager.IsPaused(), Is.False, "Should be unpaused after all pops");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Pop非栈顶令牌可以正常工作
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Pop_WithNonTopToken_Should_Work()
|
||||
{
|
||||
var token1 = _manager.Push("First");
|
||||
var token2 = _manager.Push("Second");
|
||||
var token3 = _manager.Push("Third");
|
||||
|
||||
// Pop 中间的 token
|
||||
var result = _manager.Pop(token2);
|
||||
|
||||
Assert.That(result, Is.True);
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(2));
|
||||
Assert.That(_manager.IsPaused(), Is.True);
|
||||
|
||||
// 验证剩余的令牌仍然有效
|
||||
Assert.That(_manager.Pop(token1), Is.True);
|
||||
Assert.That(_manager.Pop(token3), Is.True);
|
||||
Assert.That(_manager.IsPaused(), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证不同组独立工作
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void DifferentGroups_Should_BeIndependent()
|
||||
{
|
||||
_manager.Push("Global pause", PauseGroup.Global);
|
||||
_manager.Push("Gameplay pause", PauseGroup.Gameplay);
|
||||
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Global), Is.True);
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Gameplay), Is.True);
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Pop只影响正确的组
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Pop_Should_OnlyAffectCorrectGroup()
|
||||
{
|
||||
var globalToken = _manager.Push("Global");
|
||||
var gameplayToken = _manager.Push("Gameplay", PauseGroup.Gameplay);
|
||||
|
||||
_manager.Pop(globalToken);
|
||||
|
||||
Assert.That(_manager.IsPaused(), Is.False);
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Gameplay), Is.True);
|
||||
|
||||
// 验证 gameplayToken 仍然有效并且可以被正常弹出
|
||||
Assert.That(_manager.Pop(gameplayToken), Is.True);
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Gameplay), Is.False);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 验证GetPauseReasons返回所有原因
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetPauseReasons_Should_ReturnAllReasons()
|
||||
{
|
||||
_manager.Push("Menu opened");
|
||||
_manager.Push("Dialog shown");
|
||||
_manager.Push("Inventory opened");
|
||||
|
||||
var reasons = _manager.GetPauseReasons();
|
||||
|
||||
Assert.That(reasons.Count, Is.EqualTo(3));
|
||||
Assert.That(reasons, Does.Contain("Menu opened"));
|
||||
Assert.That(reasons, Does.Contain("Dialog shown"));
|
||||
Assert.That(reasons, Does.Contain("Inventory opened"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证空栈GetPauseReasons返回空列表
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void GetPauseReasons_WithEmptyStack_Should_ReturnEmptyList()
|
||||
{
|
||||
var reasons = _manager.GetPauseReasons();
|
||||
|
||||
Assert.That(reasons, Is.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Push触发状态变化事件
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Push_Should_TriggerEventWhenStateChanges()
|
||||
{
|
||||
bool eventTriggered = false;
|
||||
PauseGroup? eventGroup = null;
|
||||
bool? eventIsPaused = null;
|
||||
|
||||
_manager.OnPauseStateChanged += (group, isPaused) =>
|
||||
{
|
||||
eventTriggered = true;
|
||||
eventGroup = group;
|
||||
eventIsPaused = isPaused;
|
||||
};
|
||||
|
||||
_manager.Push("Test", PauseGroup.Gameplay);
|
||||
|
||||
Assert.That(eventTriggered, Is.True);
|
||||
Assert.That(eventGroup, Is.EqualTo(PauseGroup.Gameplay));
|
||||
Assert.That(eventIsPaused, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证Pop在栈变空时触发事件
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Pop_Should_TriggerEventWhenStackBecomesEmpty()
|
||||
{
|
||||
var token = _manager.Push("Test");
|
||||
|
||||
bool eventTriggered = false;
|
||||
_manager.OnPauseStateChanged += (group, isPaused) =>
|
||||
{
|
||||
eventTriggered = true;
|
||||
Assert.That(isPaused, Is.False);
|
||||
};
|
||||
|
||||
_manager.Pop(token);
|
||||
Assert.That(eventTriggered, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证多次Push只触发一次事件
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MultiplePush_Should_OnlyTriggerEventOnce()
|
||||
{
|
||||
int eventCount = 0;
|
||||
_manager.OnPauseStateChanged += (_, _) => eventCount++;
|
||||
|
||||
_manager.Push("First");
|
||||
_manager.Push("Second");
|
||||
|
||||
Assert.That(eventCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证处理器在状态变化时被通知
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Handler_Should_BeNotifiedOnStateChange()
|
||||
{
|
||||
var mockHandler = new MockPauseHandler();
|
||||
_manager.RegisterHandler(mockHandler);
|
||||
|
||||
_manager.Push("Test", PauseGroup.Global);
|
||||
|
||||
Assert.That(mockHandler.CallCount, Is.EqualTo(1));
|
||||
Assert.That(mockHandler.LastGroup, Is.EqualTo(PauseGroup.Global));
|
||||
Assert.That(mockHandler.LastIsPaused, Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证处理器在恢复时被通知
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Handler_Should_BeNotifiedOnResume()
|
||||
{
|
||||
var mockHandler = new MockPauseHandler();
|
||||
_manager.RegisterHandler(mockHandler);
|
||||
|
||||
var token = _manager.Push("Test");
|
||||
mockHandler.Reset();
|
||||
|
||||
_manager.Pop(token);
|
||||
|
||||
Assert.That(mockHandler.CallCount, Is.EqualTo(1));
|
||||
Assert.That(mockHandler.LastIsPaused, Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证多个处理器按优先级顺序调用
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void MultipleHandlers_Should_BeCalledInPriorityOrder()
|
||||
{
|
||||
var calls = new List<int>();
|
||||
|
||||
var handler1 = new MockPauseHandler { Priority = 10 };
|
||||
var handler2 = new MockPauseHandler { Priority = 5 };
|
||||
var handler3 = new MockPauseHandler { Priority = 15 };
|
||||
|
||||
handler1.OnCall = () => calls.Add(1);
|
||||
handler2.OnCall = () => calls.Add(2);
|
||||
handler3.OnCall = () => calls.Add(3);
|
||||
|
||||
_manager.RegisterHandler(handler1);
|
||||
_manager.RegisterHandler(handler2);
|
||||
_manager.RegisterHandler(handler3);
|
||||
|
||||
_manager.Push("Test");
|
||||
|
||||
Assert.That(calls, Is.EqualTo(new[] { 2, 1, 3 }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证PauseScope在Dispose时自动恢复
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void PauseScope_Should_AutoResumeOnDispose()
|
||||
{
|
||||
using (_manager.PauseScope("Test"))
|
||||
{
|
||||
Assert.That(_manager.IsPaused(), Is.True);
|
||||
}
|
||||
|
||||
Assert.That(_manager.IsPaused(), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证嵌套PauseScope正常工作
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void NestedPauseScope_Should_Work()
|
||||
{
|
||||
using (_manager.PauseScope("Outer"))
|
||||
{
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(1));
|
||||
|
||||
using (_manager.PauseScope("Inner"))
|
||||
{
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证ClearGroup移除指定组的所有暂停
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ClearGroup_Should_RemoveAllPausesForGroup()
|
||||
{
|
||||
_manager.Push("First", PauseGroup.Gameplay);
|
||||
_manager.Push("Second", PauseGroup.Gameplay);
|
||||
_manager.Push("Third", PauseGroup.Audio);
|
||||
|
||||
_manager.ClearGroup(PauseGroup.Gameplay);
|
||||
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Gameplay), Is.False);
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证ClearAll移除所有暂停
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ClearAll_Should_RemoveAllPauses()
|
||||
{
|
||||
_manager.Push("First", PauseGroup.Global);
|
||||
_manager.Push("Second", PauseGroup.Gameplay);
|
||||
_manager.Push("Third", PauseGroup.Audio);
|
||||
|
||||
_manager.ClearAll();
|
||||
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Global), Is.False);
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Gameplay), Is.False);
|
||||
Assert.That(_manager.IsPaused(PauseGroup.Audio), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证并发Push是线程安全的
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConcurrentPush_Should_BeThreadSafe()
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
var tokens = new List<PauseToken>();
|
||||
var lockObj = new object();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var index = i;
|
||||
tasks.Add(Task.Run(() =>
|
||||
{
|
||||
var token = _manager.Push($"Pause {index}");
|
||||
lock (lockObj)
|
||||
{
|
||||
tokens.Add(token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(100));
|
||||
Assert.That(tokens.Count, Is.EqualTo(100));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证并发Pop是线程安全的
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConcurrentPop_Should_BeThreadSafe()
|
||||
{
|
||||
var tokens = new List<PauseToken>();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
tokens.Add(_manager.Push($"Pause {i}"));
|
||||
}
|
||||
|
||||
var tasks = tokens.Select(token => Task.Run(() => _manager.Pop(token))).ToList();
|
||||
|
||||
Task.WaitAll(tasks.ToArray());
|
||||
|
||||
Assert.That(_manager.GetPauseDepth(), Is.EqualTo(0));
|
||||
Assert.That(_manager.IsPaused(), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试用的暂停处理器实现
|
||||
/// </summary>
|
||||
private class MockPauseHandler : IPauseHandler
|
||||
{
|
||||
public int CallCount { get; private set; }
|
||||
public PauseGroup? LastGroup { get; private set; }
|
||||
public bool? LastIsPaused { get; private set; }
|
||||
public Action? OnCall { get; set; }
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
CallCount++;
|
||||
LastGroup = group;
|
||||
LastIsPaused = isPaused;
|
||||
OnCall?.Invoke();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
CallCount = 0;
|
||||
LastGroup = null;
|
||||
LastIsPaused = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
GFramework.Core/pause/PauseEntry.cs
Normal file
29
GFramework.Core/pause/PauseEntry.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using GFramework.Core.Abstractions.pause;
|
||||
|
||||
namespace GFramework.Core.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停条目(内部数据结构)
|
||||
/// </summary>
|
||||
internal class PauseEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 令牌 ID
|
||||
/// </summary>
|
||||
public required Guid TokenId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 暂停原因
|
||||
/// </summary>
|
||||
public required string Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 暂停组
|
||||
/// </summary>
|
||||
public required PauseGroup Group { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间戳
|
||||
/// </summary>
|
||||
public required DateTime Timestamp { get; init; }
|
||||
}
|
||||
51
GFramework.Core/pause/PauseScope.cs
Normal file
51
GFramework.Core/pause/PauseScope.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using GFramework.Core.Abstractions.pause;
|
||||
|
||||
namespace GFramework.Core.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停作用域,支持 using 语法自动管理暂停生命周期
|
||||
/// </summary>
|
||||
public class PauseScope : IDisposable
|
||||
{
|
||||
private readonly IPauseStackManager _manager;
|
||||
private readonly PauseToken _token;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 创建暂停作用域
|
||||
/// </summary>
|
||||
/// <param name="manager">暂停栈管理器</param>
|
||||
/// <param name="reason">暂停原因</param>
|
||||
/// <param name="group">暂停组</param>
|
||||
public PauseScope(IPauseStackManager manager, string reason, PauseGroup group = PauseGroup.Global)
|
||||
{
|
||||
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||
_token = _manager.Push(reason, group);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放作用域,自动恢复暂停
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
/// <param name="disposing">是否正在显式释放</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_manager.Pop(_token);
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
364
GFramework.Core/pause/PauseStackManager.cs
Normal file
364
GFramework.Core/pause/PauseStackManager.cs
Normal file
@ -0,0 +1,364 @@
|
||||
using GFramework.Core.Abstractions.lifecycle;
|
||||
using GFramework.Core.Abstractions.logging;
|
||||
using GFramework.Core.Abstractions.pause;
|
||||
using GFramework.Core.logging;
|
||||
using GFramework.Core.utility;
|
||||
|
||||
namespace GFramework.Core.pause;
|
||||
|
||||
/// <summary>
|
||||
/// 暂停栈管理器实现,用于管理游戏中的暂停状态。
|
||||
/// 支持多组暂停、嵌套暂停、以及暂停状态的通知机制。
|
||||
/// </summary>
|
||||
public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAsyncDestroyable
|
||||
{
|
||||
private readonly List<IPauseHandler> _handlers = new();
|
||||
private readonly ReaderWriterLockSlim _lock = new();
|
||||
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PauseStackManager));
|
||||
private readonly Dictionary<PauseGroup, Stack<PauseEntry>> _pauseStacks = new();
|
||||
private readonly Dictionary<Guid, PauseEntry> _tokenMap = new();
|
||||
|
||||
/// <summary>
|
||||
/// 异步销毁方法,在组件销毁时调用。
|
||||
/// </summary>
|
||||
/// <returns>表示异步操作完成的任务。</returns>
|
||||
public ValueTask DestroyAsync()
|
||||
{
|
||||
_lock.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停状态变化事件,当暂停状态发生改变时触发。
|
||||
/// </summary>
|
||||
public event Action<PauseGroup, bool>? OnPauseStateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 推入一个新的暂停请求到指定的暂停组中。
|
||||
/// </summary>
|
||||
/// <param name="reason">暂停的原因描述。</param>
|
||||
/// <param name="group">暂停组,默认为全局暂停组。</param>
|
||||
/// <returns>表示此次暂停请求的令牌。</returns>
|
||||
public PauseToken Push(string reason, PauseGroup group = PauseGroup.Global)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var wasPaused = IsPausedInternal(group);
|
||||
|
||||
var entry = new PauseEntry
|
||||
{
|
||||
TokenId = Guid.NewGuid(),
|
||||
Reason = reason,
|
||||
Group = group,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
if (!_pauseStacks.TryGetValue(group, out var stack))
|
||||
{
|
||||
stack = new Stack<PauseEntry>();
|
||||
_pauseStacks[group] = stack;
|
||||
}
|
||||
|
||||
stack.Push(entry);
|
||||
_tokenMap[entry.TokenId] = entry;
|
||||
|
||||
_logger.Debug($"Pause pushed: {reason} (Group: {group}, Depth: {stack.Count})");
|
||||
|
||||
// 状态变化检测:从未暂停 → 暂停
|
||||
if (!wasPaused)
|
||||
{
|
||||
NotifyHandlers(group, true);
|
||||
}
|
||||
|
||||
return new PauseToken(entry.TokenId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹出指定的暂停请求。
|
||||
/// </summary>
|
||||
/// <param name="token">要弹出的暂停令牌。</param>
|
||||
/// <returns>如果成功弹出则返回true,否则返回false。</returns>
|
||||
public bool Pop(PauseToken token)
|
||||
{
|
||||
if (!token.IsValid)
|
||||
return false;
|
||||
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (!_tokenMap.TryGetValue(token.Id, out var entry))
|
||||
{
|
||||
_logger.Warn($"Attempted to pop invalid/expired token: {token.Id}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var group = entry.Group;
|
||||
var stack = _pauseStacks[group];
|
||||
var wasPaused = stack.Count > 0;
|
||||
|
||||
// 从栈中移除
|
||||
var tempStack = new Stack<PauseEntry>();
|
||||
bool found = false;
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var current = stack.Pop();
|
||||
if (current.TokenId == token.Id)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
tempStack.Push(current);
|
||||
}
|
||||
|
||||
// 恢复栈结构
|
||||
while (tempStack.Count > 0)
|
||||
{
|
||||
stack.Push(tempStack.Pop());
|
||||
}
|
||||
|
||||
if (found)
|
||||
{
|
||||
_tokenMap.Remove(token.Id);
|
||||
_logger.Debug($"Pause popped: {entry.Reason} (Group: {group}, Remaining: {stack.Count})");
|
||||
|
||||
// 状态变化检测:从暂停 → 未暂停
|
||||
if (wasPaused && stack.Count == 0)
|
||||
{
|
||||
NotifyHandlers(group, false);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询指定暂停组当前是否处于暂停状态。
|
||||
/// </summary>
|
||||
/// <param name="group">要查询的暂停组,默认为全局暂停组。</param>
|
||||
/// <returns>如果该组处于暂停状态则返回true,否则返回false。</returns>
|
||||
public bool IsPaused(PauseGroup group = PauseGroup.Global)
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return IsPausedInternal(group);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定暂停组的暂停深度(即嵌套暂停的层数)。
|
||||
/// </summary>
|
||||
/// <param name="group">要查询的暂停组,默认为全局暂停组。</param>
|
||||
/// <returns>暂停深度,0表示未暂停。</returns>
|
||||
public int GetPauseDepth(PauseGroup group = PauseGroup.Global)
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _pauseStacks.TryGetValue(group, out var stack) ? stack.Count : 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定暂停组的所有暂停原因。
|
||||
/// </summary>
|
||||
/// <param name="group">要查询的暂停组,默认为全局暂停组。</param>
|
||||
/// <returns>包含所有暂停原因的只读列表。</returns>
|
||||
public IReadOnlyList<string> GetPauseReasons(PauseGroup group = PauseGroup.Global)
|
||||
{
|
||||
_lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (!_pauseStacks.TryGetValue(group, out var stack))
|
||||
return Array.Empty<string>();
|
||||
|
||||
return stack.Select(e => e.Reason).ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建一个暂停作用域,支持 using 语法自动管理暂停生命周期。
|
||||
/// </summary>
|
||||
/// <param name="reason">暂停的原因描述。</param>
|
||||
/// <param name="group">暂停组,默认为全局暂停组。</param>
|
||||
/// <returns>表示暂停作用域的 IDisposable 对象。</returns>
|
||||
public IDisposable PauseScope(string reason, PauseGroup group = PauseGroup.Global)
|
||||
{
|
||||
return new PauseScope(this, reason, group);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空指定暂停组的所有暂停请求。
|
||||
/// </summary>
|
||||
/// <param name="group">要清空的暂停组。</param>
|
||||
public void ClearGroup(PauseGroup group)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (!_pauseStacks.TryGetValue(group, out var stack))
|
||||
return;
|
||||
|
||||
var wasPaused = stack.Count > 0;
|
||||
|
||||
// 移除所有令牌
|
||||
foreach (var entry in stack)
|
||||
{
|
||||
_tokenMap.Remove(entry.TokenId);
|
||||
}
|
||||
|
||||
stack.Clear();
|
||||
|
||||
_logger.Warn($"Cleared all pauses for group: {group}");
|
||||
|
||||
// 状态变化检测
|
||||
if (wasPaused)
|
||||
{
|
||||
NotifyHandlers(group, false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有暂停组的所有暂停请求。
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var pausedGroups = _pauseStacks
|
||||
.Where(kvp => kvp.Value.Count > 0)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
_pauseStacks.Clear();
|
||||
_tokenMap.Clear();
|
||||
|
||||
_logger.Warn("Cleared all pauses for all groups");
|
||||
|
||||
// 通知所有之前暂停的组
|
||||
foreach (var group in pausedGroups)
|
||||
{
|
||||
NotifyHandlers(group, false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个暂停处理器,用于监听暂停状态的变化。
|
||||
/// </summary>
|
||||
/// <param name="handler">要注册的暂停处理器。</param>
|
||||
public void RegisterHandler(IPauseHandler handler)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (!_handlers.Contains(handler))
|
||||
{
|
||||
_handlers.Add(handler);
|
||||
_logger.Debug($"Registered pause handler: {handler.GetType().Name}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注销一个已注册的暂停处理器。
|
||||
/// </summary>
|
||||
/// <param name="handler">要注销的暂停处理器。</param>
|
||||
public void UnregisterHandler(IPauseHandler handler)
|
||||
{
|
||||
_lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (_handlers.Remove(handler))
|
||||
{
|
||||
_logger.Debug($"Unregistered pause handler: {handler.GetType().Name}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部查询暂停状态的方法,不加锁。
|
||||
/// </summary>
|
||||
/// <param name="group">要查询的暂停组。</param>
|
||||
/// <returns>如果该组处于暂停状态则返回true,否则返回false。</returns>
|
||||
private bool IsPausedInternal(PauseGroup group)
|
||||
{
|
||||
return _pauseStacks.TryGetValue(group, out var stack) && stack.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知所有已注册的处理器和事件订阅者暂停状态的变化。
|
||||
/// </summary>
|
||||
/// <param name="group">发生状态变化的暂停组。</param>
|
||||
/// <param name="isPaused">新的暂停状态。</param>
|
||||
private void NotifyHandlers(PauseGroup group, bool isPaused)
|
||||
{
|
||||
_logger.Debug($"Notifying handlers: Group={group}, IsPaused={isPaused}");
|
||||
|
||||
// 按优先级排序后通知
|
||||
foreach (var handler in _handlers.OrderBy(h => h.Priority))
|
||||
{
|
||||
try
|
||||
{
|
||||
handler.OnPauseStateChanged(group, isPaused);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Handler {handler.GetType().Name} failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
OnPauseStateChanged?.Invoke(group, isPaused);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化方法,在组件初始化时调用。
|
||||
/// </summary>
|
||||
protected override void OnInit()
|
||||
{
|
||||
}
|
||||
}
|
||||
42
GFramework.Godot/pause/GodotPauseHandler.cs
Normal file
42
GFramework.Godot/pause/GodotPauseHandler.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using GFramework.Core.Abstractions.pause;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.pause;
|
||||
|
||||
/// <summary>
|
||||
/// Godot 引擎的暂停处理器
|
||||
/// 响应暂停栈状态变化,控制 SceneTree.Paused
|
||||
/// </summary>
|
||||
public class GodotPauseHandler : IPauseHandler
|
||||
{
|
||||
private readonly SceneTree _tree;
|
||||
|
||||
/// <summary>
|
||||
/// 创建 Godot 暂停处理器
|
||||
/// </summary>
|
||||
/// <param name="tree">场景树</param>
|
||||
public GodotPauseHandler(SceneTree tree)
|
||||
{
|
||||
_tree = tree ?? throw new ArgumentNullException(nameof(tree));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理器优先级
|
||||
/// </summary>
|
||||
public int Priority => 0;
|
||||
|
||||
/// <summary>
|
||||
/// 当暂停状态变化时调用
|
||||
/// </summary>
|
||||
/// <param name="group">暂停组</param>
|
||||
/// <param name="isPaused">是否暂停</param>
|
||||
public void OnPauseStateChanged(PauseGroup group, bool isPaused)
|
||||
{
|
||||
// 只有 Global 组影响 Godot 的全局暂停
|
||||
if (group == PauseGroup.Global)
|
||||
{
|
||||
_tree.Paused = isPaused;
|
||||
GD.Print($"[GodotPauseHandler] SceneTree.Paused = {isPaused}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user