feat(coroutine): 实现协程系统核心功能

- 添加协程上下文、句柄、调度器和作用域管理类
- 实现协程等待指令包括 WaitForSeconds、WaitUntil 和 WaitWhile
- 创建协程系统和全局协程作用域管理器
- 定义协程相关抽象接口 ICoroutineScheduler、ICoroutineScope 等
- 升级 Meziantou.Analyzer 依赖版本至 2.0.283
- 升级 Meziantou.Polyfill 依赖版本至 1.0.100
This commit is contained in:
GeWuYou 2026-01-20 23:05:15 +08:00
parent 5ef6145688
commit f143cf5c1b
19 changed files with 515 additions and 10 deletions

View File

@ -16,11 +16,11 @@
<Using Include="GFramework.Core.Abstractions"/>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Meziantou.Analyzer" Version="2.0.279">
<PackageReference Update="Meziantou.Analyzer" Version="2.0.283">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Meziantou.Polyfill" Version="1.0.84">
<PackageReference Update="Meziantou.Polyfill" Version="1.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -18,11 +18,11 @@
<Using Include="GFramework.Game.Abstractions"/>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Meziantou.Analyzer" Version="2.0.279">
<PackageReference Update="Meziantou.Analyzer" Version="2.0.283">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Meziantou.Polyfill" Version="1.0.84">
<PackageReference Update="Meziantou.Polyfill" Version="1.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -0,0 +1,13 @@
namespace GFramework.Game.Abstractions.coroutine;
/// <summary>
/// 协程调度器接口,定义了协程系统的基本调度方法
/// </summary>
public interface ICoroutineScheduler
{
/// <summary>
/// 更新协程调度器,处理等待中的协程
/// </summary>
/// <param name="deltaTime">自上一帧以来的时间间隔(以秒为单位)</param>
void Update(float deltaTime);
}

View File

@ -0,0 +1,17 @@
namespace GFramework.Game.Abstractions.coroutine;
/// <summary>
/// 协程作用域接口,用于管理协程的生命周期
/// </summary>
public interface ICoroutineScope
{
/// <summary>
/// 获取协程是否处于活动状态
/// </summary>
bool IsActive { get; }
/// <summary>
/// 取消协程执行
/// </summary>
void Cancel();
}

View File

@ -0,0 +1,15 @@
using GFramework.Core.Abstractions.system;
namespace GFramework.Game.Abstractions.coroutine;
/// <summary>
/// 协程系统接口继承自ISystem用于管理游戏中的协程执行
/// </summary>
public interface ICoroutineSystem : ISystem
{
/// <summary>
/// 更新协程系统,在每一帧调用以处理协程逻辑
/// </summary>
/// <param name="deltaTime">距离上一帧的时间间隔(秒)</param>
void OnUpdate(float deltaTime);
}

View File

@ -0,0 +1,18 @@
namespace GFramework.Game.Abstractions.coroutine;
/// <summary>
/// 表示一个可等待的指令接口,用于协程中的等待操作
/// </summary>
public interface IYieldInstruction
{
/// <summary>
/// 获取当前等待指令是否已完成
/// </summary>
bool IsDone { get; }
/// <summary>
/// 更新等待指令的状态
/// </summary>
/// <param name="deltaTime">自上次更新以来的时间间隔(以秒为单位)</param>
void Update(float deltaTime);
}

View File

@ -0,0 +1,27 @@
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
/// <summary>
/// 协程上下文类,用于封装协程执行所需的环境信息
/// </summary>
/// <param name="scope">协程作用域接口实例</param>
/// <param name="scheduler">协程调度器实例</param>
/// <param name="owner">协程的所有者对象默认为null</param>
public class CoroutineContext(ICoroutineScope scope, CoroutineScheduler scheduler, object? owner = null)
{
/// <summary>
/// 获取协程作用域
/// </summary>
public ICoroutineScope Scope { get; } = scope;
/// <summary>
/// 获取协程调度器
/// </summary>
public CoroutineScheduler Scheduler { get; } = scheduler;
/// <summary>
/// 获取协程所有者对象
/// </summary>
public object? Owner { get; } = owner;
}

