diff --git a/GFramework.Core.Abstractions/pause/IPauseHandler.cs b/GFramework.Core.Abstractions/pause/IPauseHandler.cs new file mode 100644 index 0000000..f8ac3fe --- /dev/null +++ b/GFramework.Core.Abstractions/pause/IPauseHandler.cs @@ -0,0 +1,19 @@ +namespace GFramework.Core.Abstractions.pause; + +/// +/// 暂停处理器接口,由引擎层实现具体的暂停/恢复逻辑 +/// +public interface IPauseHandler +{ + /// + /// 处理器优先级(数值越小优先级越高) + /// + int Priority { get; } + + /// + /// 当某个组的暂停状态变化时调用 + /// + /// 暂停组 + /// 是否暂停 + void OnPauseStateChanged(PauseGroup group, bool isPaused); +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/pause/IPauseStackManager.cs b/GFramework.Core.Abstractions/pause/IPauseStackManager.cs new file mode 100644 index 0000000..ae06c9b --- /dev/null +++ b/GFramework.Core.Abstractions/pause/IPauseStackManager.cs @@ -0,0 +1,81 @@ +using GFramework.Core.Abstractions.utility; + +namespace GFramework.Core.Abstractions.pause; + +/// +/// 暂停栈管理器接口,管理嵌套暂停状态 +/// +public interface IPauseStackManager : IContextUtility +{ + /// + /// 推入暂停请求 + /// + /// 暂停原因(用于调试) + /// 暂停组 + /// 暂停令牌(用于后续恢复) + PauseToken Push(string reason, PauseGroup group = PauseGroup.Global); + + /// + /// 弹出暂停请求 + /// + /// 暂停令牌 + /// 是否成功弹出 + bool Pop(PauseToken token); + + /// + /// 查询指定组是否暂停 + /// + /// 暂停组 + /// 是否暂停 + bool IsPaused(PauseGroup group = PauseGroup.Global); + + /// + /// 获取指定组的暂停深度(栈中元素数量) + /// + /// 暂停组 + /// 暂停深度 + int GetPauseDepth(PauseGroup group = PauseGroup.Global); + + /// + /// 获取指定组的所有暂停原因 + /// + /// 暂停组 + /// 暂停原因列表 + IReadOnlyList GetPauseReasons(PauseGroup group = PauseGroup.Global); + + /// + /// 创建暂停作用域(支持 using 语法) + /// + /// 暂停原因 + /// 暂停组 + /// 可释放的作用域对象 + IDisposable PauseScope(string reason, PauseGroup group = PauseGroup.Global); + + /// + /// 清空指定组的所有暂停请求 + /// + /// 暂停组 + void ClearGroup(PauseGroup group); + + /// + /// 清空所有暂停请求 + /// + void ClearAll(); + + /// + /// 注册暂停处理器 + /// + /// 处理器实例 + void RegisterHandler(IPauseHandler handler); + + /// + /// 注销暂停处理器 + /// + /// 处理器实例 + void UnregisterHandler(IPauseHandler handler); + + /// + /// 暂停状态变化事件 + /// + event Action? OnPauseStateChanged; +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/pause/PauseGroup.cs b/GFramework.Core.Abstractions/pause/PauseGroup.cs new file mode 100644 index 0000000..68bbf15 --- /dev/null +++ b/GFramework.Core.Abstractions/pause/PauseGroup.cs @@ -0,0 +1,42 @@ +namespace GFramework.Core.Abstractions.pause; + +/// +/// 暂停组枚举,定义不同的暂停作用域 +/// +public enum PauseGroup +{ + /// + /// 全局暂停(影响所有系统) + /// + Global = 0, + + /// + /// 游戏逻辑暂停(不影响 UI) + /// + Gameplay = 1, + + /// + /// 动画暂停 + /// + Animation = 2, + + /// + /// 音频暂停 + /// + Audio = 3, + + /// + /// 自定义组 1 + /// + Custom1 = 10, + + /// + /// 自定义组 2 + /// + Custom2 = 11, + + /// + /// 自定义组 3 + /// + Custom3 = 12 +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/pause/PauseToken.cs b/GFramework.Core.Abstractions/pause/PauseToken.cs new file mode 100644 index 0000000..1b27802 --- /dev/null +++ b/GFramework.Core.Abstractions/pause/PauseToken.cs @@ -0,0 +1,43 @@ +namespace GFramework.Core.Abstractions.pause; + +/// +/// 暂停令牌,唯一标识一个暂停请求 +/// +public readonly struct PauseToken : IEquatable +{ + /// + /// 令牌 ID + /// + public Guid Id { get; } + + /// + /// 是否为有效令牌 + /// + public bool IsValid => Id != Guid.Empty; + + /// + /// 创建暂停令牌 + /// + /// 令牌 ID + public PauseToken(Guid id) + { + Id = id; + } + + /// + /// 创建无效令牌 + /// + 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})"; +} \ No newline at end of file diff --git a/GFramework.Core.Tests/pause/PauseStackManagerTests.cs b/GFramework.Core.Tests/pause/PauseStackManagerTests.cs new file mode 100644 index 0000000..79b77d2 --- /dev/null +++ b/GFramework.Core.Tests/pause/PauseStackManagerTests.cs @@ -0,0 +1,473 @@ +using GFramework.Core.Abstractions.pause; +using GFramework.Core.pause; +using NUnit.Framework; + +namespace GFramework.Core.Tests.pause; + +/// +/// 暂停栈管理器单元测试 +/// +[TestFixture] +public class PauseStackManagerTests +{ + /// + /// 在每个测试方法执行前设置测试环境 + /// + [SetUp] + public void SetUp() + { + _manager = new PauseStackManager(); + } + + /// + /// 在每个测试方法执行后清理资源 + /// + [TearDown] + public void TearDown() + { + _manager.DestroyAsync(); + } + + private PauseStackManager _manager = null!; + + /// + /// 验证Push方法返回有效的令牌 + /// + [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)); + } + + /// + /// 验证Push方法设置暂停状态 + /// + [Test] + public void Push_Should_SetPausedState() + { + Assert.That(_manager.IsPaused(), Is.False); + + _manager.Push("Test pause"); + + Assert.That(_manager.IsPaused(), Is.True); + } + + /// + /// 验证Pop方法清除暂停状态 + /// + [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); + } + + /// + /// 验证Pop无效令牌返回false + /// + [Test] + public void Pop_WithInvalidToken_Should_ReturnFalse() + { + var result = _manager.Pop(PauseToken.Invalid); + + Assert.That(result, Is.False); + } + + /// + /// 验证Pop过期令牌返回false + /// + [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); + } + + /// + /// 验证多次Push增加深度 + /// + [Test] + public void MultiplePush_Should_IncreaseDepth() + { + _manager.Push("First"); + _manager.Push("Second"); + _manager.Push("Third"); + + Assert.That(_manager.GetPauseDepth(), Is.EqualTo(3)); + } + + /// + /// 验证嵌套暂停需要所有Pop才能恢复 + /// + [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"); + } + + /// + /// 验证Pop非栈顶令牌可以正常工作 + /// + [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); + } + + /// + /// 验证不同组独立工作 + /// + [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); + } + + /// + /// 验证Pop只影响正确的组 + /// + [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); + } + + + /// + /// 验证GetPauseReasons返回所有原因 + /// + [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")); + } + + /// + /// 验证空栈GetPauseReasons返回空列表 + /// + [Test] + public void GetPauseReasons_WithEmptyStack_Should_ReturnEmptyList() + { + var reasons = _manager.GetPauseReasons(); + + Assert.That(reasons, Is.Empty); + } + + /// + /// 验证Push触发状态变化事件 + /// + [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); + } + + /// + /// 验证Pop在栈变空时触发事件 + /// + [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); + } + + /// + /// 验证多次Push只触发一次事件 + /// + [Test] + public void MultiplePush_Should_OnlyTriggerEventOnce() + { + int eventCount = 0; + _manager.OnPauseStateChanged += (_, _) => eventCount++; + + _manager.Push("First"); + _manager.Push("Second"); + + Assert.That(eventCount, Is.EqualTo(1)); + } + + /// + /// 验证处理器在状态变化时被通知 + /// + [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); + } + + /// + /// 验证处理器在恢复时被通知 + /// + [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); + } + + /// + /// 验证多个处理器按优先级顺序调用 + /// + [Test] + public void MultipleHandlers_Should_BeCalledInPriorityOrder() + { + var calls = new List(); + + 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 })); + } + + /// + /// 验证PauseScope在Dispose时自动恢复 + /// + [Test] + public void PauseScope_Should_AutoResumeOnDispose() + { + using (_manager.PauseScope("Test")) + { + Assert.That(_manager.IsPaused(), Is.True); + } + + Assert.That(_manager.IsPaused(), Is.False); + } + + /// + /// 验证嵌套PauseScope正常工作 + /// + [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)); + } + + /// + /// 验证ClearGroup移除指定组的所有暂停 + /// + [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); + } + + /// + /// 验证ClearAll移除所有暂停 + /// + [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); + } + + /// + /// 验证并发Push是线程安全的 + /// + [Test] + public void ConcurrentPush_Should_BeThreadSafe() + { + var tasks = new List(); + var tokens = new List(); + 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)); + } + + /// + /// 验证并发Pop是线程安全的 + /// + [Test] + public void ConcurrentPop_Should_BeThreadSafe() + { + var tokens = new List(); + 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); + } + + /// + /// 测试用的暂停处理器实现 + /// + 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; + } + } +} \ No newline at end of file diff --git a/GFramework.Core/pause/PauseEntry.cs b/GFramework.Core/pause/PauseEntry.cs new file mode 100644 index 0000000..68737f7 --- /dev/null +++ b/GFramework.Core/pause/PauseEntry.cs @@ -0,0 +1,29 @@ +using GFramework.Core.Abstractions.pause; + +namespace GFramework.Core.pause; + +/// +/// 暂停条目(内部数据结构) +/// +internal class PauseEntry +{ + /// + /// 令牌 ID + /// + public required Guid TokenId { get; init; } + + /// + /// 暂停原因 + /// + public required string Reason { get; init; } + + /// + /// 暂停组 + /// + public required PauseGroup Group { get; init; } + + /// + /// 创建时间戳 + /// + public required DateTime Timestamp { get; init; } +} \ No newline at end of file diff --git a/GFramework.Core/pause/PauseScope.cs b/GFramework.Core/pause/PauseScope.cs new file mode 100644 index 0000000..5040ebc --- /dev/null +++ b/GFramework.Core/pause/PauseScope.cs @@ -0,0 +1,51 @@ +using GFramework.Core.Abstractions.pause; + +namespace GFramework.Core.pause; + +/// +/// 暂停作用域,支持 using 语法自动管理暂停生命周期 +/// +public class PauseScope : IDisposable +{ + private readonly IPauseStackManager _manager; + private readonly PauseToken _token; + private bool _disposed; + + /// + /// 创建暂停作用域 + /// + /// 暂停栈管理器 + /// 暂停原因 + /// 暂停组 + public PauseScope(IPauseStackManager manager, string reason, PauseGroup group = PauseGroup.Global) + { + _manager = manager ?? throw new ArgumentNullException(nameof(manager)); + _token = _manager.Push(reason, group); + } + + /// + /// 释放作用域,自动恢复暂停 + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// 释放资源 + /// + /// 是否正在显式释放 + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + _manager.Pop(_token); + } + + _disposed = true; + } +} \ No newline at end of file diff --git a/GFramework.Core/pause/PauseStackManager.cs b/GFramework.Core/pause/PauseStackManager.cs new file mode 100644 index 0000000..e561bdf --- /dev/null +++ b/GFramework.Core/pause/PauseStackManager.cs @@ -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; + +/// +/// 暂停栈管理器实现,用于管理游戏中的暂停状态。 +/// 支持多组暂停、嵌套暂停、以及暂停状态的通知机制。 +/// +public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAsyncDestroyable +{ + private readonly List _handlers = new(); + private readonly ReaderWriterLockSlim _lock = new(); + private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(PauseStackManager)); + private readonly Dictionary> _pauseStacks = new(); + private readonly Dictionary _tokenMap = new(); + + /// + /// 异步销毁方法,在组件销毁时调用。 + /// + /// 表示异步操作完成的任务。 + public ValueTask DestroyAsync() + { + _lock.Dispose(); + return ValueTask.CompletedTask; + } + + /// + /// 暂停状态变化事件,当暂停状态发生改变时触发。 + /// + public event Action? OnPauseStateChanged; + + /// + /// 推入一个新的暂停请求到指定的暂停组中。 + /// + /// 暂停的原因描述。 + /// 暂停组,默认为全局暂停组。 + /// 表示此次暂停请求的令牌。 + 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(); + _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(); + } + } + + /// + /// 弹出指定的暂停请求。 + /// + /// 要弹出的暂停令牌。 + /// 如果成功弹出则返回true,否则返回false。 + 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(); + 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(); + } + } + + /// + /// 查询指定暂停组当前是否处于暂停状态。 + /// + /// 要查询的暂停组,默认为全局暂停组。 + /// 如果该组处于暂停状态则返回true,否则返回false。 + public bool IsPaused(PauseGroup group = PauseGroup.Global) + { + _lock.EnterReadLock(); + try + { + return IsPausedInternal(group); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// 获取指定暂停组的暂停深度(即嵌套暂停的层数)。 + /// + /// 要查询的暂停组,默认为全局暂停组。 + /// 暂停深度,0表示未暂停。 + public int GetPauseDepth(PauseGroup group = PauseGroup.Global) + { + _lock.EnterReadLock(); + try + { + return _pauseStacks.TryGetValue(group, out var stack) ? stack.Count : 0; + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// 获取指定暂停组的所有暂停原因。 + /// + /// 要查询的暂停组,默认为全局暂停组。 + /// 包含所有暂停原因的只读列表。 + public IReadOnlyList GetPauseReasons(PauseGroup group = PauseGroup.Global) + { + _lock.EnterReadLock(); + try + { + if (!_pauseStacks.TryGetValue(group, out var stack)) + return Array.Empty(); + + return stack.Select(e => e.Reason).ToList(); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// 创建一个暂停作用域,支持 using 语法自动管理暂停生命周期。 + /// + /// 暂停的原因描述。 + /// 暂停组,默认为全局暂停组。 + /// 表示暂停作用域的 IDisposable 对象。 + public IDisposable PauseScope(string reason, PauseGroup group = PauseGroup.Global) + { + return new PauseScope(this, reason, group); + } + + /// + /// 清空指定暂停组的所有暂停请求。 + /// + /// 要清空的暂停组。 + 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(); + } + } + + /// + /// 清空所有暂停组的所有暂停请求。 + /// + 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(); + } + } + + /// + /// 注册一个暂停处理器,用于监听暂停状态的变化。 + /// + /// 要注册的暂停处理器。 + 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(); + } + } + + /// + /// 注销一个已注册的暂停处理器。 + /// + /// 要注销的暂停处理器。 + public void UnregisterHandler(IPauseHandler handler) + { + _lock.EnterWriteLock(); + try + { + if (_handlers.Remove(handler)) + { + _logger.Debug($"Unregistered pause handler: {handler.GetType().Name}"); + } + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// 内部查询暂停状态的方法,不加锁。 + /// + /// 要查询的暂停组。 + /// 如果该组处于暂停状态则返回true,否则返回false。 + private bool IsPausedInternal(PauseGroup group) + { + return _pauseStacks.TryGetValue(group, out var stack) && stack.Count > 0; + } + + /// + /// 通知所有已注册的处理器和事件订阅者暂停状态的变化。 + /// + /// 发生状态变化的暂停组。 + /// 新的暂停状态。 + 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); + } + + /// + /// 初始化方法,在组件初始化时调用。 + /// + protected override void OnInit() + { + } +} \ No newline at end of file diff --git a/GFramework.Godot/pause/GodotPauseHandler.cs b/GFramework.Godot/pause/GodotPauseHandler.cs new file mode 100644 index 0000000..f77368c --- /dev/null +++ b/GFramework.Godot/pause/GodotPauseHandler.cs @@ -0,0 +1,42 @@ +using GFramework.Core.Abstractions.pause; +using Godot; + +namespace GFramework.Godot.pause; + +/// +/// Godot 引擎的暂停处理器 +/// 响应暂停栈状态变化,控制 SceneTree.Paused +/// +public class GodotPauseHandler : IPauseHandler +{ + private readonly SceneTree _tree; + + /// + /// 创建 Godot 暂停处理器 + /// + /// 场景树 + public GodotPauseHandler(SceneTree tree) + { + _tree = tree ?? throw new ArgumentNullException(nameof(tree)); + } + + /// + /// 处理器优先级 + /// + public int Priority => 0; + + /// + /// 当暂停状态变化时调用 + /// + /// 暂停组 + /// 是否暂停 + public void OnPauseStateChanged(PauseGroup group, bool isPaused) + { + // 只有 Global 组影响 Godot 的全局暂停 + if (group == PauseGroup.Global) + { + _tree.Paused = isPaused; + GD.Print($"[GodotPauseHandler] SceneTree.Paused = {isPaused}"); + } + } +} \ No newline at end of file