feat(pause): 添加暂停栈管理系统

- 实现了 PauseStackManager 核心管理器,支持嵌套暂停和分组管理
- 添加了 PauseToken 暂停令牌和 PauseGroup 暂停组枚举
- 创建了 PauseScope 作用域类,支持 using 语法自动管理暂停生命周期
- 实现了线程安全的暂停栈操作,包括 Push、Pop 和状态查询
- 添加了暂停处理器接口 IPauseHandler 和 Godot 平台具体实现
- 提供了完整的单元测试覆盖基础功能、嵌套暂停、分组管理和线程安全场景
This commit is contained in:
GeWuYou 2026-02-27 23:29:09 +08:00 committed by gewuyou
parent aa13760748
commit 7734fba56f
9 changed files with 1144 additions and 0 deletions

View 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);
}

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

View 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
}

View 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})";
}

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

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

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

View 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()
{
}
}

View 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}");
}
}
}