View File

@ -0,0 +1,118 @@
using System.Collections;
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
public class CoroutineHandle : IYieldInstruction
{
private readonly Stack<IEnumerator> _stack = new();
private IYieldInstruction? _waitingInstruction;
internal CoroutineHandle(IEnumerator routine, CoroutineContext context, IYieldInstruction? waitingInstruction)
{
_stack.Push(routine);
Context = context;
_waitingInstruction = waitingInstruction;
}
public CoroutineContext Context { get; }
public bool IsCancelled { get; private set; }
public bool IsDone { get; private set; }
void IYieldInstruction.Update(float deltaTime)
{
InternalUpdate(deltaTime);
}
public event Action? OnComplete;
public event Action<Exception>? OnError;
private bool InternalUpdate(float deltaTime)
{
if (IsDone) return false;
if (_waitingInstruction != null)
{
_waitingInstruction.Update(deltaTime);
if (!_waitingInstruction.IsDone) return true;
_waitingInstruction = null;
}
if (_stack.Count == 0)
{
Complete();
return false;
}
try
{
var current = _stack.Peek();
if (current.MoveNext())
{
ProcessYieldValue(current.Current);
return true;
}
_stack.Pop();
return _stack.Count > 0 || !CompleteCheck();
}
catch (Exception ex)
{
HandleError(ex);
return false;
}
}
private void ProcessYieldValue(object yielded)
{
switch (yielded)
{
case null:
break;
case IEnumerator nested:
_stack.Push(nested);
break;
// ✅ 将更具体的类型放在前面
case CoroutineHandle otherHandle:
_waitingInstruction = otherHandle;
break;
case IYieldInstruction instruction:
_waitingInstruction = instruction;
break;
default:
throw new InvalidOperationException($"Unsupported yield type: {yielded.GetType()}");
}
}
private bool CompleteCheck()
{
if (_stack.Count == 0) Complete();
return IsDone;
}
private void Complete()
{
if (IsDone) return;
IsDone = true;
_stack.Clear();
_waitingInstruction = null;
OnComplete?.Invoke();
}
private void HandleError(Exception ex)
{
IsDone = true;
_stack.Clear();
_waitingInstruction = null;
OnError?.Invoke(ex);
}
public void Cancel()
{
if (IsDone) return;
IsDone = true;
IsCancelled = true;
_stack.Clear();
_waitingInstruction = null;
}
}

View File

@ -0,0 +1,52 @@
using System.Collections;
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
public class CoroutineScheduler : ICoroutineScheduler
{
private readonly List<CoroutineHandle> _active = new();
private readonly List<CoroutineHandle> _toAdd = new();
private readonly HashSet<CoroutineHandle> _toRemove = new();
public int ActiveCount => _active.Count;
public void Update(float deltaTime)
{
if (_toAdd.Count > 0)
{
_active.AddRange(_toAdd);
_toAdd.Clear();
}
for (var i = _active.Count - 1; i >= 0; i--)
{
var c = _active[i];
if (!c.Context.Scope.IsActive)
{
c.Cancel();
_toRemove.Add(c);
continue;
}
if (!c.Update(deltaTime))
_toRemove.Add(c);
}
if (_toRemove.Count <= 0) return;
{
_active.RemoveAll(c => _toRemove.Contains(c));
_toRemove.Clear();
}
}
internal CoroutineHandle StartCoroutine(IEnumerator routine, CoroutineContext context)
{
var handle = new CoroutineHandle(routine, context);
_toAdd.Add(handle);
return handle;
}
internal void RemoveCoroutine(CoroutineHandle handle) => _toRemove.Add(handle);
}

View File

