From 1c41c57d72cf530724529a56cdd43bd0208f4bab Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:06:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(coroutine):=20=E6=B7=BB=E5=8A=A0=E5=8D=8F?= =?UTF-8?q?=E7=A8=8B=E7=B3=BB=E7=BB=9F=E6=A0=B8=E5=BF=83=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=B8=8EGodot=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现CoroutineMetadata类存储协程元数据信息 - 创建CoroutineScheduler协程调度器管理协程生命周期 - 添加CoroutineSlot类管理单个协程执行状态 - 实现GodotTimeSource时间源支持缩放和真实时间 - 添加Timing类提供Godot协程管理功能 - 实现CoroutineNodeExtensions扩展方法支持节点生命周期管理 - 支持协程分组、标签、优先级等功能 - 提供协程暂停、恢复、终止等控制接口 - 实现协程统计和快照功能 - 添加等待指令处理机制支持多种等待类型 --- .../Coroutine/CoroutineCompletionStatus.cs | 28 + .../Coroutine/CoroutineExecutionStage.cs | 29 + .../CoroutineSchedulerAdvancedTests.cs | 174 +++++ .../Coroutine/CoroutineMetadata.cs | 6 + .../Coroutine/CoroutineScheduler.cs | 655 ++++++++++++++---- GFramework.Core/Coroutine/CoroutineSlot.cs | 6 + .../Coroutine/CoroutineSnapshot.cs | 29 + .../Coroutine/GodotTimeSourceTests.cs | 52 ++ .../GFramework.Godot.Tests.csproj | 23 + .../Coroutine/CoroutineNodeExtensions.cs | 19 + GFramework.Godot/Coroutine/GodotTimeSource.cs | 61 +- GFramework.Godot/Coroutine/Timing.cs | 652 ++++++++++++----- GFramework.csproj | 3 + GFramework.sln | 14 + 14 files changed, 1409 insertions(+), 342 deletions(-) create mode 100644 GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs create mode 100644 GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs create mode 100644 GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs create mode 100644 GFramework.Core/Coroutine/CoroutineSnapshot.cs create mode 100644 GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs create mode 100644 GFramework.Godot.Tests/GFramework.Godot.Tests.csproj diff --git a/GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs b/GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs new file mode 100644 index 00000000..a4b517ac --- /dev/null +++ b/GFramework.Core.Abstractions/Coroutine/CoroutineCompletionStatus.cs @@ -0,0 +1,28 @@ +namespace GFramework.Core.Abstractions.Coroutine; + +/// +/// 表示协程的最终完成结果。 +/// +public enum CoroutineCompletionStatus +{ + /// + /// 调度器无法确认该句柄的最终结果。 + /// 这通常意味着句柄无效,或者句柄对应的历史结果已经不可用。 + /// + Unknown, + + /// + /// 协程自然执行结束。 + /// + Completed, + + /// + /// 协程被外部终止、清空或取消令牌中断。 + /// + Cancelled, + + /// + /// 协程在推进过程中抛出了异常。 + /// + Faulted +} \ No newline at end of file diff --git a/GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs b/GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs new file mode 100644 index 00000000..7b405d4c --- /dev/null +++ b/GFramework.Core.Abstractions/Coroutine/CoroutineExecutionStage.cs @@ -0,0 +1,29 @@ +namespace GFramework.Core.Abstractions.Coroutine; + +/// +/// 表示协程调度器当前所处的执行阶段。 +/// +/// +/// 某些等待指令具有阶段语义,例如 WaitForFixedUpdateWaitForEndOfFrame。 +/// 宿主应为这些语义提供匹配的调度器阶段,否则这类等待不会自然完成。 +/// +public enum CoroutineExecutionStage +{ + /// + /// 默认更新阶段。 + /// 普通时间等待、下一帧等待以及大多数条件等待都会在该阶段推进。 + /// + Update, + + /// + /// 固定更新阶段。 + /// 仅与固定步相关的等待指令会在该阶段完成。 + /// + FixedUpdate, + + /// + /// 帧结束阶段。 + /// 仅与帧尾或延迟执行相关的等待指令会在该阶段完成。 + /// + EndOfFrame +} \ No newline at end of file diff --git a/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs new file mode 100644 index 00000000..0087b351 --- /dev/null +++ b/GFramework.Core.Tests/Coroutine/CoroutineSchedulerAdvancedTests.cs @@ -0,0 +1,174 @@ +using GFramework.Core.Abstractions.Coroutine; +using GFramework.Core.Coroutine; +using GFramework.Core.Coroutine.Instructions; + +namespace GFramework.Core.Tests.Coroutine; + +/// +/// 协程调度器增强行为测试。 +/// +[TestFixture] +public sealed class CoroutineSchedulerAdvancedTests +{ + /// + /// 验证 WaitForSecondsRealtime 使用独立的真实时间源推进。 + /// + [Test] + public void WaitForSecondsRealtime_Should_Use_Realtime_TimeSource_When_Provided() + { + var scaledTime = new FakeTimeSource(); + var realtimeTime = new FakeTimeSource(); + var scheduler = new CoroutineScheduler( + scaledTime, + realtimeTimeSource: realtimeTime); + var completed = false; + + IEnumerator Coroutine() + { + yield return new WaitForSecondsRealtime(1.0); + completed = true; + } + + scheduler.Run(Coroutine()); + + scaledTime.Advance(0.1); + realtimeTime.Advance(0.4); + scheduler.Update(); + + Assert.That(completed, Is.False); + Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1)); + + scaledTime.Advance(0.1); + realtimeTime.Advance(0.6); + scheduler.Update(); + + Assert.That(completed, Is.True); + Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(0)); + } + + /// + /// 验证固定更新等待指令仅在固定阶段调度器中推进。 + /// + [Test] + public void WaitForFixedUpdate_Should_Only_Advance_On_FixedUpdate_Scheduler() + { + var defaultTime = new FakeTimeSource(); + var fixedTime = new FakeTimeSource(); + var defaultScheduler = new CoroutineScheduler(defaultTime); + var fixedScheduler = new CoroutineScheduler( + fixedTime, + executionStage: CoroutineExecutionStage.FixedUpdate); + var defaultCompleted = false; + var fixedCompleted = false; + + IEnumerator DefaultCoroutine() + { + yield return new WaitForFixedUpdate(); + defaultCompleted = true; + } + + IEnumerator FixedCoroutine() + { + yield return new WaitForFixedUpdate(); + fixedCompleted = true; + } + + defaultScheduler.Run(DefaultCoroutine()); + fixedScheduler.Run(FixedCoroutine()); + + defaultTime.Advance(0.1); + fixedTime.Advance(0.1); + defaultScheduler.Update(); + fixedScheduler.Update(); + + Assert.That(defaultCompleted, Is.False); + Assert.That(defaultScheduler.ActiveCoroutineCount, Is.EqualTo(1)); + Assert.That(fixedCompleted, Is.True); + Assert.That(fixedScheduler.ActiveCoroutineCount, Is.EqualTo(0)); + } + + /// + /// 验证取消令牌会在下一次调度循环中终止协程并记录结果。 + /// + [Test] + public async Task CancellationToken_Should_Cancel_Coroutine_On_Next_Update() + { + var timeSource = new FakeTimeSource(); + var scheduler = new CoroutineScheduler(timeSource); + using var cancellationTokenSource = new CancellationTokenSource(); + + IEnumerator Coroutine() + { + yield return new Delay(10); + } + + var handle = scheduler.Run(Coroutine(), cancellationToken: cancellationTokenSource.Token); + cancellationTokenSource.Cancel(); + + timeSource.Advance(0.1); + scheduler.Update(); + + var status = await scheduler.WaitForCompletionAsync(handle); + + Assert.That(scheduler.IsCoroutineAlive(handle), Is.False); + Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Cancelled)); + } + + /// + /// 验证调度器可以暴露活跃协程快照。 + /// + [Test] + public void TryGetSnapshot_Should_Return_Current_Waiting_Instruction_And_Stage() + { + var timeSource = new FakeTimeSource(); + var scheduler = new CoroutineScheduler( + timeSource, + executionStage: CoroutineExecutionStage.EndOfFrame); + + IEnumerator Coroutine() + { + yield return new WaitForEndOfFrame(); + } + + var handle = scheduler.Run(Coroutine(), tag: "ui", group: "frame-end"); + + var found = scheduler.TryGetSnapshot(handle, out var snapshot); + + Assert.That(found, Is.True); + Assert.That(snapshot.Handle, Is.EqualTo(handle)); + Assert.That(snapshot.Tag, Is.EqualTo("ui")); + Assert.That(snapshot.Group, Is.EqualTo("frame-end")); + Assert.That(snapshot.IsWaiting, Is.True); + Assert.That(snapshot.WaitingInstructionType, Is.EqualTo(typeof(WaitForEndOfFrame))); + Assert.That(snapshot.ExecutionStage, Is.EqualTo(CoroutineExecutionStage.EndOfFrame)); + } + + /// + /// 验证异常结束的协程会记录为 Faulted。 + /// + [Test] + public async Task WaitForCompletionAsync_Should_Return_Faulted_For_Failing_Coroutine() + { + var timeSource = new FakeTimeSource(); + var scheduler = new CoroutineScheduler(timeSource); + + IEnumerator Coroutine() + { + yield return new WaitOneFrame(); + throw new InvalidOperationException("boom"); +#pragma warning disable CS0162 + yield break; +#pragma warning restore CS0162 + } + + var handle = scheduler.Run(Coroutine()); + timeSource.Advance(0.1); + scheduler.Update(); + timeSource.Advance(0.1); + scheduler.Update(); + + var status = await scheduler.WaitForCompletionAsync(handle); + + Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Faulted)); + } +} \ No newline at end of file diff --git a/GFramework.Core/Coroutine/CoroutineMetadata.cs b/GFramework.Core/Coroutine/CoroutineMetadata.cs index d9d4947e..15fe0e4e 100644 --- a/GFramework.Core/Coroutine/CoroutineMetadata.cs +++ b/GFramework.Core/Coroutine/CoroutineMetadata.cs @@ -7,6 +7,12 @@ namespace GFramework.Core.Coroutine; /// internal class CoroutineMetadata { + /// + /// 协程所属调度器的执行阶段。 + /// 该值用于诊断等待语义是否与当前宿主阶段匹配。 + /// + public CoroutineExecutionStage ExecutionStage; + /// /// 协程的分组标识符,用于批量管理协程 /// diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs index 4d59d22d..0aeb31f2 100644 --- a/GFramework.Core/Coroutine/CoroutineScheduler.cs +++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Coroutine.Instructions; @@ -6,22 +7,46 @@ using GFramework.Core.Logging; namespace GFramework.Core.Coroutine; /// -/// 协程调度器,用于管理和执行协程 -/// 线程安全说明:此类设计为单线程使用,所有方法应在同一线程中调用 +/// 协程调度器,用于管理和执行协程。 /// -/// 时间源接口,提供时间相关数据 -/// 实例ID,默认为1 -/// 初始容量,默认为256 -/// 是否启用统计功能,默认为false +/// +/// 该调度器按单线程驱动模型设计,业务代码应始终在同一主线程调用其控制 API。 +/// 唯一允许跨线程进入调度器的路径是取消令牌回调;该回调只会把待终止句柄入队, +/// 真正的清理仍然在下一次 中完成。 +/// +/// 缩放时间源,提供调度器默认推进所使用的时间数据。 +/// 协程实例编号,用于生成带宿主前缀的句柄。 +/// 调度器初始槽位容量。 +/// 是否启用协程统计功能。 +/// +/// 非缩放时间源。 +/// 若未提供,则实时等待指令会退化为使用 的时间增量。 +/// +/// +/// 当前调度器所代表的宿主阶段。 +/// 阶段型等待指令仅会在匹配的调度器阶段中完成。 +/// public sealed class CoroutineScheduler( ITimeSource timeSource, byte instanceId = 1, int initialCapacity = 256, - bool enableStatistics = false) + bool enableStatistics = false, + ITimeSource? realtimeTimeSource = null, + CoroutineExecutionStage executionStage = CoroutineExecutionStage.Update) { + private readonly Dictionary> _completionSources = + new(); + + private readonly Dictionary _completionStatuses = new(); + private readonly CoroutineExecutionStage _executionStage = executionStage; private readonly Dictionary> _grouped = new(); private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CoroutineScheduler)); private readonly Dictionary _metadata = new(); + private readonly ConcurrentQueue _pendingKills = new(); + + private readonly ITimeSource _realtimeTimeSource = realtimeTimeSource ?? timeSource ?? + throw new ArgumentNullException(nameof(timeSource)); + private readonly CoroutineStatistics? _statistics = enableStatistics ? new CoroutineStatistics() : null; private readonly Dictionary> _tagged = new(); private readonly ITimeSource _timeSource = timeSource ?? throw new ArgumentNullException(nameof(timeSource)); @@ -32,59 +57,166 @@ public sealed class CoroutineScheduler( private CoroutineSlot?[] _slots = new CoroutineSlot?[initialCapacity]; /// - /// 获取时间差值 + /// 获取默认时间源在当前更新周期内的时间增量。 /// public double DeltaTime => _timeSource.DeltaTime; /// - /// 获取活跃协程数量 + /// 获取实时时间源在当前更新周期内的时间增量。 + /// + public double RealtimeDeltaTime => _realtimeTimeSource.DeltaTime; + + /// + /// 获取当前调度器代表的执行阶段。 + /// + public CoroutineExecutionStage ExecutionStage => _executionStage; + + /// + /// 获取活跃协程数量。 /// public int ActiveCoroutineCount { get; private set; } /// - /// 获取协程统计信息(如果启用) + /// 获取协程统计信息。 + /// 仅当构造时启用了统计功能时才会返回非空对象。 /// public ICoroutineStatistics? Statistics => _statistics; /// - /// 协程异常处理回调,当协程执行过程中发生异常时触发 - /// 注意:事件处理程序会在独立任务中异步调用,以避免阻塞调度器主循环 + /// 当协程执行过程中发生异常时触发。 /// + /// + /// 为了避免阻塞调度器主循环,该事件会被派发到线程池回调中执行。 + /// 如果调用方需要与宿主线程保持一致,请同时订阅 。 + /// public event Action? OnCoroutineException; /// - /// 检查指定的协程句柄是否仍然存活 + /// 当协程以完成、取消或失败任一结果结束时触发。 /// - /// 要检查的协程句柄 - /// 如果协程仍然存活则返回 true,否则返回 false + /// + /// 该事件在调度器所在的驱动线程中同步触发,适合与宿主生命周期管理逻辑集成。 + /// + public event Action? OnCoroutineFinished; + + /// + /// 检查指定协程句柄是否仍然处于活跃状态。 + /// + /// 要检查的协程句柄。 + /// 如果协程仍受该调度器管理,则返回 public bool IsCoroutineAlive(CoroutineHandle handle) { - // 检查元数据字典中是否包含指定的协程句柄 return _metadata.ContainsKey(handle); } + /// + /// 尝试获取指定句柄的当前运行快照。 + /// + /// 要查询的协程句柄。 + /// 查询成功时返回协程快照。 + /// 如果句柄当前仍然活跃,则返回 + public bool TryGetSnapshot(CoroutineHandle handle, out CoroutineSnapshot snapshot) + { + if (!_metadata.TryGetValue(handle, out var meta)) + { + snapshot = default; + return false; + } + + var slot = _slots[meta.SlotIndex]; + if (slot == null) + { + snapshot = default; + return false; + } + + snapshot = CreateSnapshot(meta, slot); + return true; + } + + /// + /// 获取所有活跃协程的运行快照。 + /// + /// 包含所有活跃协程的快照列表。 + public IReadOnlyList GetActiveSnapshots() + { + var snapshots = new List(_metadata.Count); + + foreach (var pair in _metadata) + { + var slot = _slots[pair.Value.SlotIndex]; + if (slot == null) + { + continue; + } + + snapshots.Add(CreateSnapshot(pair.Value, slot)); + } + + return snapshots; + } + + /// + /// 获取指定协程的完成任务。 + /// + /// 要等待完成的协程句柄。 + /// 返回表示协程最终结果的任务。 + /// + /// 如果句柄已经结束,则返回一个已完成任务。 + /// 如果句柄无效或结果未知,则任务结果为 。 + /// + public Task WaitForCompletionAsync(CoroutineHandle handle) + { + if (_completionSources.TryGetValue(handle, out var source)) + { + return source.Task; + } + + return Task.FromResult( + TryGetCompletionStatus(handle, out var status) + ? status + : CoroutineCompletionStatus.Unknown); + } + + /// + /// 尝试读取协程的已知最终结果。 + /// + /// 要查询的协程句柄。 + /// 如果查询成功则返回最终状态。 + /// 当调度器仍保留该句柄的完成历史时返回 + public bool TryGetCompletionStatus(CoroutineHandle handle, out CoroutineCompletionStatus status) + { + return _completionStatuses.TryGetValue(handle, out status); + } + #region Run / Update /// - /// 运行协程 + /// 运行协程。 /// - /// 要运行的协程枚举器 - /// 协程标签,可选 - /// 协程优先级,默认为Normal - /// 协程分组,可选 - /// 协程句柄 + /// 要运行的协程枚举器。 + /// 协程标签,可选。 + /// 协程优先级,默认为 。 + /// 协程分组,可选。 + /// 用于取消协程的外部令牌。 + /// 返回新创建的协程句柄;如果输入为空或取消已请求,则返回无效句柄。 public CoroutineHandle Run( IEnumerator? coroutine, string? tag = null, CoroutinePriority priority = CoroutinePriority.Normal, - string? group = null) + string? group = null, + CancellationToken cancellationToken = default) { - if (coroutine == null) + if (coroutine == null || cancellationToken.IsCancellationRequested) + { return default; + } if (_nextSlot >= _slots.Length) + { Expand(); + } var handle = new CoroutineHandle(instanceId); var slotIndex = _nextSlot++; @@ -97,75 +229,94 @@ public sealed class CoroutineScheduler( Priority = priority }; + if (cancellationToken.CanBeCanceled) + { + // 取消回调可能在任意线程触发,因此这里只做排队,真正清理由 Update 主线程完成。 + slot.CancellationRegistration = cancellationToken.Register(() => _pendingKills.Enqueue(handle)); + } + _slots[slotIndex] = slot; _metadata[handle] = new CoroutineMetadata { - SlotIndex = slotIndex, - State = CoroutineState.Running, - Tag = tag, - Priority = priority, + ExecutionStage = _executionStage, Group = group, - StartTime = _timeSource.CurrentTime * 1000 // 转换为毫秒 + Priority = priority, + SlotIndex = slotIndex, + StartTime = _timeSource.CurrentTime * 1000, + State = CoroutineState.Running, + Tag = tag }; + _completionSources[handle] = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _completionStatuses.Remove(handle); + if (!string.IsNullOrEmpty(tag)) + { AddTag(tag, handle); + } if (!string.IsNullOrEmpty(group)) + { AddGroup(group, handle); + } _statistics?.RecordStart(priority, tag); + ActiveCoroutineCount++; Prewarm(slotIndex); - ActiveCoroutineCount++; + UpdateStatisticsSnapshot(); return handle; } /// - /// 更新所有协程状态 + /// 推进当前调度器中的所有协程。 /// public void Update() { _timeSource.Update(); - var delta = _timeSource.DeltaTime; - - // 更新统计信息 - if (_statistics != null) + if (!ReferenceEquals(_realtimeTimeSource, _timeSource)) { - _statistics.ActiveCount = ActiveCoroutineCount; - _statistics.PausedCount = _pausedCount; + _realtimeTimeSource.Update(); } - // 按优先级排序槽位索引(高优先级优先执行) + ProcessPendingKills(); + UpdateStatisticsSnapshot(); + var sortedIndices = new List(_nextSlot); for (var i = 0; i < _nextSlot; i++) { var slot = _slots[i]; if (slot is { State: CoroutineState.Running }) + { sortedIndices.Add(i); + } } - // 按优先级降序排序 sortedIndices.Sort((a, b) => { var slotA = _slots[a]; var slotB = _slots[b]; if (slotA == null || slotB == null) + { return 0; + } + return slotB.Priority.CompareTo(slotA.Priority); }); - // 遍历所有槽位并更新协程状态 foreach (var i in sortedIndices) { var slot = _slots[i]; if (slot is not { State: CoroutineState.Running }) + { continue; + } try { - ProcessWaitingInstruction(slot, delta); + ProcessWaitingInstruction(slot); if (!IsWaiting(slot)) { @@ -177,43 +328,51 @@ public sealed class CoroutineScheduler( OnError(i, ex); } } + + UpdateStatisticsSnapshot(); } /// - /// 处理协程的等待指令 + /// 处理协程当前等待指令的推进。 /// - /// 协程槽位 - /// 时间差值 - private static void ProcessWaitingInstruction(CoroutineSlot slot, double delta) + /// 当前协程槽位。 + private void ProcessWaitingInstruction(CoroutineSlot slot) { if (slot.Waiting == null) + { return; + } - slot.Waiting.Update(delta); - if (slot.Waiting.IsDone) - slot.Waiting = null; + if (!CanAdvanceInstruction(slot.Waiting)) + { + return; + } + + slot.Waiting.Update(GetInstructionDelta(slot.Waiting)); } /// - /// 判断协程是否正在等待 + /// 判断协程当前是否仍处于阻塞等待状态。 /// - /// 协程槽位 - /// 是否正在等待 + /// 协程槽位。 + /// 如果协程仍需等待则返回 private static bool IsWaiting(CoroutineSlot slot) { return slot.Waiting != null && !slot.Waiting.IsDone; } /// - /// 处理协程步骤推进 + /// 推进协程到下一条等待指令,或在枚举器结束时完成协程。 /// - /// 协程槽位 - /// 槽位索引 + /// 要推进的协程槽位。 + /// 协程槽位索引。 private void ProcessCoroutineStep(CoroutineSlot slot, int slotIndex) { + DisposeWaitingInstruction(slot); + if (!slot.Enumerator.MoveNext()) { - Complete(slotIndex); + FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Completed); return; } @@ -222,23 +381,22 @@ public sealed class CoroutineScheduler( } /// - /// 处理协程的yield指令 + /// 处理协程返回的等待指令。 /// - /// 协程槽位 - /// yield指令 + /// 当前协程槽位。 + /// 当前等待指令。 private void HandleYieldInstruction(CoroutineSlot slot, IYieldInstruction instruction) { switch (instruction) { - // 处理 WaitForCoroutine 指令 case WaitForCoroutine waitForCoroutine: { - // 启动被等待的协程并建立等待关系 var targetHandle = Run(waitForCoroutine.Coroutine); slot.Waiting = waitForCoroutine; WaitForCoroutine(slot.Handle, targetHandle); break; } + default: slot.Waiting = instruction; break; @@ -250,56 +408,69 @@ public sealed class CoroutineScheduler( #region Pause / Resume / Kill /// - /// 暂停指定协程 + /// 暂停指定协程。 /// - /// 协程句柄 - /// 是否成功暂停 + /// 协程句柄。 + /// 如果成功暂停则返回 public bool Pause(CoroutineHandle handle) { if (!_metadata.TryGetValue(handle, out var meta)) + { return false; + } var slot = _slots[meta.SlotIndex]; if (slot == null || slot.State != CoroutineState.Running) + { return false; + } slot.State = CoroutineState.Paused; meta.State = CoroutineState.Paused; _pausedCount++; + UpdateStatisticsSnapshot(); return true; } /// - /// 恢复指定协程 + /// 恢复指定协程。 /// - /// 协程句柄 - /// 是否成功恢复 + /// 协程句柄。 + /// 如果成功恢复则返回 public bool Resume(CoroutineHandle handle) { if (!_metadata.TryGetValue(handle, out var meta)) + { return false; + } var slot = _slots[meta.SlotIndex]; if (slot == null || slot.State != CoroutineState.Paused) + { return false; + } slot.State = CoroutineState.Running; meta.State = CoroutineState.Running; _pausedCount--; + UpdateStatisticsSnapshot(); return true; } /// - /// 终止指定协程 + /// 终止指定协程。 /// - /// 协程句柄 - /// 是否成功终止 + /// 协程句柄。 + /// 如果成功终止则返回 public bool Kill(CoroutineHandle handle) { if (!_metadata.TryGetValue(handle, out var meta)) + { return false; + } - Complete(meta.SlotIndex); + FinalizeCoroutine(meta.SlotIndex, CoroutineCompletionStatus.Cancelled); + UpdateStatisticsSnapshot(); return true; } @@ -308,50 +479,56 @@ public sealed class CoroutineScheduler( #region Group Management /// - /// 暂停指定分组的所有协程 + /// 暂停指定分组的所有协程。 /// - /// 分组名称 - /// 被暂停的协程数量 + /// 分组名称。 + /// 实际被暂停的协程数量。 public int PauseGroup(string group) { if (!_grouped.TryGetValue(group, out var handles)) + { return 0; + } return handles.Count(Pause); } /// - /// 恢复指定分组的所有协程 + /// 恢复指定分组的所有协程。 /// - /// 分组名称 - /// 被恢复的协程数量 + /// 分组名称。 + /// 实际被恢复的协程数量。 public int ResumeGroup(string group) { if (!_grouped.TryGetValue(group, out var handles)) + { return 0; + } return handles.Count(Resume); } /// - /// 终止指定分组的所有协程 + /// 终止指定分组的所有协程。 /// - /// 分组名称 - /// 被终止的协程数量 + /// 分组名称。 + /// 实际被终止的协程数量。 public int KillGroup(string group) { if (!_grouped.TryGetValue(group, out var handles)) + { return 0; + } var copy = handles.ToArray(); return copy.Count(Kill); } /// - /// 获取指定分组的协程数量 + /// 获取指定分组当前包含的活跃协程数量。 /// - /// 分组名称 - /// 协程数量 + /// 分组名称。 + /// 分组中的活跃协程数量。 public int GetGroupCount(string group) { return _grouped.TryGetValue(group, out var handles) ? handles.Count : 0; @@ -362,19 +539,21 @@ public sealed class CoroutineScheduler( #region Wait / Tag / Clear /// - /// 让当前协程等待目标协程完成 + /// 让当前协程等待目标协程完成。 /// - /// 当前协程句柄 - /// 目标协程句柄 - public void WaitForCoroutine( - CoroutineHandle current, - CoroutineHandle target) + /// 当前协程句柄。 + /// 目标协程句柄。 + public void WaitForCoroutine(CoroutineHandle current, CoroutineHandle target) { if (current == target) + { throw new InvalidOperationException("Coroutine cannot wait for itself."); + } if (!_metadata.ContainsKey(target)) + { return; + } if (!_waiting.TryGetValue(target, out var set)) { @@ -386,26 +565,37 @@ public sealed class CoroutineScheduler( } /// - /// 根据标签终止协程 + /// 根据标签终止协程。 /// - /// 协程标签 - /// 被终止的协程数量 + /// 协程标签。 + /// 被终止的协程数量。 public int KillByTag(string tag) { if (!_tagged.TryGetValue(tag, out var handles)) + { return 0; + } var copy = handles.ToArray(); return copy.Count(Kill); } /// - /// 清空所有协程 + /// 清空当前调度器中的所有协程。 /// - /// 被清除的协程数量 + /// 被清理的协程数量。 public int Clear() { var count = ActiveCoroutineCount; + + for (var i = 0; i < _nextSlot; i++) + { + if (_slots[i] != null) + { + FinalizeCoroutine(i, CoroutineCompletionStatus.Cancelled); + } + } + Array.Clear(_slots); _metadata.Clear(); _tagged.Clear(); @@ -415,6 +605,7 @@ public sealed class CoroutineScheduler( _nextSlot = 0; ActiveCoroutineCount = 0; _pausedCount = 0; + UpdateStatisticsSnapshot(); return count; } @@ -424,21 +615,27 @@ public sealed class CoroutineScheduler( #region Internal /// - /// 预热协程槽位,执行协程的第一步 + /// 预热协程槽位,执行协程的第一步。 /// - /// 槽位索引 + /// 槽位索引。 private void Prewarm(int slotIndex) { var slot = _slots[slotIndex]; if (slot == null) + { return; + } try { if (!slot.Enumerator.MoveNext()) - Complete(slotIndex); + { + FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Completed); + } else + { slot.Waiting = slot.Enumerator.Current; + } } catch (Exception ex) { @@ -447,75 +644,87 @@ public sealed class CoroutineScheduler( } /// - /// 完成指定槽位的协程 + /// 按给定结果完成协程并释放相关资源。 /// - /// 槽位索引 - private void Complete(int slotIndex) + /// 目标槽位索引。 + /// 最终结果。 + /// 若协程失败,则传入对应异常。 + private void FinalizeCoroutine( + int slotIndex, + CoroutineCompletionStatus completionStatus, + Exception? exception = null) { var slot = _slots[slotIndex]; if (slot == null) + { return; + } var handle = slot.Handle; if (!handle.IsValid) - return; - - // 记录统计信息 - if (_metadata.TryGetValue(handle, out var meta)) { - var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime; - _statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag); + return; } + if (_metadata.TryGetValue(handle, out var meta)) + { + if (meta.State == CoroutineState.Paused && _pausedCount > 0) + { + _pausedCount--; + } + + var executionTime = _timeSource.CurrentTime * 1000 - meta.StartTime; + switch (completionStatus) + { + case CoroutineCompletionStatus.Completed: + meta.State = CoroutineState.Completed; + _statistics?.RecordComplete(executionTime, meta.Priority, meta.Tag); + break; + + case CoroutineCompletionStatus.Faulted: + meta.State = CoroutineState.Completed; + _statistics?.RecordFailure(meta.Priority, meta.Tag); + break; + + case CoroutineCompletionStatus.Cancelled: + meta.State = CoroutineState.Cancelled; + break; + } + } + + DisposeSlotResources(slot); + _slots[slotIndex] = null; - ActiveCoroutineCount--; + if (ActiveCoroutineCount > 0) + { + ActiveCoroutineCount--; + } RemoveTag(handle); RemoveGroup(handle); _metadata.Remove(handle); - // 唤醒等待者 - if (!_waiting.TryGetValue(handle, out var waiters)) return; - foreach (var waiter in waiters) - { - if (!_metadata.TryGetValue(waiter, out var waiterMeta)) continue; - var s = _slots[waiterMeta.SlotIndex]; - if (s == null) continue; - switch (s.Waiting) - { - // 通知 WaitForCoroutine 指令协程已完成 - case WaitForCoroutine wfc: - wfc.Complete(); - break; - default: - // 其他类型的等待指令不需要特殊处理 - break; - } + WakeWaiters(handle); - s.State = CoroutineState.Running; - waiterMeta.State = CoroutineState.Running; + if (_completionSources.Remove(handle, out var source)) + { + source.TrySetResult(completionStatus); } - _waiting.Remove(handle); + _completionStatuses[handle] = completionStatus; + OnCoroutineFinished?.Invoke(handle, completionStatus, exception); } /// - /// 处理协程执行中的错误 + /// 处理协程执行中的错误。 /// - /// 槽位索引 - /// 异常对象 + /// 槽位索引。 + /// 异常对象。 private void OnError(int slotIndex, Exception ex) { var slot = _slots[slotIndex]; var handle = slot?.Handle ?? default; - // 记录统计信息 - if (handle.IsValid && _metadata.TryGetValue(handle, out var meta)) - { - _statistics?.RecordFailure(meta.Priority, meta.Tag); - } - - // 将异常回调派发到线程池,避免阻塞调度器主循环 var handler = OnCoroutineException; if (handler != null) { @@ -527,21 +736,141 @@ public sealed class CoroutineScheduler( } catch (Exception callbackEx) { - // 防止回调异常传播,记录到控制台 _logger.Error($"[CoroutineScheduler] Exception in error callback: {callbackEx}"); } }); } - // 输出到控制台作为后备 _logger.Error($"[CoroutineScheduler] Coroutine {handle} failed with exception: {ex}"); - - // 完成协程 - Complete(slotIndex); + FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Faulted, ex); } /// - /// 扩展协程槽位数组容量 + /// 判断指定等待指令是否允许在当前调度器阶段中推进。 + /// + /// 要检查的等待指令。 + /// 如果当前阶段允许推进该等待指令,则返回 + private bool CanAdvanceInstruction(IYieldInstruction instruction) + { + return instruction switch + { + WaitForFixedUpdate => _executionStage == CoroutineExecutionStage.FixedUpdate, + WaitForEndOfFrame => _executionStage == CoroutineExecutionStage.EndOfFrame, + _ => true + }; + } + + /// + /// 为指定等待指令选择合适的时间增量。 + /// + /// 待推进的等待指令。 + /// 与等待语义匹配的时间增量。 + private double GetInstructionDelta(IYieldInstruction instruction) + { + return instruction switch + { + WaitForSecondsRealtime => RealtimeDeltaTime, + _ => DeltaTime + }; + } + + /// + /// 处理跨线程入队的待终止协程。 + /// + private void ProcessPendingKills() + { + while (_pendingKills.TryDequeue(out var handle)) + { + Kill(handle); + } + } + + /// + /// 释放单个槽位持有的资源。 + /// + /// 待释放的槽位。 + private static void DisposeSlotResources(CoroutineSlot slot) + { + DisposeWaitingInstruction(slot); + slot.CancellationRegistration.Dispose(); + slot.Enumerator.Dispose(); + } + + /// + /// 如果当前等待指令实现了可释放接口,则在协程继续前先释放该指令。 + /// + /// 当前协程槽位。 + private static void DisposeWaitingInstruction(CoroutineSlot slot) + { + if (slot.Waiting is IDisposable disposable) + { + disposable.Dispose(); + } + + slot.Waiting = null; + } + + /// + /// 唤醒所有等待目标协程完成的协程。 + /// + /// 已结束的目标协程句柄。 + private void WakeWaiters(CoroutineHandle handle) + { + if (!_waiting.TryGetValue(handle, out var waiters)) + { + return; + } + + foreach (var waiter in waiters) + { + if (!_metadata.TryGetValue(waiter, out var waiterMeta)) + { + continue; + } + + var waiterSlot = _slots[waiterMeta.SlotIndex]; + if (waiterSlot == null) + { + continue; + } + + if (waiterSlot.Waiting is WaitForCoroutine waitForCoroutine) + { + waitForCoroutine.Complete(); + } + + if (waiterSlot.State != CoroutineState.Paused) + { + waiterSlot.State = CoroutineState.Running; + waiterMeta.State = CoroutineState.Running; + } + } + + _waiting.Remove(handle); + } + + /// + /// 创建协程快照。 + /// + /// 协程元数据。 + /// 协程槽位。 + /// 与当前槽位一致的只读快照。 + private CoroutineSnapshot CreateSnapshot(CoroutineMetadata metadata, CoroutineSlot slot) + { + return new CoroutineSnapshot( + slot.Handle, + metadata.State, + metadata.Priority, + metadata.Tag, + metadata.Group, + metadata.StartTime, + IsWaiting(slot), + slot.Waiting?.GetType(), + metadata.ExecutionStage); + } + + /// + /// 扩展协程槽位数组容量。 /// private void Expand() { @@ -549,10 +878,24 @@ public sealed class CoroutineScheduler( } /// - /// 为协程添加标签 + /// 更新统计对象中的活动快照数据。 /// - /// 标签名称 - /// 协程句柄 + private void UpdateStatisticsSnapshot() + { + if (_statistics == null) + { + return; + } + + _statistics.ActiveCount = ActiveCoroutineCount; + _statistics.PausedCount = _pausedCount; + } + + /// + /// 为协程添加标签。 + /// + /// 标签名称。 + /// 协程句柄。 private void AddTag(string tag, CoroutineHandle handle) { if (!_tagged.TryGetValue(tag, out var set)) @@ -566,29 +909,33 @@ public sealed class CoroutineScheduler( } /// - /// 移除协程标签 + /// 移除协程标签。 /// - /// 协程句柄 + /// 协程句柄。 private void RemoveTag(CoroutineHandle handle) { if (!_metadata.TryGetValue(handle, out var meta) || meta.Tag == null) + { return; + } if (_tagged.TryGetValue(meta.Tag, out var set)) { set.Remove(handle); if (set.Count == 0) + { _tagged.Remove(meta.Tag); + } } meta.Tag = null; } /// - /// 为协程添加分组 + /// 为协程添加分组。 /// - /// 分组名称 - /// 协程句柄 + /// 分组名称。 + /// 协程句柄。 private void AddGroup(string group, CoroutineHandle handle) { if (!_grouped.TryGetValue(group, out var set)) @@ -602,19 +949,23 @@ public sealed class CoroutineScheduler( } /// - /// 移除协程分组 + /// 移除协程分组。 /// - /// 协程句柄 + /// 协程句柄。 private void RemoveGroup(CoroutineHandle handle) { if (!_metadata.TryGetValue(handle, out var meta) || meta.Group == null) + { return; + } if (_grouped.TryGetValue(meta.Group, out var set)) { set.Remove(handle); if (set.Count == 0) + { _grouped.Remove(meta.Group); + } } meta.Group = null; diff --git a/GFramework.Core/Coroutine/CoroutineSlot.cs b/GFramework.Core/Coroutine/CoroutineSlot.cs index 7821e486..e4478037 100644 --- a/GFramework.Core/Coroutine/CoroutineSlot.cs +++ b/GFramework.Core/Coroutine/CoroutineSlot.cs @@ -7,6 +7,12 @@ namespace GFramework.Core.Coroutine; /// internal sealed class CoroutineSlot { + /// + /// 由外部取消令牌创建的注册。 + /// 调度器在协程结束时必须释放该注册,避免泄漏取消回调。 + /// + public CancellationTokenRegistration CancellationRegistration; + /// /// 协程枚举器,包含协程的执行逻辑 /// diff --git a/GFramework.Core/Coroutine/CoroutineSnapshot.cs b/GFramework.Core/Coroutine/CoroutineSnapshot.cs new file mode 100644 index 00000000..c96b77bd --- /dev/null +++ b/GFramework.Core/Coroutine/CoroutineSnapshot.cs @@ -0,0 +1,29 @@ +using GFramework.Core.Abstractions.Coroutine; + +namespace GFramework.Core.Coroutine; + +/// +/// 表示某个活跃协程在调度器中的只读运行快照。 +/// +/// 协程句柄。 +/// 当前协程状态。 +/// 当前协程优先级。 +/// 可选标签。 +/// 可选分组。 +/// 协程启动时间,单位为毫秒。 +/// 当前是否正被等待指令阻塞。 +/// +/// 当前等待指令的具体类型。 +/// 若协程当前未处于等待状态,则该值为 。 +/// +/// 所属调度器的执行阶段。 +public readonly record struct CoroutineSnapshot( + CoroutineHandle Handle, + CoroutineState State, + CoroutinePriority Priority, + string? Tag, + string? Group, + double StartTimeMs, + bool IsWaiting, + Type? WaitingInstructionType, + CoroutineExecutionStage ExecutionStage); \ No newline at end of file diff --git a/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs new file mode 100644 index 00000000..f1527dac --- /dev/null +++ b/GFramework.Godot.Tests/Coroutine/GodotTimeSourceTests.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using GFramework.Godot.Coroutine; +using NUnit.Framework; + +namespace GFramework.Godot.Tests.Coroutine; + +/// +/// GodotTimeSource 的单元测试。 +/// +[TestFixture] +public sealed class GodotTimeSourceTests +{ + /// + /// 验证增量模式会直接累加传入的 delta。 + /// + [Test] + public void Update_Should_Accumulate_Delta_When_Using_Delta_Mode() + { + var values = new Queue([0.1, 0.2]); + var timeSource = new GodotTimeSource(() => values.Dequeue()); + + timeSource.Update(); + Assert.That(timeSource.DeltaTime, Is.EqualTo(0.1).Within(0.0001)); + Assert.That(timeSource.CurrentTime, Is.EqualTo(0.1).Within(0.0001)); + + timeSource.Update(); + Assert.That(timeSource.DeltaTime, Is.EqualTo(0.2).Within(0.0001)); + Assert.That(timeSource.CurrentTime, Is.EqualTo(0.3).Within(0.0001)); + } + + /// + /// 验证绝对时间模式会根据前后两次采样计算 delta。 + /// + [Test] + public void Update_Should_Calculate_Delta_When_Using_Absolute_Time_Mode() + { + var values = new Queue([1.0, 1.25, 2.0]); + var timeSource = new GodotTimeSource(() => values.Dequeue(), useAbsoluteTime: true); + + timeSource.Update(); + Assert.That(timeSource.DeltaTime, Is.EqualTo(0).Within(0.0001)); + Assert.That(timeSource.CurrentTime, Is.EqualTo(1.0).Within(0.0001)); + + timeSource.Update(); + Assert.That(timeSource.DeltaTime, Is.EqualTo(0.25).Within(0.0001)); + Assert.That(timeSource.CurrentTime, Is.EqualTo(1.25).Within(0.0001)); + + timeSource.Update(); + Assert.That(timeSource.DeltaTime, Is.EqualTo(0.75).Within(0.0001)); + Assert.That(timeSource.CurrentTime, Is.EqualTo(2.0).Within(0.0001)); + } +} \ No newline at end of file diff --git a/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj new file mode 100644 index 00000000..62e41903 --- /dev/null +++ b/GFramework.Godot.Tests/GFramework.Godot.Tests.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + $(TestTargetFrameworks) + disable + enable + false + true + 0 + + + + + + + + + + + + + diff --git a/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs b/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs index d233ad17..48ca3913 100644 --- a/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs +++ b/GFramework.Godot/Coroutine/CoroutineNodeExtensions.cs @@ -35,6 +35,25 @@ public static class CoroutineNodeExtensions return Timing.RunCoroutine(coroutine, segment, tag); } + /// + /// 以指定节点作为生命周期所有者运行协程。 + /// + /// 拥有该协程生命周期的节点。 + /// 要启动的协程枚举器。 + /// 协程运行的时间段。 + /// 协程标签。 + /// 可选取消令牌。 + /// 返回协程句柄。 + public static CoroutineHandle RunCoroutine( + this Node owner, + IEnumerator coroutine, + Segment segment = Segment.Process, + string? tag = null, + CancellationToken cancellationToken = default) + { + return Timing.RunOwnedCoroutine(owner, coroutine, segment, tag, cancellationToken); + } + /// /// 让协程在指定节点被销毁时自动取消。 /// diff --git a/GFramework.Godot/Coroutine/GodotTimeSource.cs b/GFramework.Godot/Coroutine/GodotTimeSource.cs index b33bc3d7..ffdd3ae4 100644 --- a/GFramework.Godot/Coroutine/GodotTimeSource.cs +++ b/GFramework.Godot/Coroutine/GodotTimeSource.cs @@ -1,42 +1,81 @@ using GFramework.Core.Abstractions.Coroutine; +using Godot; namespace GFramework.Godot.Coroutine; /// -/// Godot时间源实现,用于提供基于Godot引擎的时间信息 +/// Godot 时间源实现,用于为协程调度器提供缩放时间或真实时间数据。 /// -/// 获取增量时间的函数委托 -public class GodotTimeSource(Func getDeltaFunc) : ITimeSource +/// +/// 时间提供函数。 +/// 在默认模式下该函数返回“本帧增量”;在绝对时间模式下该函数返回“当前绝对时间(秒)”。 +/// +/// +/// 是否把 返回值解释为绝对时间。 +/// 启用后, 会通过相邻两次读数计算 。 +/// +public sealed class GodotTimeSource(Func timeProvider, bool useAbsoluteTime = false) : ITimeSource { - private readonly Func _getDeltaFunc = getDeltaFunc ?? throw new ArgumentNullException(nameof(getDeltaFunc)); + private readonly Func _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); + private bool _initialized; + private double _lastAbsoluteTime; /// - /// 获取当前累计时间 + /// 获取当前累计时间。 /// public double CurrentTime { get; private set; } /// - /// 获取上一帧的时间增量 + /// 获取上一帧的时间增量。 /// public double DeltaTime { get; private set; } /// - /// 更新时间源,计算新的增量时间和累计时间 + /// 更新时间源,计算新的时间增量与累计时间。 /// public void Update() { - // 调用外部提供的函数获取当前帧的时间增量 - DeltaTime = _getDeltaFunc(); - // 累加到总时间中 + var value = _timeProvider(); + if (useAbsoluteTime) + { + if (!_initialized) + { + _initialized = true; + _lastAbsoluteTime = value; + CurrentTime = value; + DeltaTime = 0; + return; + } + + DeltaTime = Math.Max(0, value - _lastAbsoluteTime); + _lastAbsoluteTime = value; + CurrentTime = value; + return; + } + + DeltaTime = value; CurrentTime += DeltaTime; } /// - /// 重置时间源到初始状态 + /// 创建基于 Godot 单调时钟的真实时间源。 + /// + /// 返回一个不受场景暂停与时间缩放影响的时间源实例。 + public static GodotTimeSource CreateRealtime() + { + return new GodotTimeSource( + () => Time.GetTicksUsec() / 1_000_000.0, + useAbsoluteTime: true); + } + + /// + /// 重置时间源到初始状态。 /// public void Reset() { CurrentTime = 0; DeltaTime = 0; + _initialized = false; + _lastAbsoluteTime = 0; } } \ No newline at end of file diff --git a/GFramework.Godot/Coroutine/Timing.cs b/GFramework.Godot/Coroutine/Timing.cs index 881c36be..983fdc6f 100644 --- a/GFramework.Godot/Coroutine/Timing.cs +++ b/GFramework.Godot/Coroutine/Timing.cs @@ -8,68 +8,74 @@ using Godot; namespace GFramework.Godot.Coroutine; /// -/// Godot协程管理器,提供基于不同更新循环的协程调度功能 -/// 支持Process、PhysicsProcess和DeferredProcess三种执行段的协程管理 +/// Godot 协程管理器,负责在不同的引擎更新阶段驱动 Core 协程调度器。 /// +/// +/// 该类型为 Godot 层协程功能的统一入口。 +/// 它不仅提供静态运行 API,也负责把 Godot 的 Process、Physics 与 Deferred 生命周期映射为 +/// ,以保证阶段型等待指令的语义真实有效。 +/// public partial class Timing : Node { - private static Timing? _instance; + private const string NotInitializedMessage = "Timing not yet initialized (_Ready not executed)"; + private static readonly Timing?[] ActiveInstances = new Timing?[16]; + private static Timing? _instance; + private readonly Dictionary _ownedCoroutineRegistrations = new(); + private readonly Dictionary> _ownedCoroutinesByNode = new(); + private GodotTimeSource? _deferredRealtimeTimeSource; private CoroutineScheduler? _deferredScheduler; private GodotTimeSource? _deferredTimeSource; private ushort _frameCounter; - private byte _instanceId = 1; + private GodotTimeSource? _physicsRealtimeTimeSource; private CoroutineScheduler? _physicsScheduler; private GodotTimeSource? _physicsTimeSource; - - private CoroutineScheduler? _processScheduler; - - private GodotTimeSource? _processTimeSource; + private GodotTimeSource? _processIgnorePauseRealtimeTimeSource; private CoroutineScheduler? _processIgnorePauseScheduler; private GodotTimeSource? _processIgnorePauseTimeSource; - private const string NotInitializedMessage = "Timing not yet initialized (_Ready not executed)"; + private GodotTimeSource? _processRealtimeTimeSource; + private CoroutineScheduler? _processScheduler; + private GodotTimeSource? _processTimeSource; /// - /// 获取Process调度器,如果未初始化则抛出异常 + /// 获取 Process 调度器。 /// private CoroutineScheduler ProcessScheduler => - _processScheduler ?? throw new InvalidOperationException( - NotInitializedMessage); + _processScheduler ?? throw new InvalidOperationException(NotInitializedMessage); /// - /// 获取忽略暂停的Process调度器,如果未初始化则抛出异常 + /// 获取忽略暂停的 Process 调度器。 /// private CoroutineScheduler ProcessIgnorePauseScheduler => - _processIgnorePauseScheduler ?? throw new InvalidOperationException( - NotInitializedMessage); + _processIgnorePauseScheduler ?? throw new InvalidOperationException(NotInitializedMessage); /// - /// 获取Physics调度器,如果未初始化则抛出异常 + /// 获取 Physics 调度器。 /// private CoroutineScheduler PhysicsScheduler => - _physicsScheduler ?? throw new InvalidOperationException( - NotInitializedMessage); + _physicsScheduler ?? throw new InvalidOperationException(NotInitializedMessage); /// - /// 获取Deferred调度器,如果未初始化则抛出异常 + /// 获取 Deferred 调度器。 /// private CoroutineScheduler DeferredScheduler => - _deferredScheduler ?? throw new InvalidOperationException( - NotInitializedMessage); + _deferredScheduler ?? throw new InvalidOperationException(NotInitializedMessage); #region 单例 /// - /// 获取Timing单例实例 - /// 如果实例不存在则自动创建并添加到场景树根节点 + /// 获取 Timing 单例实例。 + /// 如果实例不存在,则会自动创建并挂载到场景树根节点。 /// public static Timing Instance { get { if (_instance != null) + { return _instance; + } var tree = (SceneTree)Engine.GetMainLoop(); _instance = tree.Root.GetNodeOrNull(nameof(Timing)); @@ -83,27 +89,70 @@ public partial class Timing : Node Name = nameof(Timing) }; tree.Root.WaitUntilReady(() => tree.Root.AddChild(_instance)); - return _instance; } } #endregion + private sealed class OwnedCoroutineRegistration + { + /// + /// 创建一个节点归属协程注册记录。 + /// + /// 归属节点。 + /// 归属节点的 Godot 实例 ID。 + /// 被管理的协程句柄。 + /// 节点退出场景树时触发的终止逻辑。 + public OwnedCoroutineRegistration(Node owner, ulong ownerId, CoroutineHandle handle, + Action killCallback) + { + Handle = handle; + Owner = new WeakReference(owner); + OwnerId = ownerId; + OnOwnerTreeExiting = () => killCallback(handle); + } + + /// + /// 获取协程句柄。 + /// + public CoroutineHandle Handle { get; } + + /// + /// 获取节点弱引用。 + /// + public WeakReference Owner { get; } + + /// + /// 获取归属节点 ID。 + /// + public ulong OwnerId { get; } + + /// + /// 获取节点退出场景树时使用的清理回调。 + /// + public Action OnOwnerTreeExiting { get; } + } + #region Debug 信息 /// - /// 获取Process段活跃协程数量 + /// 获取 Process 段活跃协程数量。 /// public int ProcessCoroutines => _processScheduler?.ActiveCoroutineCount ?? 0; /// - /// 获取Physics段活跃协程数量 + /// 获取忽略暂停的 Process 段活跃协程数量。 + /// + public int ProcessIgnorePauseCoroutines => _processIgnorePauseScheduler?.ActiveCoroutineCount ?? 0; + + /// + /// 获取 Physics 段活跃协程数量。 /// public int PhysicsCoroutines => _physicsScheduler?.ActiveCoroutineCount ?? 0; /// - /// 获取Deferred段活跃协程数量 + /// 获取 Deferred 段活跃协程数量。 /// public int DeferredCoroutines => _deferredScheduler?.ActiveCoroutineCount ?? 0; @@ -112,34 +161,36 @@ public partial class Timing : Node #region 生命周期 /// - /// 节点就绪时的初始化方法 - /// 设置处理优先级,初始化调度器,并注册实例 + /// 节点就绪时初始化所有调度器与生命周期桥接。 /// public override void _Ready() { ProcessPriority = -1; ProcessMode = ProcessModeEnum.Always; - TrySetPhysicsPriority(-1); - - InitializeSchedulers(); RegisterInstance(); + TrySetPhysicsPriority(-1); + InitializeSchedulers(); } /// - /// 节点退出场景树时的清理方法 - /// 从活动实例数组中移除当前实例并清理必要资源 + /// 节点退出场景树时清理实例与归属关系。 /// public override void _ExitTree() { + DetachAllOwnedRegistrations(); + ClearOnInstance(); + if (_instanceId < ActiveInstances.Length) + { ActiveInstances[_instanceId] = null; + } CleanupInstanceIfNecessary(); } /// - /// 清理实例引用 + /// 清理单例引用。 /// private static void CleanupInstanceIfNecessary() { @@ -147,41 +198,41 @@ public partial class Timing : Node } /// - /// 每帧处理逻辑 - /// 更新Process调度器,增加帧计数器,并安排延迟处理 + /// Godot 每帧更新逻辑。 /// - /// 时间增量 + /// 本帧 Process 增量。 public override void _Process(double delta) { var paused = GetTree().Paused; if (!paused) + { _processScheduler?.Update(); + } _processIgnorePauseScheduler?.Update(); _frameCounter++; - CallDeferred(nameof(ProcessDeferred)); } /// - /// 物理处理逻辑 - /// 更新Physics调度器 + /// Godot 物理帧更新逻辑。 /// - /// 物理时间增量 + /// 本帧 Physics 增量。 public override void _PhysicsProcess(double delta) { _physicsScheduler?.Update(); } /// - /// 延迟处理逻辑 - /// 更新Deferred调度器 + /// 当前帧尾的延迟更新逻辑。 /// private void ProcessDeferred() { if (GetTree().Paused) + { return; + } _deferredScheduler?.Update(); } @@ -189,54 +240,78 @@ public partial class Timing : Node #endregion #region 初始化 + /// - /// 预热函数,用于确保实例已初始化。 - /// 此函数通过访问 Instance 属性来触发可能的延迟初始化逻辑, - /// 从而避免在首次使用时产生性能开销。 + /// 预热 Timing 单例,以便在业务逻辑首次使用前完成挂载。 /// public static void Prewarm() { - // 访问 Instance 属性以触发初始化逻辑 _ = Instance; } + /// - /// 初始化所有调度器和时间源 - /// 创建Process、Physics和Deferred三个调度器实例 + /// 初始化所有调度器和时间源。 /// private void InitializeSchedulers() { _processTimeSource = new GodotTimeSource(GetProcessDeltaTime); + _processRealtimeTimeSource = GodotTimeSource.CreateRealtime(); _processIgnorePauseTimeSource = new GodotTimeSource(GetProcessDeltaTime); + _processIgnorePauseRealtimeTimeSource = GodotTimeSource.CreateRealtime(); _physicsTimeSource = new GodotTimeSource(GetPhysicsProcessDeltaTime); + _physicsRealtimeTimeSource = GodotTimeSource.CreateRealtime(); _deferredTimeSource = new GodotTimeSource(GetProcessDeltaTime); + _deferredRealtimeTimeSource = GodotTimeSource.CreateRealtime(); _processScheduler = new CoroutineScheduler( _processTimeSource, - _instanceId - ); + _instanceId, + 256, + false, + _processRealtimeTimeSource, + CoroutineExecutionStage.Update); _processIgnorePauseScheduler = new CoroutineScheduler( _processIgnorePauseTimeSource, - _instanceId - ); + _instanceId, + 256, + false, + _processIgnorePauseRealtimeTimeSource, + CoroutineExecutionStage.Update); _physicsScheduler = new CoroutineScheduler( _physicsTimeSource, _instanceId, - 128 - ); + 128, + false, + _physicsRealtimeTimeSource, + CoroutineExecutionStage.FixedUpdate); _deferredScheduler = new CoroutineScheduler( _deferredTimeSource, _instanceId, - 64 - ); + 64, + false, + _deferredRealtimeTimeSource, + CoroutineExecutionStage.EndOfFrame); + AttachSchedulerLifecycleHandlers(ProcessScheduler); + AttachSchedulerLifecycleHandlers(ProcessIgnorePauseScheduler); + AttachSchedulerLifecycleHandlers(PhysicsScheduler); + AttachSchedulerLifecycleHandlers(DeferredScheduler); } /// - /// 注册当前实例到活动实例数组中 - /// 如果当前ID已被占用则寻找可用ID + /// 把调度器的完成通知接入 Timing 的节点归属清理逻辑。 + /// + /// 待桥接的调度器。 + private void AttachSchedulerLifecycleHandlers(CoroutineScheduler scheduler) + { + scheduler.OnCoroutineFinished += HandleCoroutineFinished; + } + + /// + /// 注册当前 Timing 实例到实例槽位表中。 /// private void RegisterInstance() { @@ -247,31 +322,31 @@ public partial class Timing : Node } for (byte i = 1; i < ActiveInstances.Length; i++) + { if (ActiveInstances[i] == null) { _instanceId = i; ActiveInstances[i] = this; return; } + } throw new OverflowException("最多只能存在 15 个 Timing 实例"); } /// - /// 尝试设置物理处理优先级 - /// 使用反射方式设置ProcessPhysicsPriority属性 + /// 通过反射设置 Physics 处理优先级,兼容不同 Godot 版本的 API 表面。 /// - /// 物理处理优先级 - private static void TrySetPhysicsPriority(int priority) + /// 要设置的优先级。 + private void TrySetPhysicsPriority(int priority) { try { typeof(Node) .GetProperty( "ProcessPhysicsPriority", - BindingFlags.Instance | - BindingFlags.Public) - ?.SetValue(Instance, priority); + BindingFlags.Instance | BindingFlags.Public) + ?.SetValue(this, priority); } catch { @@ -284,70 +359,119 @@ public partial class Timing : Node #region 协程启动 API /// - /// 运行游戏级协程(受暂停影响) + /// 运行受场景暂停影响的游戏级协程。 /// - /// 要运行的协程枚举器 - /// 协程标签,用于批量操作 - /// 协程句柄 - public static CoroutineHandle RunGameCoroutine( - IEnumerator coroutine, - string? tag = null) + /// 要运行的协程枚举器。 + /// 协程标签。 + /// 新创建的协程句柄。 + public static CoroutineHandle RunGameCoroutine(IEnumerator coroutine, string? tag = null) { return RunCoroutine(coroutine, Segment.Process, tag); } /// - /// 运行UI级协程(忽略暂停) + /// 运行忽略场景暂停的 UI 级协程。 /// - /// 要运行的协程枚举器 - /// 协程标签,用于批量操作 - /// 协程句柄 - public static CoroutineHandle RunUiCoroutine( - IEnumerator coroutine, - string? tag = null) + /// 要运行的协程枚举器。 + /// 协程标签。 + /// 新创建的协程句柄。 + public static CoroutineHandle RunUiCoroutine(IEnumerator coroutine, string? tag = null) { return RunCoroutine(coroutine, Segment.ProcessIgnorePause, tag); } /// - /// 在指定段运行协程 + /// 在指定段运行协程。 /// - /// 要运行的协程枚举器 - /// 协程执行段(Process/PhysicsProcess/DeferredProcess) - /// 协程标签,用于批量操作 - /// 协程句柄 + /// 要运行的协程枚举器。 + /// 协程执行段。 + /// 协程标签。 + /// 可选取消令牌。 + /// 新创建的协程句柄。 public static CoroutineHandle RunCoroutine( IEnumerator coroutine, Segment segment = Segment.Process, - string? tag = null) + string? tag = null, + CancellationToken cancellationToken = default) { - return Instance.RunCoroutineOnInstance(coroutine, segment, tag); + return Instance.RunCoroutineOnInstance(coroutine, segment, tag, cancellationToken); } /// - /// 在当前实例上运行协程 - /// 根据指定的段选择对应的调度器运行协程 + /// 运行一个显式归属于指定节点的协程。 /// - /// 要运行的协程枚举器 - /// 协程执行段 - /// 协程标签 - /// 协程句柄 + /// 拥有该协程生命周期的节点。 + /// 要运行的协程枚举器。 + /// 协程执行段。 + /// 协程标签。 + /// 可选取消令牌。 + /// 新创建的协程句柄。 + public static CoroutineHandle RunOwnedCoroutine( + Node owner, + IEnumerator coroutine, + Segment segment = Segment.Process, + string? tag = null, + CancellationToken cancellationToken = default) + { + return Instance.RunOwnedCoroutineOnInstance(owner, coroutine, segment, tag, cancellationToken); + } + + /// + /// 在当前实例上运行协程。 + /// + /// 要运行的协程枚举器。 + /// 协程执行段。 + /// 协程标签。 + /// 可选取消令牌。 + /// 新创建的协程句柄。 public CoroutineHandle RunCoroutineOnInstance( IEnumerator? coroutine, Segment segment = Segment.Process, - string? tag = null) + string? tag = null, + CancellationToken cancellationToken = default) { if (coroutine == null) - return default; - - return segment switch { - Segment.Process => ProcessScheduler.Run(coroutine, tag), - Segment.ProcessIgnorePause => ProcessIgnorePauseScheduler.Run(coroutine, tag), - Segment.PhysicsProcess => PhysicsScheduler.Run(coroutine, tag), - Segment.DeferredProcess => DeferredScheduler.Run(coroutine, tag), - _ => default - }; + return default; + } + + return GetScheduler(segment).Run(coroutine, tag, group: null, cancellationToken: cancellationToken); + } + + /// + /// 在当前实例上运行归属于指定节点的协程。 + /// + /// 拥有该协程的节点。 + /// 要运行的协程枚举器。 + /// 协程执行段。 + /// 协程标签。 + /// 可选取消令牌。 + /// 新创建的协程句柄。 + public CoroutineHandle RunOwnedCoroutineOnInstance( + Node? owner, + IEnumerator? coroutine, + Segment segment = Segment.Process, + string? tag = null, + CancellationToken cancellationToken = default) + { + if (owner == null || coroutine == null || !IsNodeAlive(owner)) + { + return default; + } + + var handle = RunCoroutineOnInstance( + coroutine.CancelWith(owner), + segment, + tag, + cancellationToken); + + if (!handle.IsValid) + { + return handle; + } + + RegisterOwnedCoroutine(owner, handle); + return handle; } #endregion @@ -355,60 +479,97 @@ public partial class Timing : Node #region 协程控制 API /// - /// 暂停指定的协程 + /// 暂停指定的协程。 /// - /// 协程句柄 - /// 是否成功暂停 + /// 协程句柄。 + /// 如果成功暂停则返回 public static bool PauseCoroutine(CoroutineHandle handle) { return GetInstance(handle.Key)?.PauseOnInstance(handle) == true; } /// - /// 恢复指定的协程 + /// 恢复指定的协程。 /// - /// 协程句柄 - /// 是否成功恢复 + /// 协程句柄。 + /// 如果成功恢复则返回 public static bool ResumeCoroutine(CoroutineHandle handle) { return GetInstance(handle.Key)?.ResumeOnInstance(handle) == true; } /// - /// 终止指定的协程 + /// 终止指定的协程。 /// - /// 协程句柄 - /// 是否成功终止 + /// 协程句柄。 + /// 如果成功终止则返回 public static bool KillCoroutine(CoroutineHandle handle) { return GetInstance(handle.Key)?.KillOnInstance(handle) == true; } /// - /// 终止所有具有指定标签的协程 + /// 终止某个节点归属的所有协程。 /// - /// 协程标签 - /// 被终止的协程数量 + /// 协程归属节点。 + /// 被终止的协程数量。 + public static int KillCoroutines(Node owner) + { + return Instance.KillOwnedCoroutinesOnInstance(owner); + } + + /// + /// 终止所有具有指定标签的协程。 + /// + /// 协程标签。 + /// 被终止的协程数量。 public static int KillCoroutines(string tag) { return Instance.KillByTagOnInstance(tag); } /// - /// 终止所有协程 + /// 终止所有协程。 /// - /// 被终止的协程总数 + /// 被终止的协程总数。 public static int KillAllCoroutines() { return Instance.ClearOnInstance(); } /// - /// 在当前实例上暂停协程 - /// 尝试在所有调度器中查找并暂停指定协程 + /// 根据协程句柄查询其当前快照。 /// - /// 协程句柄 - /// 是否成功暂停 + /// 要查询的协程句柄。 + /// 查询成功时返回快照。 + /// 如果找到活跃协程则返回 + public static bool TryGetCoroutineSnapshot(CoroutineHandle handle, out CoroutineSnapshot snapshot) + { + var instance = GetInstance(handle.Key); + if (instance == null) + { + snapshot = default; + return false; + } + + return instance.TryGetSnapshotOnInstance(handle, out snapshot); + } + + /// + /// 获取某个节点当前归属的活跃协程数量。 + /// + /// 要查询的节点。 + /// 该节点当前归属的活跃协程数量。 + public static int GetOwnedCoroutineCount(Node owner) + { + return Instance.GetOwnedCoroutineCountOnInstance(owner); + } + + /// + /// 在当前实例上暂停协程。 + /// + /// 协程句柄。 + /// 如果成功暂停则返回 private bool PauseOnInstance(CoroutineHandle handle) { return ProcessScheduler.Pause(handle) @@ -418,11 +579,10 @@ public partial class Timing : Node } /// - /// 在当前实例上恢复协程 - /// 尝试在所有调度器中查找并恢复指定协程 + /// 在当前实例上恢复协程。 /// - /// 协程句柄 - /// 是否成功恢复 + /// 协程句柄。 + /// 如果成功恢复则返回 private bool ResumeOnInstance(CoroutineHandle handle) { return ProcessScheduler.Resume(handle) @@ -432,11 +592,10 @@ public partial class Timing : Node } /// - /// 在当前实例上终止协程 - /// 尝试在所有调度器中查找并终止指定协程 + /// 在当前实例上终止协程。 /// - /// 协程句柄 - /// 是否成功终止 + /// 协程句柄。 + /// 如果成功终止则返回 private bool KillOnInstance(CoroutineHandle handle) { return ProcessScheduler.Kill(handle) @@ -446,11 +605,10 @@ public partial class Timing : Node } /// - /// 在当前实例上根据标签终止协程 - /// 在所有调度器中查找并终止具有指定标签的协程 + /// 在当前实例上根据标签终止协程。 /// - /// 协程标签 - /// 被终止的协程数量 + /// 协程标签。 + /// 被终止的协程数量。 private int KillByTagOnInstance(string tag) { var count = 0; @@ -462,10 +620,45 @@ public partial class Timing : Node } /// - /// 清空当前实例上的所有协程 - /// 从所有调度器中清除协程 + /// 在当前实例上终止某个节点归属的所有协程。 /// - /// 被清除的协程总数 + /// 协程归属节点。 + /// 被终止的协程数量。 + private int KillOwnedCoroutinesOnInstance(Node owner) + { + var ownerId = owner.GetInstanceId(); + if (!_ownedCoroutinesByNode.TryGetValue(ownerId, out var handles)) + { + return 0; + } + + var count = 0; + foreach (var handle in handles.ToArray()) + { + if (KillOnInstance(handle)) + { + count++; + } + } + + return count; + } + + /// + /// 获取某个节点当前归属的活跃协程数量。 + /// + /// 要查询的节点。 + /// 活跃归属协程数量。 + private int GetOwnedCoroutineCountOnInstance(Node owner) + { + var ownerId = owner.GetInstanceId(); + return _ownedCoroutinesByNode.TryGetValue(ownerId, out var handles) ? handles.Count : 0; + } + + /// + /// 清空当前实例上的所有协程。 + /// + /// 被清除的协程总数。 private int ClearOnInstance() { var count = 0; @@ -481,60 +674,163 @@ public partial class Timing : Node #region 工具方法 /// - /// 根据ID获取Timing实例 + /// 根据 ID 获取 Timing 实例。 /// - /// 实例ID - /// 对应的Timing实例或null + /// 实例 ID。 + /// 对应的 Timing 实例;如果不存在则返回 public static Timing? GetInstance(byte id) { return id < ActiveInstances.Length ? ActiveInstances[id] : null; } - /// - /// 检查节点是否处于有效状态 + /// 检查节点是否处于有效状态。 /// - /// 要检查的节点 - /// 如果节点存在且有效则返回true,否则返回false + /// 要检查的节点。 + /// 如果节点存在、实例有效、未进入删除队列且仍在场景树中,则返回 public static bool IsNodeAlive(Node? node) { - // 验证节点是否存在、实例是否有效、未被标记为删除且在场景树中 return node != null && IsInstanceValid(node) && !node.IsQueuedForDeletion() && node.IsInsideTree(); } + /// + /// 根据分段选择具体调度器。 + /// + /// 目标执行段。 + /// 与分段对应的协程调度器。 + private CoroutineScheduler GetScheduler(Segment segment) + { + return segment switch + { + Segment.Process => ProcessScheduler, + Segment.ProcessIgnorePause => ProcessIgnorePauseScheduler, + Segment.PhysicsProcess => PhysicsScheduler, + Segment.DeferredProcess => DeferredScheduler, + _ => throw new ArgumentOutOfRangeException(nameof(segment), segment, "Unsupported coroutine segment.") + }; + } + + /// + /// 在当前实例上查询指定句柄的快照。 + /// + /// 协程句柄。 + /// 查询成功时返回快照。 + /// 如果找到活跃协程则返回 + private bool TryGetSnapshotOnInstance(CoroutineHandle handle, out CoroutineSnapshot snapshot) + { + return ProcessScheduler.TryGetSnapshot(handle, out snapshot) + || ProcessIgnorePauseScheduler.TryGetSnapshot(handle, out snapshot) + || PhysicsScheduler.TryGetSnapshot(handle, out snapshot) + || DeferredScheduler.TryGetSnapshot(handle, out snapshot); + } + + /// + /// 注册节点归属协程,并在节点退树时强制终止该协程。 + /// + /// 协程归属节点。 + /// 要登记的协程句柄。 + private void RegisterOwnedCoroutine(Node owner, CoroutineHandle handle) + { + var ownerId = owner.GetInstanceId(); + var registration = + new OwnedCoroutineRegistration(owner, ownerId, handle, ownedHandle => _ = KillOnInstance(ownedHandle)); + + _ownedCoroutineRegistrations[handle] = registration; + if (!_ownedCoroutinesByNode.TryGetValue(ownerId, out var handles)) + { + handles = new HashSet(); + _ownedCoroutinesByNode[ownerId] = handles; + } + + handles.Add(handle); + owner.TreeExiting += registration.OnOwnerTreeExiting; + } + + /// + /// 在协程结束时解除节点归属回调并清理索引。 + /// + /// 已结束的协程句柄。 + /// 协程最终状态。 + /// 若失败则为异常对象。 + private void HandleCoroutineFinished( + CoroutineHandle handle, + CoroutineCompletionStatus status, + Exception? exception) + { + CleanupOwnedCoroutineRegistration(handle); + } + + /// + /// 清理单个协程对应的节点归属注册。 + /// + /// 要清理的协程句柄。 + private void CleanupOwnedCoroutineRegistration(CoroutineHandle handle) + { + if (!_ownedCoroutineRegistrations.TryGetValue(handle, out var registration)) + { + return; + } + + if (registration.Owner.TryGetTarget(out var owner) && IsInstanceValid(owner)) + { + owner.TreeExiting -= registration.OnOwnerTreeExiting; + } + + if (_ownedCoroutinesByNode.TryGetValue(registration.OwnerId, out var handles)) + { + handles.Remove(handle); + if (handles.Count == 0) + { + _ownedCoroutinesByNode.Remove(registration.OwnerId); + } + } + + _ownedCoroutineRegistrations.Remove(handle); + } + + /// + /// 清理所有已登记的节点归属回调。 + /// + private void DetachAllOwnedRegistrations() + { + foreach (var handle in _ownedCoroutineRegistrations.Keys.ToArray()) + { + CleanupOwnedCoroutineRegistration(handle); + } + } + #endregion #region 延迟调用 /// - /// 延迟调用指定动作 + /// 延迟执行指定动作。 /// - /// 延迟时间(秒) - /// 要执行的动作 - /// 执行段 - /// 协程句柄 - public static CoroutineHandle CallDelayed( - double delay, - Action? action, - Segment segment = Segment.Process) + /// 延迟时间,单位秒。 + /// 到期时执行的动作。 + /// 执行所在的协程段。 + /// 新创建的协程句柄。 + public static CoroutineHandle CallDelayed(double delay, Action? action, Segment segment = Segment.Process) { if (action == null) + { return default; + } return RunCoroutine(DelayedCallCoroutine(delay, action), segment); } /// - /// 延迟调用指定动作,支持取消条件 + /// 延迟执行指定动作,并在节点失活时自动放弃执行。 /// - /// 延迟时间(秒) - /// 要执行的动作 - /// 取消条件节点 - /// 执行段 - /// 协程句柄 + /// 延迟时间,单位秒。 + /// 到期时执行的动作。 + /// 用于控制生命周期的节点。 + /// 执行所在的协程段。 + /// 新创建的协程句柄。 public static CoroutineHandle CallDelayed( double delay, Action? action, @@ -542,44 +838,42 @@ public partial class Timing : Node Segment segment = Segment.Process) { if (action == null) + { return default; + } - return RunCoroutine( - DelayedCallWithCancelCoroutine(delay, action, cancelWith), - segment); + return RunOwnedCoroutine(cancelWith, DelayedCallWithCancelCoroutine(delay, action, cancelWith), segment); } /// - /// 延迟调用协程实现 + /// 延迟调用协程实现。 /// - /// 延迟时间 - /// 要执行的动作 - /// 协程枚举器 - private static IEnumerator DelayedCallCoroutine( - double delay, - Action action) + /// 延迟时间。 + /// 要执行的动作。 + /// 可直接交给调度器运行的协程枚举器。 + private static IEnumerator DelayedCallCoroutine(double delay, Action action) { yield return new Delay(delay); action(); } /// - /// 带取消条件的延迟调用协程实现 + /// 带节点生命周期判断的延迟调用协程实现。 /// - /// 延迟时间 - /// 要执行的动作 - /// 取消条件节点 - /// 协程枚举器 - private static IEnumerator DelayedCallWithCancelCoroutine( - double delay, - Action action, + /// 延迟时间。 + /// 要执行的动作。 + /// 生命周期检查节点。 + /// 可直接交给调度器运行的协程枚举器。 + private static IEnumerator DelayedCallWithCancelCoroutine(double delay, Action action, Node cancelWith) { yield return new Delay(delay); if (IsNodeAlive(cancelWith)) + { action(); + } } #endregion -} +} \ No newline at end of file diff --git a/GFramework.csproj b/GFramework.csproj index 6221359b..b34d43f5 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -62,6 +62,7 @@ + @@ -102,6 +103,7 @@ + @@ -128,6 +130,7 @@ + diff --git a/GFramework.sln b/GFramework.sln index 0cef5d4d..6d4eb45f 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGenerators.Tests", "GFramework.Godot.SourceGenerators.Tests\GFramework.Godot.SourceGenerators.Tests.csproj", "{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -262,6 +264,18 @@ Global {E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x64.Build.0 = Release|Any CPU {E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.ActiveCfg = Release|Any CPU {E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.Build.0 = Release|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x64.ActiveCfg = Debug|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x64.Build.0 = Debug|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x86.ActiveCfg = Debug|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Debug|x86.Build.0 = Debug|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|Any CPU.Build.0 = Release|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.ActiveCfg = Release|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x64.Build.0 = Release|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.ActiveCfg = Release|Any CPU + {576119E2-13D0-4ACF-A012-D01C320E8BF3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE