调度器
一次性延迟、循环计时器、下一帧回调,以及从后台线程安全回到主线程的桥梁。
让代码稍后运行——延迟之后、按固定间隔、下一帧,或回到主线程。
| 接口 | IScheduler |
| 关闭开关 | NullScheduler |
| 程序集 | CommonGameSystem.Core |
| 启动 | 启动时自动注册——无需任何设置 |
功能概览
调度器让你的操作稍后运行:延迟后执行一次(After)、按固定间隔重复执行(Every)、在下一帧执行(NextFrame),或把后台线程投递的操作放到主线程执行(Post / RunOnMain)。
每个计时器都绑定到一个时钟(clock)——由时间服务持有的具名时间流。共有三个:Gameplay 随游戏暂停而暂停、随慢动作而变慢;UI 在暂停菜单期间继续运行;Background 始终运行。绑定在 Gameplay 时钟上的冷却计时器会在菜单打开时自动冻结——你完全不用编写暂停逻辑。
Post 是框架计时栈中唯一线程安全的入口点,因此后台任务(文件加载、网络调用)可以把结果交回主线程,而不会在线程外触碰 Unity API。
快速上手
在 Awake() 或 Start() 中解析一次并缓存引用:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // Run after the framework has started.
public class MyTimerUser : MonoBehaviour
{
private IScheduler _scheduler;
private void Awake()
{
_scheduler = ServiceLocator.Resolve<IScheduler>();
}
}
SL 是 ServiceLocator 的简短别名:var scheduler = SL.Resolve<IScheduler>();
API 参考
一次性延迟
每次调用都会返回一个令牌;在计时器触发前释放(dispose)令牌即可取消它。
| 成员 | 说明 |
|---|---|
IDisposable After(float delaySeconds, Action callback) | 延迟后触发一次,运行在 Gameplay 时钟上(随游戏一起暂停)。 |
IDisposable After(float delaySeconds, Clock clock, Action callback) | 延迟后触发一次,运行在你指定的时钟上。 |
循环计时器
你必须释放返回的令牌才能停止计时器。
| 成员 | 说明 |
|---|---|
IDisposable Every(float intervalSeconds, Action callback) | 在 Gameplay 时钟上按固定间隔重复。 |
IDisposable Every(float intervalSeconds, Clock clock, Action callback) | 在你指定的时钟上按固定间隔重复。 |
下一帧
基于帧而非时间——即使游戏暂停也会运行。
| 成员 | 说明 |
|---|---|
IDisposable NextFrame(Action callback) | 在下一帧运行回调。 |
后台线程到主线程(线程安全)
| 成员 | 说明 |
|---|---|
void Post(Action action) | 从任意线程排队该操作。排队的操作在下一次主线程 tick 时按投递顺序运行。 |
void RunOnMain(Action action) | 在主线程上时立即运行该操作。在后台线程上时像 Post 一样排队。可以安全地在另一个已调度回调的内部调用。 |
诊断
| 成员 | 说明 |
|---|---|
int PendingCount { get; } | 存活计时器加排队投递的近似计数。仅用于性能分析。 |
示例
using System;
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class CooldownManager : MonoBehaviour
{
[SerializeField] private GameObject enemyPrefab; // assign in the Inspector
private IScheduler _scheduler;
private IDisposable _aiLoopToken;
private void Awake()
{
_scheduler = ServiceLocator.Resolve<IScheduler>();
}
// One-shot: "in 2 seconds, spawn an enemy".
public void SpawnEnemyIn2Seconds()
{
_scheduler.After(2f, () => Instantiate(enemyPrefab));
}
// Repeating: "tick the AI every 0.5 seconds; pause when the game pauses".
public void StartAiLoop()
{
_aiLoopToken = _scheduler.Every(0.5f, Clock.Gameplay, TickAi);
}
private void TickAi()
{
// Runs every 0.5 game-seconds. Pauses automatically while menus are open.
}
// Next frame: "refresh the UI on the next frame".
public void RefreshUiNextFrame()
{
_scheduler.NextFrame(RefreshUi);
}
private void RefreshUi()
{
// Update health bars, labels, and so on.
}
// Background thread to main thread: "loading finished, update the UI safely".
public void StartAsyncLoad()
{
System.Threading.Tasks.Task.Run(() =>
{
string data = LoadDataExpensively();
_scheduler.Post(() => OnDataLoaded(data)); // safe from any thread
});
}
private string LoadDataExpensively() => "loaded"; // your real loading work here
private void OnDataLoaded(string data)
{
// Update the UI with the loaded data.
}
// Stop the repeating timer when this object goes away.
private void OnDestroy()
{
_aiLoopToken?.Dispose();
}
}
关闭该服务
ServiceLocator.Replace<IScheduler>(new NullScheduler());
这会让所有调度静默。After、Every 和 NextFrame 不注册任何东西,返回一个什么都不做的令牌。Post 和 RunOnMain 直接丢弃操作。PendingCount 为 0。适用于压力测试或临时禁用调度逻辑。第一次调用会记录一条警告,因此误换实现仍然可见。
常见陷阱
- 选对时钟。 时钟选错不会报错——只是暂停行为不对。冷却和 AI 用
Clock.Gameplay(默认值)。暂停期间必须继续运行的菜单和 HUD 动画用Clock.UI。始终要运行的工作(如音乐淡出)用Clock.Background。完整的时钟契约见时间服务。 Every必须释放。 如果丢弃返回的令牌,计时器会永远运行。把它存到字段里,用完时调用Dispose()——通常在OnDestroy()中。Post是唯一线程安全的入口点。 只有Post(),以及从后台线程调用的RunOnMain(),可以在主线程之外使用。其他任何方法从工作线程调用都会抛出InvalidOperationException。- 回调在帧内运行。 某个回调抛出的异常会被记录并隔离——其他计时器照常触发。但缓慢的回调会像任何主线程工作一样拖慢这一帧。
NullScheduler会静默一切,包括Post。 如果你依赖后台到主线程的分发,把调度器换成NullScheduler会静默丢弃那些结果。