@ -0,0 +1,57 @@
using System.Collections;
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
public class CoroutineScope : ICoroutineScope, IDisposable
{
private readonly List<CoroutineScope> _children = new();
private readonly CoroutineScope _parent;
private readonly HashSet<CoroutineHandle> _runningCoroutines = new();
private readonly CoroutineScheduler _scheduler;
private bool _isActive = true;
public CoroutineScope(CoroutineScheduler scheduler, string name = null, CoroutineScope parent = null)
{
_scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler));
_parent = parent;
_parent?._children.Add(this);
Name = name ?? $"Scope_{GetHashCode()}";
}
public string Name { get; }
public bool IsActive => _isActive;
public void Cancel()
{
if (!_isActive) return;
_isActive = false;
foreach (var child in _children.ToList())
child.Cancel();
foreach (var handle in _runningCoroutines.ToList())
handle.Cancel();
_runningCoroutines.Clear();
}
public void Dispose() => Cancel();
public CoroutineHandle Launch(IEnumerator routine)
{
if (!_isActive)
throw new InvalidOperationException($"Scope '{Name}' is not active");
var context = new CoroutineContext(this, _scheduler, this);
var handle = _scheduler.StartCoroutine(routine, context);
_runningCoroutines.Add(handle);
handle.OnComplete += () => _runningCoroutines.Remove(handle);
handle.OnError += (ex) => _runningCoroutines.Remove(handle);
return handle;
}
}

View File

@ -0,0 +1,57 @@
using System.Collections;
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
/// <summary>
/// 为协程作用域提供扩展方法,支持延迟执行和重复执行功能
/// </summary>
public static class CoroutineScopeExtensions
{
/// <summary>
/// 启动一个延迟执行的协程
/// </summary>
/// <param name="scope">协程作用域</param>
/// <param name="delay">延迟时间(秒)</param>
/// <param name="action">延迟后要执行的动作</param>
/// <returns>协程句柄,可用于控制协程的生命周期</returns>
public static CoroutineHandle LaunchDelayed(this ICoroutineScope scope, float delay, Action action)
=> ((CoroutineScope)scope).Launch(DelayedRoutine(delay, action));
/// <summary>
/// 创建延迟执行的协程迭代器
/// </summary>
/// <param name="delay">延迟时间(秒)</param>
/// <param name="action">要执行的动作</param>
/// <returns>协程迭代器</returns>
private static IEnumerator DelayedRoutine(float delay, Action? action)
{
yield return new WaitForSeconds(delay);
action?.Invoke();
}
/// <summary>
/// 启动一个重复执行的协程
/// </summary>
/// <param name="scope">协程作用域</param>
/// <param name="interval">重复间隔时间(秒)</param>
/// <param name="action">每次重复时要执行的动作</param>
/// <returns>协程句柄,可用于控制协程的生命周期</returns>
public static CoroutineHandle LaunchRepeating(this ICoroutineScope scope, float interval, Action action)
=> ((CoroutineScope)scope).Launch(RepeatingRoutine(interval, action));
/// <summary>
/// 创建重复执行的协程迭代器
/// </summary>
/// <param name="interval">重复间隔时间(秒)</param>
/// <param name="action">要执行的动作</param>
/// <returns>协程迭代器</returns>
private static IEnumerator RepeatingRoutine(float interval, Action action)
{
while (true)
{
action?.Invoke();
yield return new WaitForSeconds(interval);
}
}
}

View File

