Merge pull request #176 from GeWuYou/feat/coroutine-core-and-godot-integration

Feat/coroutine core and godot integration
This commit is contained in:
gewuyou 2026-04-06 07:23:05 +08:00 committed by GitHub
commit e67cfd4808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1891 additions and 1418 deletions

View File

@ -156,6 +156,11 @@ jobs:
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \ --logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
--results-directory TestResults & --results-directory TestResults &
dotnet test GFramework.Godot.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=godot-$RANDOM.trx" \
--results-directory TestResults &
# 等待所有后台测试完成 # 等待所有后台测试完成
wait wait

View File

@ -0,0 +1,28 @@
namespace GFramework.Core.Abstractions.Coroutine;
/// <summary>
/// 表示协程的最终完成结果。
/// </summary>
public enum CoroutineCompletionStatus
{
/// <summary>
/// 调度器无法确认该句柄的最终结果。
/// 这通常意味着句柄无效,或者句柄对应的历史结果已经不可用。
/// </summary>
Unknown,
/// <summary>
/// 协程自然执行结束。
/// </summary>
Completed,
/// <summary>
/// 协程被外部终止、清空或取消令牌中断。
/// </summary>
Cancelled,
/// <summary>
/// 协程在推进过程中抛出了异常。
/// </summary>
Faulted
}

View File

@ -0,0 +1,29 @@
namespace GFramework.Core.Abstractions.Coroutine;
/// <summary>
/// 表示协程调度器当前所处的执行阶段。
/// </summary>
/// <remarks>
/// 某些等待指令具有阶段语义,例如 <c>WaitForFixedUpdate</c> 和 <c>WaitForEndOfFrame</c>。
/// 宿主应为这些语义提供匹配的调度器阶段,否则这类等待不会自然完成。
/// </remarks>
public enum CoroutineExecutionStage
{
/// <summary>
/// 默认更新阶段。
/// 普通时间等待、下一帧等待以及大多数条件等待都会在该阶段推进。
/// </summary>
Update,
/// <summary>
/// 固定更新阶段。
/// 仅与固定步相关的等待指令会在该阶段完成。
/// </summary>
FixedUpdate,
/// <summary>
/// 帧结束阶段。
/// 仅与帧尾或延迟执行相关的等待指令会在该阶段完成。
/// </summary>
EndOfFrame
}

View File

@ -0,0 +1,233 @@
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine;
using GFramework.Core.Coroutine.Instructions;
namespace GFramework.Core.Tests.Coroutine;
/// <summary>
/// 协程调度器增强行为测试。
/// </summary>
[TestFixture]
public sealed class CoroutineSchedulerAdvancedTests
{
/// <summary>
/// 验证 WaitForSecondsRealtime 使用独立的真实时间源推进。
/// </summary>
[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<IYieldInstruction> 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));
}
/// <summary>
/// 验证固定更新等待指令仅在固定阶段调度器中推进。
/// </summary>
[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<IYieldInstruction> DefaultCoroutine()
{
yield return new WaitForFixedUpdate();
defaultCompleted = true;
}
IEnumerator<IYieldInstruction> 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));
}
/// <summary>
/// 验证取消令牌会在下一次调度循环中终止协程并记录结果。
/// </summary>
[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<IYieldInstruction> 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));
}
/// <summary>
/// 验证调度器可以暴露活跃协程快照。
/// </summary>
[Test]
public void TryGetSnapshot_Should_Return_Current_Waiting_Instruction_And_Stage()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(
timeSource,
executionStage: CoroutineExecutionStage.EndOfFrame);
IEnumerator<IYieldInstruction> 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));
}
/// <summary>
/// 验证异常结束的协程会记录为 Faulted。
/// </summary>
[Test]
public async Task WaitForCompletionAsync_Should_Return_Faulted_For_Failing_Coroutine()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
IEnumerator<IYieldInstruction> 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));
}
/// <summary>
/// 验证完成状态缓存有固定上限,避免无限增长。
/// </summary>
[Test]
public void CompletionStatusHistory_Should_Be_Bounded()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
var handles = new List<CoroutineHandle>();
IEnumerator<IYieldInstruction> ImmediateCoroutine()
{
yield break;
}
for (var i = 0; i < 1100; i++)
{
handles.Add(scheduler.Run(ImmediateCoroutine()));
}
Assert.That(scheduler.TryGetCompletionStatus(handles[0], out _), Is.False);
Assert.That(scheduler.TryGetCompletionStatus(handles[^1], out var latestStatus), Is.True);
Assert.That(latestStatus, Is.EqualTo(CoroutineCompletionStatus.Completed));
}
/// <summary>
/// 验证作为首个等待指令的 WaitForCoroutine 会立即启动子协程,并沿用父协程取消令牌。
/// </summary>
[Test]
public async Task WaitForCoroutine_Should_Start_Child_During_Prewarm_And_Propagate_Cancellation()
{
var timeSource = new FakeTimeSource();
var scheduler = new CoroutineScheduler(timeSource);
using var cancellationTokenSource = new CancellationTokenSource();
IEnumerator<IYieldInstruction> ChildCoroutine()
{
yield return new Delay(10);
}
IEnumerator<IYieldInstruction> ParentCoroutine()
{
yield return new WaitForCoroutine(ChildCoroutine());
}
var handle = scheduler.Run(ParentCoroutine(), cancellationToken: cancellationTokenSource.Token);
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(2));
cancellationTokenSource.Cancel();
timeSource.Advance(0.1);
scheduler.Update();
var status = await scheduler.WaitForCompletionAsync(handle);
Assert.That(status, Is.EqualTo(CoroutineCompletionStatus.Cancelled));
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(0));
}
}

View File