@ -0,0 +1,28 @@
using GFramework.Core.system;
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
/// <summary>
/// 协程系统类,负责管理和更新协程调度器
/// </summary>
/// <param name="scheduler">协程调度器实例</param>
public class CoroutineSystem(CoroutineScheduler scheduler) : AbstractSystem, ICoroutineSystem
{
/// <summary>
/// 更新协程系统,驱动协程调度器执行协程逻辑
/// </summary>
/// <param name="deltaTime">时间间隔,表示自上一帧以来经过的时间(秒)</param>
public void OnUpdate(float deltaTime)
{
// 更新协程调度器,处理等待中的协程
scheduler.Update(deltaTime);
}
/// <summary>
/// 初始化协程系统
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -0,0 +1,31 @@
using System.Collections;
namespace GFramework.Game.coroutine;
/// <summary>
/// 全局协程作用域管理器,提供全局唯一的协程执行环境
/// </summary>
public static class GlobalCoroutineScope
{
private static CoroutineScope? _instance;
/// <summary>
/// 获取全局协程作用域实例,如果未初始化则抛出异常
/// </summary>
private static CoroutineScope Instance =>
_instance ?? throw new InvalidOperationException("GlobalScope not initialized");
/// <summary>
/// 初始化全局协程作用域
/// </summary>
/// <param name="scheduler">协程调度器实例</param>
public static void Initialize(CoroutineScheduler scheduler) =>
_instance = new CoroutineScope(scheduler, "GlobalScope");
/// <summary>
/// 在全局作用域中启动一个协程
/// </summary>
/// <param name="routine">要执行的协程枚举器</param>
/// <returns>协程句柄,用于控制和管理协程生命周期</returns>
public static CoroutineHandle Launch(IEnumerator routine) => Instance.Launch(routine);
}

View File

@ -0,0 +1,24 @@
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
/// <summary>
/// 表示一个等待指定秒数的时间延迟指令
/// </summary>
/// <param name="seconds">需要等待的秒数</param>
public class WaitForSeconds(float seconds) : IYieldInstruction
{
private float _elapsed;
public bool IsDone { get; private set; }
/// <summary>
/// 更新时间进度,当累计时间达到指定秒数时标记完成
/// </summary>
/// <param name="deltaTime">自上次更新以来经过的时间(秒)</param>
public void Update(float deltaTime)
{
if (IsDone) return;
_elapsed += deltaTime;
if (_elapsed >= seconds) IsDone = true;
}
}

View File

@ -0,0 +1,24 @@
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
/// <summary>
/// 等待直到指定条件满足的协程等待指令
/// </summary>
/// <param name="predicate">用于判断等待条件是否满足的布尔函数委托</param>
public class WaitUntil(Func<bool> predicate) : IYieldInstruction
{
/// <summary>
/// 获取等待指令是否已完成
/// </summary>
public bool IsDone { get; private set; }
/// <summary>
/// 更新等待状态,在每一帧调用以检查条件是否满足
/// </summary>
/// <param name="deltaTime">自上一帧以来的时间间隔</param>
public void Update(float deltaTime)
{
if (!IsDone) IsDone = predicate();
}
}

View File

@ -0,0 +1,24 @@
using GFramework.Game.Abstractions.coroutine;
namespace GFramework.Game.coroutine;
/// <summary>
/// 等待条件为假的等待指令当指定的谓词条件变为false时完成等待
/// </summary>
/// <param name="predicate">用于判断是否继续等待的条件函数返回true表示继续等待返回false表示等待结束</param>
public class WaitWhile(Func<bool> predicate) : IYieldInstruction
{
/// <summary>
/// 获取等待指令是否已完成
/// </summary>
public bool IsDone { get; private set; }
/// <summary>
/// 更新等待状态,检查谓词条件是否满足结束等待的要求
/// </summary>
/// <param name="deltaTime">自上次更新以来的时间间隔</param>
public void Update(float deltaTime)
{
if (!IsDone) IsDone = !predicate();
}
}

View File

@ -19,11 +19,11 @@
<Folder Include="logging\"/>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Meziantou.Analyzer" Version="2.0.279">
<PackageReference Update="Meziantou.Analyzer" Version="2.0.283">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Meziantou.Polyfill" Version="1.0.84">
<PackageReference Update="Meziantou.Polyfill" Version="1.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -18,11 +18,11 @@
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Meziantou.Analyzer" Version="2.0.279">
<PackageReference Update="Meziantou.Analyzer" Version="2.0.283">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Meziantou.Polyfill" Version="1.0.84">
<PackageReference Update="Meziantou.Polyfill" Version="1.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

View File

@ -22,11 +22,11 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" PrivateAssets="all"/>
<PackageReference Update="Meziantou.Analyzer" Version="2.0.279">
<PackageReference Update="Meziantou.Analyzer" Version="2.0.283">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Update="Meziantou.Polyfill" Version="1.0.84">
<PackageReference Update="Meziantou.Polyfill" Version="1.0.100">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>