@ -7,6 +7,12 @@ namespace GFramework.Core.Coroutine;
/// </summary> /// </summary>
internal class CoroutineMetadata internal class CoroutineMetadata
{ {
/// <summary>
/// 协程所属调度器的执行阶段。
/// 该值用于诊断等待语义是否与当前宿主阶段匹配。
/// </summary>
public CoroutineExecutionStage ExecutionStage;
/// <summary> /// <summary>
/// 协程的分组标识符,用于批量管理协程 /// 协程的分组标识符,用于批量管理协程
/// </summary> /// </summary>

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,18 @@ namespace GFramework.Core.Coroutine;
/// </summary> /// </summary>
internal sealed class CoroutineSlot internal sealed class CoroutineSlot
{ {
/// <summary>
/// 由外部取消令牌创建的注册。
/// 调度器在协程结束时必须释放该注册,避免泄漏取消回调。
/// </summary>
public CancellationTokenRegistration CancellationRegistration;
/// <summary>
/// 创建该协程时传入的取消令牌。
/// 当协程启动子协程时,会把同一个取消令牌继续传递下去,以保持父子协程的取消语义一致。
/// </summary>
public CancellationToken CancellationToken;
/// <summary> /// <summary>
/// 协程枚举器,包含协程的执行逻辑 /// 协程枚举器,包含协程的执行逻辑
/// </summary> /// </summary>

View File

@ -0,0 +1,29 @@
using GFramework.Core.Abstractions.Coroutine;
namespace GFramework.Core.Coroutine;
/// <summary>
/// 表示某个活跃协程在调度器中的只读运行快照。
/// </summary>
/// <param name="Handle">协程句柄。</param>
/// <param name="State">当前协程状态。</param>
/// <param name="Priority">当前协程优先级。</param>
/// <param name="Tag">可选标签。</param>
/// <param name="Group">可选分组。</param>
/// <param name="StartTimeMs">协程启动时间,单位为毫秒。</param>
/// <param name="IsWaiting">当前是否正被等待指令阻塞。</param>
/// <param name="WaitingInstructionType">
/// 当前等待指令的具体类型。
/// 若协程当前未处于等待状态,则该值为 <see langword="null" />。
/// </param>
/// <param name="ExecutionStage">所属调度器的执行阶段。</param>
public readonly record struct CoroutineSnapshot(
CoroutineHandle Handle,
CoroutineState State,
CoroutinePriority Priority,
string? Tag,
string? Group,
double StartTimeMs,
bool IsWaiting,
Type? WaitingInstructionType,
CoroutineExecutionStage ExecutionStage);

View File

@ -0,0 +1,73 @@
using System.Collections.Generic;
using GFramework.Godot.Coroutine;
using NUnit.Framework;
namespace GFramework.Godot.Tests.Coroutine;
/// <summary>
/// GodotTimeSource 的单元测试。
/// </summary>
[TestFixture]
public sealed class GodotTimeSourceTests
{
/// <summary>
/// 验证增量模式会直接累加传入的 delta。
/// </summary>
[Test]
public void Update_Should_Accumulate_Delta_When_Using_Delta_Mode()
{
var values = new Queue<double>([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));
}
/// <summary>
/// 验证绝对时间模式会根据前后两次采样计算 delta。
/// </summary>
[Test]
public void Update_Should_Calculate_Delta_When_Using_Absolute_Time_Mode()
{
var values = new Queue<double>([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));
}
/// <summary>
/// 验证绝对时间源在回拨时仍保持单调,不会把 CurrentTime 拉回去。
/// </summary>
[Test]
public void Update_Should_Keep_Absolute_Time_Monotonic_When_Provider_Goes_Backwards()
{
var values = new Queue<double>([5.0, 4.0, 6.5]);
var timeSource = new GodotTimeSource(() => values.Dequeue(), useAbsoluteTime: true);
timeSource.Update();
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(0).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(5.0).Within(0.0001));
timeSource.Update();
Assert.That(timeSource.DeltaTime, Is.EqualTo(1.5).Within(0.0001));
Assert.That(timeSource.CurrentTime, Is.EqualTo(6.5).Within(0.0001));
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0"/>
<PackageReference Include="NUnit" Version="4.5.1"/>
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Godot\GFramework.Godot.csproj"/>
</ItemGroup>
</Project>

View File

@ -35,6 +35,25 @@ public static class CoroutineNodeExtensions
return Timing.RunCoroutine(coroutine, segment, tag); return Timing.RunCoroutine(coroutine, segment, tag);
} }
/// <summary>
/// 以指定节点作为生命周期所有者运行协程。
/// </summary>
/// <param name="owner">拥有该协程生命周期的节点。</param>
/// <param name="coroutine">要启动的协程枚举器。</param>
/// <param name="segment">协程运行的时间段。</param>
/// <param name="tag">协程标签。</param>
/// <param name="cancellationToken">可选取消令牌。</param>
/// <returns>返回协程句柄。</returns>
public static CoroutineHandle RunCoroutine(
this Node owner,
IEnumerator<IYieldInstruction> coroutine,
Segment segment = Segment.Process,
string? tag = null,
CancellationToken cancellationToken = default)
{
return Timing.RunOwnedCoroutine(owner, coroutine, segment, tag, cancellationToken);
}
/// <summary> /// <summary>
/// 让协程在指定节点被销毁时自动取消。 /// 让协程在指定节点被销毁时自动取消。
/// </summary> /// </summary>

View File

@ -3,40 +3,80 @@
namespace GFramework.Godot.Coroutine; namespace GFramework.Godot.Coroutine;
/// <summary> /// <summary>
/// Godot时间源实现用于提供基于Godot引擎的时间信息 /// Godot 时间源实现,用于为协程调度器提供缩放时间或真实时间数据。
/// </summary> /// </summary>
/// <param name="getDeltaFunc">获取增量时间的函数委托</param> /// <param name="timeProvider">
public class GodotTimeSource(Func<double> getDeltaFunc) : ITimeSource /// 时间提供函数。
/// 在默认模式下该函数返回“本帧增量”;在绝对时间模式下该函数返回“当前绝对时间(秒)”。
/// </param>
/// <param name="useAbsoluteTime">
/// 是否把 <paramref name="timeProvider" /> 返回值解释为绝对时间。
/// 启用后,<see cref="Update" /> 会通过相邻两次读数计算 <see cref="DeltaTime" />。
/// </param>
public sealed class GodotTimeSource(Func<double> timeProvider, bool useAbsoluteTime = false) : ITimeSource
{ {
private readonly Func<double> _getDeltaFunc = getDeltaFunc ?? throw new ArgumentNullException(nameof(getDeltaFunc)); private readonly Func<double> _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
private bool _initialized;
private double _lastAbsoluteTime;
/// <summary> /// <summary>
/// 获取当前累计时间 /// 获取当前累计时间
/// </summary> /// </summary>
public double CurrentTime { get; private set; } public double CurrentTime { get; private set; }
/// <summary> /// <summary>
/// 获取上一帧的时间增量 /// 获取上一帧的时间增量
/// </summary> /// </summary>
public double DeltaTime { get; private set; } public double DeltaTime { get; private set; }
/// <summary> /// <summary>
/// 更新时间源,计算新的增量时间和累计时间 /// 更新时间源,计算新的时间增量与累计时间。
/// </summary> /// </summary>
public void Update() public void Update()
{ {
// 调用外部提供的函数获取当前帧的时间增量 var value = _timeProvider();
DeltaTime = _getDeltaFunc(); if (useAbsoluteTime)
// 累加到总时间中 {
if (!_initialized)
{
_initialized = true;
_lastAbsoluteTime = value;
CurrentTime = value;
DeltaTime = 0;
return;
}
// 对绝对时间源做单调钳制,避免 provider 回拨后把 CurrentTime 也拉回去。
var nextTime = Math.Max(value, _lastAbsoluteTime);
DeltaTime = nextTime - _lastAbsoluteTime;
_lastAbsoluteTime = nextTime;
CurrentTime = nextTime;
return;
}
DeltaTime = value;
CurrentTime += DeltaTime; CurrentTime += DeltaTime;
} }
/// <summary> /// <summary>
/// 重置时间源到初始状态 /// 创建基于 Godot 单调时钟的真实时间源。
/// </summary>
/// <returns>返回一个不受场景暂停与时间缩放影响的时间源实例。</returns>
public static GodotTimeSource CreateRealtime()
{
return new GodotTimeSource(
() => Time.GetTicksUsec() / 1_000_000.0,
useAbsoluteTime: true);
}
/// <summary>
/// 重置时间源到初始状态。
/// </summary> /// </summary>
public void Reset() public void Reset()
{ {
CurrentTime = 0; CurrentTime = 0;
DeltaTime = 0; DeltaTime = 0;
_initialized = false;
_lastAbsoluteTime = 0;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -62,6 +62,7 @@
<None Remove="GFramework.SourceGenerators.Attributes\**"/> <None Remove="GFramework.SourceGenerators.Attributes\**"/>
<None Remove="Godot\**"/> <None Remove="Godot\**"/>
<None Remove="GFramework.Game.Tests\**"/> <None Remove="GFramework.Game.Tests\**"/>
<None Remove="GFramework.Godot.Tests\**"/>
</ItemGroup> </ItemGroup>
<!-- 聚合核心模块 --> <!-- 聚合核心模块 -->
<ItemGroup> <ItemGroup>
@ -102,6 +103,7 @@
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/> <Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
<Compile Remove="Godot\**"/> <Compile Remove="Godot\**"/>
<Compile Remove="GFramework.Game.Tests\**"/> <Compile Remove="GFramework.Game.Tests\**"/>
<Compile Remove="GFramework.Godot.Tests\**"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Remove="GFramework.Core\**"/> <EmbeddedResource Remove="GFramework.Core\**"/>
@ -128,6 +130,7 @@
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/> <EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
<EmbeddedResource Remove="Godot\**"/> <EmbeddedResource Remove="Godot\**"/>
<EmbeddedResource Remove="GFramework.Game.Tests\**"/> <EmbeddedResource Remove="GFramework.Game.Tests\**"/>
<EmbeddedResource Remove="GFramework.Godot.Tests\**"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/> <AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>

View File

@ -36,6 +36,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GF
EndProject 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}" 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.Tests", "GFramework.Godot.Tests\GFramework.Godot.Tests.csproj", "{576119E2-13D0-4ACF-A012-D01C320E8BF3}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|x64.Build.0 = Release|Any CPU
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -1,61 +1,95 @@
--- ---
title: 协程系统 title: 协程系统
description: 协程系统提供基于 IEnumerator<IYieldInstruction>调度、等待和组合能力可与事件、Task、命令与查询集成 description: 基于 IEnumerator<IYieldInstruction>协程调度系统支持时间等待、阶段等待、Task 桥接、事件等待与运行时快照查询
--- ---
# 协程系统 # 协程系统
## 概述 ## 概述
GFramework 的 Core 协程系统基于 `IEnumerator<IYieldInstruction>` 构建,通过 `CoroutineScheduler` `GFramework.Core.Coroutine` 提供一个宿主无关的协程内核。它围绕 `CoroutineScheduler` 工作,统一处理:
统一推进协程执行。它适合处理分帧逻辑、时间等待、条件等待、Task 桥接,以及事件驱动的异步流程。
协程系统主要由以下部分组成: - `IEnumerator<IYieldInstruction>` 形式的协程推进
- 时间等待、条件等待、Task 等待与事件等待
- 标签、分组、暂停、恢复与终止
- 取消令牌、完成状态查询与运行快照
- 调度阶段语义,例如默认更新、固定更新和帧结束
- `CoroutineScheduler`:负责运行、更新和控制协程 Core 协程本身不依赖任何具体引擎;阶段语义是否真实成立,取决于宿主是否为调度器提供了匹配的执行阶段。
- `CoroutineHandle`:用于标识协程实例并控制其状态
- `IYieldInstruction`:定义等待行为的统一接口
- `Instructions`:内置等待指令集合
- `CoroutineHelper`:提供常用等待与生成器辅助方法
- `Extensions`:提供 Task、组合、命令、查询和 Mediator 场景下的扩展方法
## 核心概念 ## CoroutineScheduler
### CoroutineScheduler ### 基础创建
`CoroutineScheduler` 是协程系统的核心调度器。构造时需要提供 `ITimeSource`,调度器会在每次 `Update()` 时读取时间增量并推进所有活跃协程。
```csharp ```csharp
using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine; using GFramework.Core.Coroutine;
ITimeSource timeSource = /* 你的时间源实现 */; ITimeSource scaledTimeSource = /* 游戏时间 */;
var scheduler = new CoroutineScheduler(timeSource); ITimeSource realtimeTimeSource = /* 真实时间,可选 */;
var handle = scheduler.Run(MyCoroutine()); var scheduler = new CoroutineScheduler(
scaledTimeSource,
realtimeTimeSource: realtimeTimeSource,
executionStage: CoroutineExecutionStage.Update);
// 在你的主循环中推进协程 var handle = scheduler.Run(MyCoroutine(), tag: "bootstrap", group: "loading");
// 在宿主主循环中推进协程
scheduler.Update(); scheduler.Update();
``` ```
如果需要统计信息,可以启用构造函数的 `enableStatistics` 参数。 构造参数中最重要的两个语义是:
### CoroutineHandle - `realtimeTimeSource`
- 如果提供,`WaitForSecondsRealtime` 会使用它的 `DeltaTime`
- 如果不提供,实时等待会退化为使用默认时间源
- `executionStage`
- `Update`:默认阶段
- `FixedUpdate`:固定步阶段
- `EndOfFrame`:帧结束阶段
`CoroutineHandle` 用于引用具体协程,并配合调度器进行控制: ### 控制与完成状态
```csharp ```csharp
var handle = scheduler.Run(MyCoroutine(), tag: "gameplay", group: "battle"); using var cts = new CancellationTokenSource();
if (scheduler.IsCoroutineAlive(handle)) var handle = scheduler.Run(
{ LoadResources(),
scheduler.Pause(handle); tag: "loading",
scheduler.Resume(handle); group: "bootstrap",
scheduler.Kill(handle); cancellationToken: cts.Token);
}
scheduler.Pause(handle);
scheduler.Resume(handle);
scheduler.Kill(handle);
var completionStatus = await scheduler.WaitForCompletionAsync(handle);
``` ```
### IYieldInstruction 协程的最终结果由 `CoroutineCompletionStatus` 表示:
- `Completed`
- `Cancelled`
- `Faulted`
- `Unknown`
### 快照与可观测性
```csharp
if (scheduler.TryGetSnapshot(handle, out var snapshot))
{
Console.WriteLine(snapshot.State);
Console.WriteLine(snapshot.WaitingInstructionType);
Console.WriteLine(snapshot.ExecutionStage);
}
var allSnapshots = scheduler.GetActiveSnapshots();
```
快照适合做诊断、调试面板和运行中状态检查。
## IYieldInstruction
协程通过 `yield return IYieldInstruction` 表达等待逻辑: 协程通过 `yield return IYieldInstruction` 表达等待逻辑:
@ -67,91 +101,40 @@ public interface IYieldInstruction
} }
``` ```
## 基本用法
### 创建简单协程
```csharp
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions;
public IEnumerator<IYieldInstruction> SimpleCoroutine()
{
Console.WriteLine("开始");
yield return new Delay(2.0);
Console.WriteLine("2 秒后");
yield return new WaitOneFrame();
Console.WriteLine("下一帧");
}
```
### 使用 CoroutineHelper
`CoroutineHelper` 提供了一组常用等待和生成器辅助方法:
```csharp
using GFramework.Core.Coroutine;
public IEnumerator<IYieldInstruction> HelperCoroutine()
{
yield return CoroutineHelper.WaitForSeconds(1.5);
yield return CoroutineHelper.WaitForOneFrame();
yield return CoroutineHelper.WaitForFrames(10);
yield return CoroutineHelper.WaitUntil(() => isReady);
yield return CoroutineHelper.WaitWhile(() => isLoading);
}
```
除了直接返回等待指令,`CoroutineHelper` 也可以直接生成可运行的协程枚举器:
```csharp
scheduler.Run(CoroutineHelper.DelayedCall(2.0, () => Console.WriteLine("延迟执行")));
scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () => Console.WriteLine("重复执行")));
using var cts = new CancellationTokenSource();
scheduler.Run(CoroutineHelper.RepeatCallForever(1.0, () => Console.WriteLine("持续执行"), cts.Token));
```
### 控制协程状态
```csharp
var handle = scheduler.Run(LoadResources(), tag: "loading", group: "bootstrap");
scheduler.Pause(handle);
scheduler.Resume(handle);
scheduler.Kill(handle);
scheduler.KillByTag("loading");
scheduler.PauseGroup("bootstrap");
scheduler.ResumeGroup("bootstrap");
scheduler.KillGroup("bootstrap");
var cleared = scheduler.Clear();
```
## 常用等待指令 ## 常用等待指令
### 时间与帧 ### 时间与帧
```csharp ```csharp
yield return new Delay(1.0); yield return new Delay(1.0);
yield return new WaitForSecondsScaled(1.0);
yield return new WaitForSecondsRealtime(1.0); yield return new WaitForSecondsRealtime(1.0);
yield return new WaitOneFrame(); yield return new WaitOneFrame();
yield return new WaitForNextFrame(); yield return new WaitForNextFrame();
yield return new WaitForFrames(5); yield return new WaitForFrames(5);
yield return new WaitForEndOfFrame();
yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate();
yield return new WaitForEndOfFrame();
``` ```
语义说明:
- `Delay``WaitForSecondsScaled`
- 使用调度器默认时间源推进
- `WaitForSecondsRealtime`
- 优先使用调度器的 `realtimeTimeSource`
- `WaitForFixedUpdate`
- 仅在 `CoroutineExecutionStage.FixedUpdate` 调度器中推进
- `WaitForEndOfFrame`
- 仅在 `CoroutineExecutionStage.EndOfFrame` 调度器中推进
如果宿主没有提供匹配阶段,这类阶段型等待不会自然完成。
### 条件等待 ### 条件等待
```csharp ```csharp
yield return new WaitUntil(() => health > 0); yield return new WaitUntil(() => health > 0);
yield return new WaitWhile(() => isLoading); yield return new WaitWhile(() => isLoading);
yield return new WaitForPredicate(() => hp >= maxHp); yield return new WaitForPredicate(() => hp >= maxHp);
yield return new WaitForPredicate(() => isBusy, waitForTrue: false);
yield return new WaitUntilOrTimeout(() => connected, timeoutSeconds: 5.0); yield return new WaitUntilOrTimeout(() => connected, timeoutSeconds: 5.0);
yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: true); yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: true);
``` ```
@ -159,27 +142,14 @@ yield return new WaitForConditionChange(() => isPaused, waitForTransitionTo: tru
### Task 桥接 ### Task 桥接
```csharp ```csharp
using System.Threading.Tasks;
using GFramework.Core.Coroutine.Extensions; using GFramework.Core.Coroutine.Extensions;
Task loadTask = LoadDataAsync(); Task loadTask = LoadDataAsync();
yield return loadTask.AsCoroutineInstruction(); yield return loadTask.AsCoroutineInstruction();
var handle = scheduler.StartTaskAsCoroutine(LoadDataAsync());
``` ```
也可以将 `Task` 转成协程枚举器后直接交给调度器:
```csharp
var coroutine = LoadDataAsync().ToCoroutineEnumerator();
var handle1 = scheduler.Run(coroutine);
var handle2 = scheduler.StartTaskAsCoroutine(LoadDataAsync());
```
- `AsCoroutineInstruction()` 适合已经处在某个协程内部,只需要在当前位置等待 `Task` 完成的场景。
- `ToCoroutineEnumerator()` 适合需要把 `Task` 先转换成 `IEnumerator<IYieldInstruction>`,再传给 `scheduler.Run(...)`
`Sequence(...)` 或其他只接受协程枚举器的 API。
- `StartTaskAsCoroutine()` 适合已经持有 `CoroutineScheduler`,并希望把 `Task` 直接作为一个顶层协程启动的场景。
### 等待事件 ### 等待事件
```csharp ```csharp
@ -188,236 +158,52 @@ using GFramework.Core.Coroutine.Instructions;
public IEnumerator<IYieldInstruction> WaitForEventExample(IEventBus eventBus) public IEnumerator<IYieldInstruction> WaitForEventExample(IEventBus eventBus)
{ {
using var waitEvent = new WaitForEvent<PlayerDiedEvent>(eventBus); using var wait = new WaitForEvent<PlayerJoinedEvent>(eventBus);
yield return waitEvent;
var eventData = waitEvent.EventData;
Console.WriteLine($"玩家 {eventData!.PlayerId} 死亡");
}
```
为事件等待附加超时:
```csharp
public IEnumerator<IYieldInstruction> WaitForEventWithTimeoutExample(IEventBus eventBus)
{
using var waitEvent = new WaitForEvent<PlayerJoinedEvent>(eventBus);
var timeoutWait = new WaitForEventWithTimeout<PlayerJoinedEvent>(waitEvent, 5.0f);
yield return timeoutWait;
if (timeoutWait.IsTimeout)
Console.WriteLine("等待超时");
else
Console.WriteLine($"玩家加入: {timeoutWait.EventData!.PlayerName}");
}
```
等待两个事件中的任意一个:
```csharp
public IEnumerator<IYieldInstruction> WaitForEitherEvent(IEventBus eventBus)
{
using var wait = new WaitForMultipleEvents<PlayerReadyEvent, PlayerQuitEvent>(eventBus);
yield return wait; yield return wait;
if (wait.TriggeredBy == 1) Console.WriteLine(wait.EventData?.PlayerName);
Console.WriteLine($"Ready: {wait.FirstEventData}");
else
Console.WriteLine($"Quit: {wait.SecondEventData}");
} }
``` ```
### 协程组合 ## CoroutineHelper
等待子协程完成: `CoroutineHelper` 提供一组常用简写:
```csharp
yield return CoroutineHelper.WaitForSeconds(1.5);
yield return CoroutineHelper.WaitForOneFrame();
yield return CoroutineHelper.WaitForFrames(10);
yield return CoroutineHelper.WaitUntil(() => isReady);
yield return CoroutineHelper.WaitWhile(() => isLoading);
```
也可以直接生成可运行的协程枚举器:
```csharp
scheduler.Run(CoroutineHelper.DelayedCall(2.0, () => Console.WriteLine("延迟执行")));
scheduler.Run(CoroutineHelper.RepeatCall(1.0, 5, () => Console.WriteLine("重复执行")));
```
## 协程组合
```csharp ```csharp
public IEnumerator<IYieldInstruction> ParentCoroutine() public IEnumerator<IYieldInstruction> ParentCoroutine()
{ {
Console.WriteLine("父协程开始");
yield return new WaitForCoroutine(ChildCoroutine()); yield return new WaitForCoroutine(ChildCoroutine());
Console.WriteLine("子协程完成");
} }
private IEnumerator<IYieldInstruction> ChildCoroutine() private IEnumerator<IYieldInstruction> ChildCoroutine()
{ {
yield return CoroutineHelper.WaitForSeconds(1.0); yield return new Delay(1.0);
Console.WriteLine("子协程执行");
} }
``` ```
等待多个句柄全部完成: 如果需要等待多个顶层协程句柄,可以结合 `WaitForAllCoroutines``ParallelCoroutines(...)` 使用。
```csharp ## 建议
public IEnumerator<IYieldInstruction> WaitForMultipleCoroutines(CoroutineScheduler scheduler)
{
var handles = new List<CoroutineHandle>
{
scheduler.Run(LoadTexture()),
scheduler.Run(LoadAudio()),
scheduler.Run(LoadModel())
};
yield return new WaitForAllCoroutines(scheduler, handles); - 普通游戏时间等待优先使用 `Delay``WaitForSecondsScaled`
- 只有宿主提供真实时间源时再使用 `WaitForSecondsRealtime`
Console.WriteLine("所有资源加载完成"); - 只有宿主显式区分阶段时才使用 `WaitForFixedUpdate``WaitForEndOfFrame`
} - 需要对接生命周期或外部取消时,优先传入 `CancellationToken`
``` - 需要诊断线上状态时,优先使用 `TryGetSnapshot(...)``GetActiveSnapshots()`
### 进度等待
```csharp
public IEnumerator<IYieldInstruction> LoadingWithProgress()
{
yield return CoroutineHelper.WaitForProgress(
duration: 3.0,
onProgress: progress => Console.WriteLine($"加载进度: {progress * 100:F0}%"));
}
```
## 扩展方法
### 组合扩展
`CoroutineComposeExtensions` 提供链式顺序组合能力:
```csharp
using GFramework.Core.Coroutine.Extensions;
var chained =
LoadConfig()
.Then(() => Console.WriteLine("配置加载完成"))
.Then(StartBattle());
scheduler.Run(chained);
```
### 协程生成扩展
`CoroutineExtensions` 提供了一些常用的协程生成器:
```csharp
using GFramework.Core.Coroutine.Extensions;
var delayed = CoroutineExtensions.ExecuteAfter(2.0, () => Console.WriteLine("延迟执行"));
var repeated = CoroutineExtensions.RepeatEvery(1.0, () => Console.WriteLine("tick"), count: 5);
var progress = CoroutineExtensions.WaitForSecondsWithProgress(3.0, p => Console.WriteLine(p));
scheduler.Run(delayed);
scheduler.Run(repeated);
scheduler.Run(progress);
```
顺序或并行组合多个协程:
```csharp
var sequence = CoroutineExtensions.Sequence(LoadConfig(), LoadScene(), StartBattle());
scheduler.Run(sequence);
var parallel = scheduler.ParallelCoroutines(LoadTexture(), LoadAudio(), LoadModel());
scheduler.Run(parallel);
```
### Task 扩展
`TaskCoroutineExtensions` 提供了三类扩展:
- `AsCoroutineInstruction()`:把 `Task` / `Task<T>` 包装成等待指令
- `ToCoroutineEnumerator()`:把 `Task` / `Task<T>` 转成协程枚举器
- `StartTaskAsCoroutine()`:直接通过调度器启动 Task 协程
### 命令、查询与 Mediator 扩展
这些扩展都定义在 `GFramework.Core.Coroutine.Extensions` 命名空间中。
### 命令协程
```csharp
using GFramework.Core.Coroutine.Extensions;
public IEnumerator<IYieldInstruction> ExecuteCommand(IContextAware contextAware)
{
yield return contextAware.SendCommandCoroutineWithErrorHandler(
new LoadSceneCommand(),
ex => Console.WriteLine(ex.Message));
}
```
如果命令执行后需要等待事件:
```csharp
public IEnumerator<IYieldInstruction> ExecuteCommandAndWaitEvent(IContextAware contextAware)
{
yield return contextAware.SendCommandAndWaitEventCoroutine<LoadSceneCommand, SceneLoadedEvent>(
new LoadSceneCommand(),
evt => Console.WriteLine($"场景加载完成: {evt.SceneName}"),
timeout: 5.0f);
}
```
### 查询协程
`SendQueryCoroutine` 会同步执行查询,并通过回调返回结果:
```csharp
public IEnumerator<IYieldInstruction> QueryPlayer(IContextAware contextAware)
{
yield return contextAware.SendQueryCoroutine<GetPlayerDataQuery, PlayerData>(
new GetPlayerDataQuery { PlayerId = 1 },
playerData => Console.WriteLine($"玩家名称: {playerData.Name}"));
}
```
### Mediator 协程
如果项目使用 `Mediator.IMediator`,还可以使用 `MediatorCoroutineExtensions`
```csharp
public IEnumerator<IYieldInstruction> ExecuteMediatorCommand(IContextAware contextAware)
{
yield return contextAware.SendCommandCoroutine(
new SaveArchiveCommand(),
ex => Console.WriteLine(ex.Message));
}
```
## 异常处理
调度器会在协程抛出未捕获异常时触发 `OnCoroutineException`
```csharp
scheduler.OnCoroutineException += (handle, exception) =>
{
Console.WriteLine($"协程 {handle} 异常: {exception.Message}");
};
```
如果协程等待的是 `Task`,也可以通过 `WaitForTask` / `WaitForTask<T>` 检查任务异常。
## 常见问题
### 协程什么时候执行?
协程在调度器的 `Update()` 中推进。调度器每次更新都会先更新 `ITimeSource`,再推进所有活跃协程。
### 协程是多线程的吗?
不是。协程本身仍由调用 `Update()` 的线程推进,通常用于主线程上的分帧流程控制。
### `Delay``CoroutineHelper.WaitForSeconds()` 有什么区别?
两者表达的是同一类等待语义。`CoroutineHelper.WaitForSeconds()` 只是 `Delay` 的辅助构造方法。
### 如何等待异步方法?
在现有协程里等待 `Task` 时,优先使用 `yield return task.AsCoroutineInstruction()`;如果要把 `Task` 单独交给调度器启动,使用
`scheduler.StartTaskAsCoroutine(task)`;如果中间还需要传给只接受协程枚举器的 API则先调用 `task.ToCoroutineEnumerator()`
## 相关文档
- [事件系统](/zh-CN/core/events)
- [CQRS](/zh-CN/core/cqrs)
- [协程系统教程](/zh-CN/tutorials/coroutine-tutorial)

View File

@ -2,41 +2,30 @@
## 概述 ## 概述
GFramework 的协程系统由两层组成 `GFramework.Godot.Coroutine` 在 Core 协程内核之上提供 Godot 宿主集成,负责把 Godot 的不同更新循环映射为真实的协程阶段语义
- `GFramework.Core.Coroutine` 提供通用调度器、`IYieldInstruction` 和一组等待指令。 - `Segment.Process`
- `GFramework.Godot.Coroutine` 提供 Godot 环境下的运行入口、分段调度以及节点生命周期辅助方法。 - `Segment.ProcessIgnorePause`
- `Segment.PhysicsProcess`
- `Segment.DeferredProcess`
Godot 集成层的核心入口包括: 它同时补充了以下宿主能力
- `RunCoroutine(...)` - 节点归属协程运行入口
- `Timing.RunGameCoroutine(...)` - 节点退树自动终止
- `Timing.RunUiCoroutine(...)` - Godot 真实时间源
- `Timing.CallDelayed(...)` - 句柄控制与快照查询
- `CancelWith(...)`
协程本身使用 `IEnumerator<IYieldInstruction>` ## 启动协程
## 主要能力 ### 直接运行枚举器
- 在 Godot 中按不同更新阶段运行协程
- 等待时间、帧、条件、Task 和事件总线事件
- 显式将协程与一个或多个 `Node` 的生命周期绑定
- 通过 `CoroutineHandle` 暂停、恢复、终止协程
- 将命令、查询、发布操作直接包装为协程运行
## 基本用法
### 启动协程
```csharp ```csharp
using System.Collections.Generic;
using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Coroutine.Instructions;
using GFramework.Godot.Coroutine; using GFramework.Godot.Coroutine;
using Godot;
public partial class MyNode : Node public partial class DemoNode : Node
{ {
public override void _Ready() public override void _Ready()
{ {
@ -45,242 +34,131 @@ public partial class MyNode : Node
private IEnumerator<IYieldInstruction> Demo() private IEnumerator<IYieldInstruction> Demo()
{ {
GD.Print("开始执行"); GD.Print("start");
yield return new Delay(1.0);
yield return new Delay(2.0);
GD.Print("2 秒后继续执行");
yield return new WaitForEndOfFrame(); yield return new WaitForEndOfFrame();
GD.Print("当前帧结束后继续执行"); GD.Print("done");
} }
} }
``` ```
`RunCoroutine()` 默认在 `Segment.Process` 上运行,也就是普通帧更新阶段 默认情况下,`RunCoroutine()` 会在 `Segment.Process` 上运行
除了枚举器扩展方法,也可以直接使用 `Timing` 的静态入口: ### 以 Node 作为生命周期所有者运行
更推荐的方式是以节点为入口运行协程:
```csharp ```csharp
Timing.RunCoroutine(Demo()); public override void _Ready()
Timing.RunGameCoroutine(GameLoop());
Timing.RunUiCoroutine(MenuAnimation());
```
### 显式绑定节点生命周期
可以使用 `CancelWith(...)` 将协程与一个或多个节点的生命周期关联。
```csharp
using System.Collections.Generic;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions;
using GFramework.Godot.Coroutine;
using Godot;
public partial class MyNode : Node
{ {
public override void _Ready() this.RunCoroutine(LongRunningTask(), Segment.Process, tag: "ui-blink");
{
LongRunningTask()
.CancelWith(this)
.RunCoroutine();
}
private IEnumerator<IYieldInstruction> LongRunningTask()
{
while (true)
{
GD.Print("tick");
yield return new Delay(1.0);
}
}
} }
``` ```
`CancelWith` 目前有三种重载: 这会自动把协程登记为该节点归属协程,并在节点退出场景树时终止它。
- `CancelWith(Node node)` 你仍然可以继续使用 `CancelWith(...)` 包装已有枚举器;它适合把一个协程显式绑定到多个节点生命周期。
- `CancelWith(Node node1, Node node2)`
- `CancelWith(params Node[] nodes)`
`CancelWith(...)` 内部通过 `Timing.IsNodeAlive(...)` 判断节点是否仍然有效。只要任一被监视的节点出现以下任一情况,包装后的协程就会停止继续枚举: ## Segment 与阶段语义
- 节点引用为 `null` Godot 层会把不同 segment 映射为不同的 `CoroutineExecutionStage`
- Godot 实例已经失效或已被释放
- 节点已进入 `queue_free` / `IsQueuedForDeletion()`
- 节点已退出场景树,`IsInsideTree()` 返回 `false`
这意味着协程不只会在节点真正释放时停止;节点一旦退出场景树,下一次推进时也会停止。 - `Segment.Process`
- 对应默认更新阶段
## Segment 分段 - 场景树暂停时不会推进
- `Segment.ProcessIgnorePause`
Godot 层通过 `Segment` 决定协程挂在哪个调度器上: - 同样对应默认更新阶段
- 场景树暂停时仍会推进
```csharp - `Segment.PhysicsProcess`
public enum Segment - 对应固定更新阶段
{ - `WaitForFixedUpdate` 会在这里真实完成
Process, - `Segment.DeferredProcess`
ProcessIgnorePause, - 对应帧结束阶段
PhysicsProcess, - `WaitForEndOfFrame` 会在这里真实完成
DeferredProcess
}
```
- `Process`:普通 `_Process` 段,场景树暂停时不会推进。
- `ProcessIgnorePause`:同样使用 process delta但即使场景树暂停也会推进。
- `PhysicsProcess`:在 `_PhysicsProcess` 段推进。
- `DeferredProcess`:通过 `CallDeferred` 在当前帧之后推进,场景树暂停时不会推进。
示例: 示例:
```csharp ```csharp
UiAnimation().RunCoroutine(Segment.ProcessIgnorePause); this.RunCoroutine(PhysicsRoutine(), Segment.PhysicsProcess);
PhysicsRoutine().RunCoroutine(Segment.PhysicsProcess); this.RunCoroutine(UiAnimation(), Segment.ProcessIgnorePause);
``` ```
如果你更偏向语义化入口,也可以直接使用: ## 时间等待语义
Godot 集成层为每个调度器同时提供了两套时间源:
- 缩放时间
- 来自 `_Process` / `_PhysicsProcess` 的帧增量
- 真实时间
- 来自 Godot 单调时钟,不受时间缩放和暂停影响
因此:
- `Delay` / `WaitForSecondsScaled` 使用宿主帧增量
- `WaitForSecondsRealtime` 使用真实时间
这意味着 UI 或暂停菜单中的协程可以安全使用 `WaitForSecondsRealtime` 保持真实计时。
## 生命周期管理
### 自动归属
```csharp ```csharp
Timing.RunGameCoroutine(GameLoop()); var handle = this.RunCoroutine(LoadAvatar(), tag: "avatar");
Timing.RunUiCoroutine(MenuAnimation());
``` ```
### 延迟调用 ### 手动绑定多个节点
`Timing` 还提供了两个延迟调用快捷方法: ```csharp
LongRunningTask()
.CancelWith(this, panelNode)
.RunCoroutine();
```
### 主动清理
```csharp
Timing.KillCoroutine(handle);
Timing.KillCoroutines(this);
Timing.KillCoroutines("avatar");
Timing.KillAllCoroutines();
```
## 调试与查询
```csharp
if (Timing.TryGetCoroutineSnapshot(handle, out var snapshot))
{
GD.Print(snapshot.ExecutionStage);
GD.Print(snapshot.WaitingInstructionType);
}
var ownedCount = Timing.GetOwnedCoroutineCount(this);
```
实例级计数器:
- `Timing.Instance.ProcessCoroutines`
- `Timing.Instance.ProcessIgnorePauseCoroutines`
- `Timing.Instance.PhysicsCoroutines`
- `Timing.Instance.DeferredCoroutines`
## 延迟调用
```csharp ```csharp
Timing.CallDelayed(1.0, () => GD.Print("1 秒后执行")); Timing.CallDelayed(1.0, () => GD.Print("1 秒后执行"));
Timing.CallDelayed(1.0, () => GD.Print("节点仍然有效时执行"), this); Timing.CallDelayed(1.0, () => GD.Print("节点仍然有效时执行"), this);
``` ```
第二个重载会在执行前检查传入节点是否仍然存活。 第二个重载内部使用节点归属语义,因此节点退树后不会再触发动作。
## 常用等待指令
以下类型可直接用于 `yield return`
### 时间与帧
```csharp
yield return new Delay(1.0);
yield return new WaitForSecondsRealtime(1.0);
yield return new WaitOneFrame();
yield return new WaitForNextFrame();
yield return new WaitForFrames(5);
yield return new WaitForEndOfFrame();
```
说明:
- `Delay` 是最直接的秒级等待。
- `WaitForSecondsRealtime` 常用于需要独立计时语义的协程场景。
- `WaitOneFrame``WaitForNextFrame``WaitForEndOfFrame` 用于帧级调度控制。
### 条件等待
```csharp
yield return new WaitUntil(() => health > 0);
yield return new WaitWhile(() => isLoading);
```
### Task 等待
```csharp
using System.Threading.Tasks;
using GFramework.Core.Coroutine.Extensions;
Task loadTask = LoadSomethingAsync();
yield return loadTask.AsCoroutineInstruction();
```
也可以先把 `Task` 转成协程枚举器,再直接运行:
```csharp
LoadSomethingAsync()
.ToCoroutineEnumerator()
.RunCoroutine();
```
- 已经在一个协程内部时,优先使用 `yield return task.AsCoroutineInstruction()`,这样可以直接把 `Task` 嵌入当前协程流程。
- 如果要把一个现成的 `Task` 当作独立协程入口交给 Godot 协程系统运行,再使用
`task.ToCoroutineEnumerator().RunCoroutine()`
### 等待事件总线事件
可以通过事件总线等待业务事件:
```csharp
using System.Collections.Generic;
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Coroutine.Instructions;
private IEnumerator<IYieldInstruction> WaitForGameEvent(IEventBus eventBus)
{
using var wait = new WaitForEvent<PlayerSpawnedEvent>(eventBus);
yield return wait;
var evt = wait.EventData;
}
```
如需为事件等待附加超时控制,可结合 `WaitForEventWithTimeout<TEvent>`
## 协程控制
协程启动后会返回 `CoroutineHandle`,可用于控制运行状态:
```csharp
var handle = Demo().RunCoroutine(tag: "demo");
Timing.PauseCoroutine(handle);
Timing.ResumeCoroutine(handle);
Timing.KillCoroutine(handle);
Timing.KillCoroutines("demo");
Timing.KillAllCoroutines();
```
如果希望在场景初始化阶段主动确保调度器存在,也可以调用:
```csharp
Timing.Prewarm();
```
## 与 IContextAware 集成 ## 与 IContextAware 集成
`GFramework.Godot.Coroutine` 还提供了一组扩展方法,用于把命令、查询和通知直接包装成协程: Godot 层还提供以下扩展方法,用于把命令、查询和通知直接包装成协程并交给 Timing 调度:
- `RunCommandCoroutine(...)` - `RunCommandCoroutine(...)`
- `RunCommandCoroutine<TResponse>(...)` - `RunCommandCoroutine<TResponse>(...)`
- `RunQueryCoroutine<TResponse>(...)` - `RunQueryCoroutine<TResponse>(...)`
- `RunPublishCoroutine(...)` - `RunPublishCoroutine(...)`
这些方法会把异步操作转换为协程,并交给 `RunCoroutine(...)` 调度执行。 这些 API 仍然可以与 `Segment`、节点归属和标签控制一起使用。
例如:
```csharp
public void StartCoroutines(IContextAware contextAware)
{
contextAware.RunCommandCoroutine(
new EnterBattleCommand(),
Segment.Process,
tag: "battle");
contextAware.RunQueryCoroutine(
new LoadPlayerQuery(),
Segment.ProcessIgnorePause,
tag: "ui");
}
```
这些扩展适合在 Godot 节点或控制器中直接启动和跟踪业务协程。
## 相关文档
- [Godot 概述](./index.md)
- [Godot 扩展方法](./extensions.md)
- [信号扩展](./signal.md)
- [事件系统](../core/events.md)

View File

@ -1,6 +1,6 @@
--- ---
title: 使用协程系统 title: 使用协程系统
description: 学习如何使用协程系统实现异步操作和时间控制 description: 学习如何在 GFramework 中创建调度器、运行协程并结合时间、阶段、Task 与生命周期管理实现常见异步流程。
--- ---
# 使用协程系统 # 使用协程系统
@ -9,590 +9,184 @@ description: 学习如何使用协程系统实现异步操作和时间控制
完成本教程后,你将能够: 完成本教程后,你将能够:
- 理解协程的基本概念和执行机制 - 创建并驱动 `CoroutineScheduler`
- 创建和启动协程 - 编写 `IEnumerator<IYieldInstruction>` 协程
- 使用各种等待指令控制协程执行 - 区分缩放时间、真实时间与阶段等待
- 在架构组件中使用协程 - 使用句柄、取消令牌和快照查询控制协程
- 实现常见的游戏逻辑(延迟执行、循环任务、事件等待) - 在 Godot 中把协程绑定到节点生命周期
## 前置条件
- 已安装 GFramework.Core NuGet 包
- 了解 C# 基础语法和迭代器IEnumerator
- 阅读过[快速开始](/zh-CN/getting-started/quick-start)
- 了解[生命周期管理](/zh-CN/core/lifecycle)
## 步骤 1创建第一个协程 ## 步骤 1创建第一个协程
首先,让我们创建一个简单的协程来理解基本概念。
```csharp ```csharp
using GFramework.Core.Abstractions.Coroutine; using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine; using GFramework.Core.Coroutine;
using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Coroutine.Instructions;
namespace MyGame.Systems public sealed class TutorialLoop
{ {
public class TutorialSystem : AbstractSystem private readonly CoroutineScheduler _scheduler;
public TutorialLoop(ITimeSource timeSource)
{ {
protected override void OnInit() _scheduler = new CoroutineScheduler(timeSource);
{ }
// 启动协程
this.StartCoroutine(MyFirstCoroutine());
}
/// <summary> public void Start()
/// 第一个协程示例 {
/// </summary> _scheduler.Run(MyFirstCoroutine(), tag: "tutorial");
private IEnumerator<IYieldInstruction> MyFirstCoroutine() }
{
Console.WriteLine("协程开始执行");
// 等待 1 秒 public void Tick()
yield return CoroutineHelper.WaitForSeconds(1.0); {
_scheduler.Update();
}
Console.WriteLine("1 秒后执行"); private IEnumerator<IYieldInstruction> MyFirstCoroutine()
{
Console.WriteLine("协程开始");
// 等待 1 帧 yield return new Delay(1.0);
yield return CoroutineHelper.WaitForOneFrame(); Console.WriteLine("1 秒后");
Console.WriteLine("下一帧执行"); yield return new WaitOneFrame();
Console.WriteLine("下一帧");
// 等待 5 帧 yield return new WaitForFrames(3);
yield return CoroutineHelper.WaitForFrames(5); Console.WriteLine("3 帧后");
Console.WriteLine("5 帧后执行");
}
} }
} }
``` ```
**代码说明** 关键点
- 协程方法返回 `IEnumerator<IYieldInstruction>` - 协程返回类型必须是 `IEnumerator<IYieldInstruction>`
- 使用 `yield return` 返回等待指令 - 调度器不会自动运行,你必须在宿主主循环中调用 `Update()`
- `this.StartCoroutine()` 扩展方法启动协程 - `Run(...)` 返回 `CoroutineHandle`,后续控制都依赖这个句柄
- `WaitForSeconds` 等待指定秒数
- `WaitForOneFrame` 等待一帧
- `WaitForFrames` 等待多帧
## 步骤 2实现生命值自动恢复 ## 步骤 2控制协程生命周期
让我们实现一个实用的功能:玩家生命值自动恢复。
```csharp ```csharp
using GFramework.Core.Abstractions.Model; using var cts = new CancellationTokenSource();
using GFramework.Core.Abstractions.Property;
using GFramework.Core.Model;
namespace MyGame.Models var handle = _scheduler.Run(
HealthRegenerationCoroutine(),
tag: "regen",
group: "player",
cancellationToken: cts.Token);
_scheduler.Pause(handle);
_scheduler.Resume(handle);
// 外部取消会在下一次 Update 时生效
cts.Cancel();
var status = await _scheduler.WaitForCompletionAsync(handle);
Console.WriteLine(status);
```
如果你需要观察运行中状态:
```csharp
if (_scheduler.TryGetSnapshot(handle, out var snapshot))
{ {
public class PlayerModel : AbstractModel Console.WriteLine(snapshot.State);
{ Console.WriteLine(snapshot.WaitingInstructionType);
// 当前生命值
public BindableProperty<int> Health { get; } = new(100);
// 最大生命值
public BindableProperty<int> MaxHealth { get; } = new(100);
// 是否启用自动恢复
public BindableProperty<bool> AutoRegenEnabled { get; } = new(true);
private CoroutineHandle? _regenHandle;
protected override void OnInit()
{
// 启动生命值恢复协程
StartHealthRegeneration();
}
/// <summary>
/// 启动生命值恢复
/// </summary>
public void StartHealthRegeneration()
{
// 如果已经在运行,先停止
if (_regenHandle.HasValue)
{
this.StopCoroutine(_regenHandle.Value);
}
// 启动新的恢复协程
_regenHandle = this.StartCoroutine(HealthRegenerationCoroutine());
}
/// <summary>
/// 停止生命值恢复
/// </summary>
public void StopHealthRegeneration()
{
if (_regenHandle.HasValue)
{
this.StopCoroutine(_regenHandle.Value);
_regenHandle = null;
}
}
/// <summary>
/// 生命值恢复协程
/// </summary>
private IEnumerator<IYieldInstruction> HealthRegenerationCoroutine()
{
while (true)
{
// 等待 1 秒
yield return CoroutineHelper.WaitForSeconds(1.0);
// 检查是否启用自动恢复
if (!AutoRegenEnabled.Value)
continue;
// 如果生命值未满,恢复 5 点
if (Health.Value < MaxHealth.Value)
{
Health.Value = Math.Min(Health.Value + 5, MaxHealth.Value);
Console.WriteLine($"生命值恢复: {Health.Value}/{MaxHealth.Value}");
}
}
}
}
} }
``` ```
**代码说明** ## 步骤 3区分时间等待
- 使用 `while (true)` 创建无限循环协程
- 保存协程句柄以便后续控制
- 使用 `StopCoroutine` 停止协程
- 协程中可以访问类成员变量
## 步骤 3实现技能冷却系统
接下来实现一个技能冷却系统,展示如何使用协程管理时间相关的游戏逻辑。
```csharp ```csharp
using GFramework.Core.System; private IEnumerator<IYieldInstruction> CooldownCoroutine()
using System.Collections.Generic;
namespace MyGame.Systems
{ {
public class SkillSystem : AbstractSystem // 使用宿主默认时间
{ yield return new Delay(2.0);
// 技能冷却状态
private readonly Dictionary<string, bool> _skillCooldowns = new();
/// <summary> // 使用真实时间,需要调度器提供 realtimeTimeSource
/// 使用技能 yield return new WaitForSecondsRealtime(2.0);
/// </summary>
public bool UseSkill(string skillName, double cooldownTime)
{
// 检查是否在冷却中
if (_skillCooldowns.TryGetValue(skillName, out var isOnCooldown) && isOnCooldown)
{
Console.WriteLine($"技能 {skillName} 冷却中...");
return false;
}
// 执行技能
Console.WriteLine($"使用技能: {skillName}");
// 启动冷却协程
this.StartCoroutine(SkillCooldownCoroutine(skillName, cooldownTime));
return true;
}
/// <summary>
/// 技能冷却协程
/// </summary>
private IEnumerator<IYieldInstruction> SkillCooldownCoroutine(string skillName, double cooldownTime)
{
// 标记为冷却中
_skillCooldowns[skillName] = true;
Console.WriteLine($"技能 {skillName} 开始冷却 {cooldownTime} 秒");
// 等待冷却时间
yield return CoroutineHelper.WaitForSeconds(cooldownTime);
// 冷却结束
_skillCooldowns[skillName] = false;
Console.WriteLine($"技能 {skillName} 冷却完成");
}
/// <summary>
/// 带进度显示的技能冷却
/// </summary>
private IEnumerator<IYieldInstruction> SkillCooldownWithProgressCoroutine(
string skillName,
double cooldownTime)
{
_skillCooldowns[skillName] = true;
// 使用 WaitForProgress 显示冷却进度
yield return CoroutineHelper.WaitForProgress(
duration: cooldownTime,
onProgress: progress =>
{
Console.WriteLine($"技能 {skillName} 冷却进度: {progress * 100:F0}%");
}
);
_skillCooldowns[skillName] = false;
Console.WriteLine($"技能 {skillName} 冷却完成");
}
}
} }
``` ```
**代码说明** 建议:
- 使用字典管理多个技能的冷却状态 - 普通游戏逻辑优先使用 `Delay`
- 每个技能使用独立的协程管理冷却 - 暂停菜单、真实倒计时、网络超时等场景使用 `WaitForSecondsRealtime`
- `WaitForProgress` 可以在等待期间执行回调
- 协程结束后自动清理冷却状态
## 步骤 4等待事件触发 ## 步骤 4使用阶段等待
实现一个等待玩家完成任务的系统,展示如何在协程中等待事件。 只有宿主为调度器提供了匹配阶段时,阶段等待才会真实生效:
```csharp ```csharp
using GFramework.Core.Abstractions.Events; var fixedScheduler = new CoroutineScheduler(
fixedTimeSource,
executionStage: CoroutineExecutionStage.FixedUpdate);
private IEnumerator<IYieldInstruction> PhysicsCoroutine()
{
yield return new WaitForFixedUpdate();
Console.WriteLine("下一次固定步到达");
}
```
同理,`WaitForEndOfFrame` 需要运行在 `CoroutineExecutionStage.EndOfFrame` 的调度器上。
## 步骤 5等待 Task
```csharp
using GFramework.Core.Coroutine.Extensions;
private IEnumerator<IYieldInstruction> LoadCoroutine()
{
var task = LoadDataAsync();
yield return task.AsCoroutineInstruction();
Console.WriteLine("Task 已完成");
}
```
如果你已经持有调度器,也可以直接把 `Task` 作为顶层协程启动:
```csharp
var handle = _scheduler.StartTaskAsCoroutine(LoadDataAsync());
```
## 步骤 6在 Godot 中绑定 Node 生命周期
```csharp
using GFramework.Core.Abstractions.Coroutine;
using GFramework.Core.Coroutine.Instructions; using GFramework.Core.Coroutine.Instructions;
using GFramework.Godot.Coroutine;
using Godot;
namespace MyGame.Systems public partial class DemoNode : Node
{ {
// 任务完成事件 public override void _Ready()
public record QuestCompletedEvent(int QuestId, string QuestName) : IEvent;
public class QuestSystem : AbstractSystem
{ {
/// <summary> // 推荐:节点作为所有者运行协程
/// 开始任务并等待完成 this.RunCoroutine(BlinkCoroutine(), Segment.ProcessIgnorePause, tag: "blink");
/// </summary> }
public void StartQuest(int questId, string questName)
private IEnumerator<IYieldInstruction> BlinkCoroutine()
{
while (true)
{ {
this.StartCoroutine(QuestCoroutine(questId, questName)); Visible = !Visible;
} yield return new WaitForSecondsRealtime(0.5);
/// <summary>
/// 任务协程
/// </summary>
private IEnumerator<IYieldInstruction> QuestCoroutine(int questId, string questName)
{
Console.WriteLine($"任务开始: {questName}");
// 获取事件总线
var eventBus = this.GetService<IEventBus>();
// 等待任务完成事件
var waitEvent = new WaitForEvent<QuestCompletedEvent>(
eventBus,
evt => evt.QuestId == questId // 过滤条件
);
yield return waitEvent;
// 获取事件数据
var completedEvent = waitEvent.EventData;
Console.WriteLine($"任务完成: {completedEvent.QuestName}");
// 发放奖励
GiveReward(questId);
}
/// <summary>
/// 带超时的任务
/// </summary>
private IEnumerator<IYieldInstruction> TimedQuestCoroutine(
int questId,
string questName,
double timeLimit)
{
Console.WriteLine($"限时任务开始: {questName} (时限: {timeLimit}秒)");
var eventBus = this.GetService<IEventBus>();
// 等待事件,带超时
var waitEvent = new WaitForEventWithTimeout<QuestCompletedEvent>(
eventBus,
timeout: timeLimit,
predicate: evt => evt.QuestId == questId
);
yield return waitEvent;
if (waitEvent.IsTimeout)
{
Console.WriteLine($"任务超时失败: {questName}");
}
else
{
Console.WriteLine($"任务完成: {questName}");
GiveReward(questId);
}
}
private void GiveReward(int questId)
{
Console.WriteLine($"发放任务 {questId} 的奖励");
} }
} }
} }
``` ```
**代码说明** `DemoNode` 退出场景树时,上面的协程会被自动终止。
- `WaitForEvent` 等待特定事件触发 如果你需要绑定多个节点,可以继续使用:
- 可以使用 `predicate` 参数过滤事件
- `WaitForEventWithTimeout` 支持超时机制
- 通过 `EventData` 属性获取事件数据
## 步骤 5协程组合与嵌套
实现一个复杂的游戏流程,展示如何组合多个协程。
```csharp ```csharp
namespace MyGame.Systems BlinkCoroutine()
{ .CancelWith(this, anotherNode)
public class GameFlowSystem : AbstractSystem .RunCoroutine();
{
/// <summary>
/// 游戏开始流程
/// </summary>
public void StartGame()
{
this.StartCoroutine(GameStartSequence());
}
/// <summary>
/// 游戏开始序列
/// </summary>
private IEnumerator<IYieldInstruction> GameStartSequence()
{
Console.WriteLine("=== 游戏开始 ===");
// 1. 显示标题
yield return ShowTitle();
// 2. 加载资源
yield return LoadResources();
// 3. 初始化玩家
yield return InitializePlayer();
// 4. 播放开场动画
yield return PlayOpeningAnimation();
Console.WriteLine("=== 游戏准备完成 ===");
}
/// <summary>
/// 显示标题
/// </summary>
private IEnumerator<IYieldInstruction> ShowTitle()
{
Console.WriteLine("显示游戏标题...");
yield return CoroutineHelper.WaitForSeconds(2.0);
Console.WriteLine("标题显示完成");
}
/// <summary>
/// 加载资源
/// </summary>
private IEnumerator<IYieldInstruction> LoadResources()
{
Console.WriteLine("开始加载资源...");
// 并行加载多个资源
var loadTextures = LoadTexturesCoroutine();
var loadAudio = LoadAudioCoroutine();
var loadModels = LoadModelsCoroutine();
// 等待所有资源加载完成
yield return new WaitForAllCoroutines(
this.GetCoroutineScheduler(),
loadTextures,
loadAudio,
loadModels
);
Console.WriteLine("所有资源加载完成");
}
private IEnumerator<IYieldInstruction> LoadTexturesCoroutine()
{
Console.WriteLine(" 加载纹理...");
yield return CoroutineHelper.WaitForSeconds(1.0);
Console.WriteLine(" 纹理加载完成");
}
private IEnumerator<IYieldInstruction> LoadAudioCoroutine()
{
Console.WriteLine(" 加载音频...");
yield return CoroutineHelper.WaitForSeconds(1.5);
Console.WriteLine(" 音频加载完成");
}
private IEnumerator<IYieldInstruction> LoadModelsCoroutine()
{
Console.WriteLine(" 加载模型...");
yield return CoroutineHelper.WaitForSeconds(0.8);
Console.WriteLine(" 模型加载完成");
}
private IEnumerator<IYieldInstruction> InitializePlayer()
{
Console.WriteLine("初始化玩家...");
yield return CoroutineHelper.WaitForSeconds(0.5);
Console.WriteLine("玩家初始化完成");
}
private IEnumerator<IYieldInstruction> PlayOpeningAnimation()
{
Console.WriteLine("播放开场动画...");
yield return CoroutineHelper.WaitForSeconds(3.0);
Console.WriteLine("开场动画播放完成");
}
/// <summary>
/// 获取协程调度器
/// </summary>
private CoroutineScheduler GetCoroutineScheduler()
{
// 从架构服务中获取
return this.GetService<CoroutineScheduler>();
}
}
}
``` ```
**代码说明**
- 使用 `yield return` 调用其他协程实现嵌套
- `WaitForAllCoroutines` 并行执行多个协程
- 协程可以像函数一样组合和复用
- 清晰的流程控制,避免回调嵌套
## 完整代码
### GameArchitecture.cs
```csharp
using GFramework.Core.Architecture;
namespace MyGame
{
public class GameArchitecture : Architecture
{
public static IArchitecture Interface { get; private set; }
protected override void Init()
{
Interface = this;
// 注册 Model
RegisterModel(new PlayerModel());
// 注册 System
RegisterSystem(new TutorialSystem());
RegisterSystem(new SkillSystem());
RegisterSystem(new QuestSystem());
RegisterSystem(new GameFlowSystem());
}
}
}
```
### 测试代码
```csharp
using MyGame;
using MyGame.Systems;
// 初始化架构
var architecture = new GameArchitecture();
architecture.Initialize();
await architecture.WaitUntilReadyAsync();
// 测试技能系统
var skillSystem = architecture.GetSystem<SkillSystem>();
skillSystem.UseSkill("火球术", 3.0);
await Task.Delay(1000);
skillSystem.UseSkill("火球术", 3.0); // 冷却中
await Task.Delay(3000);
skillSystem.UseSkill("火球术", 3.0); // 冷却完成
// 测试任务系统
var questSystem = architecture.GetSystem<QuestSystem>();
questSystem.StartQuest(1, "击败史莱姆");
// 模拟任务完成
await Task.Delay(2000);
var eventBus = architecture.GetService<IEventBus>();
eventBus.Publish(new QuestCompletedEvent(1, "击败史莱姆"));
// 测试游戏流程
var gameFlowSystem = architecture.GetSystem<GameFlowSystem>();
gameFlowSystem.StartGame();
```
## 运行结果
运行程序后,你将看到类似以下的输出:
```
协程开始执行
1 秒后执行
下一帧执行
5 帧后执行
使用技能: 火球术
技能 火球术 开始冷却 3.0 秒
技能 火球术 冷却中...
技能 火球术 冷却完成
使用技能: 火球术
任务开始: 击败史莱姆
任务完成: 击败史莱姆
发放任务 1 的奖励
=== 游戏开始 ===
显示游戏标题...
标题显示完成
开始加载资源...
加载纹理...
加载音频...
加载模型...
模型加载完成
纹理加载完成
音频加载完成
所有资源加载完成
初始化玩家...
玩家初始化完成
播放开场动画...
开场动画播放完成
=== 游戏准备完成 ===
```
**验证步骤**
1. 协程按预期顺序执行
2. 技能冷却系统正常工作
3. 事件等待功能正确
4. 并行加载资源成功
## 下一步 ## 下一步
恭喜!你已经掌握了协程系统的基本用法。接下来可以学习: - Core 侧更完整的 API 说明见 [Core 协程系统](/zh-CN/core/coroutine)
- Godot 集成细节见 [Godot 协程系统](/zh-CN/godot/coroutine)
- [实现状态机](/zh-CN/tutorials/state-machine-tutorial) - 使用协程实现状态转换
- [资源管理最佳实践](/zh-CN/tutorials/resource-management) - 在协程中加载资源
- [使用事件系统](/zh-CN/core/events) - 协程与事件系统集成
## 相关文档
- [协程系统](/zh-CN/core/coroutine) - 协程系统详细说明
- [事件系统](/zh-CN/core/events) - 事件系统详解
- [生命周期管理](/zh-CN/core/lifecycle) - 组件生命周期
- [System 层](/zh-CN/core/system) - System 详细说明