调度器

一次性延迟、循环计时器、下一帧回调,以及从后台线程安全回到主线程的桥梁。

让代码稍后运行——延迟之后、按固定间隔、下一帧,或回到主线程。

接口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>();
    }
}

SLServiceLocator 的简短别名: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());

这会让所有调度静默。AfterEveryNextFrame 不注册任何东西,返回一个什么都不做的令牌。PostRunOnMain 直接丢弃操作。PendingCount 为 0。适用于压力测试或临时禁用调度逻辑。第一次调用会记录一条警告,因此误换实现仍然可见。

常见陷阱

  • 选对时钟。 时钟选错不会报错——只是暂停行为不对。冷却和 AI 用 Clock.Gameplay(默认值)。暂停期间必须继续运行的菜单和 HUD 动画用 Clock.UI。始终要运行的工作(如音乐淡出)用 Clock.Background。完整的时钟契约见时间服务
  • Every 必须释放。 如果丢弃返回的令牌,计时器会永远运行。把它存到字段里,用完时调用 Dispose()——通常在 OnDestroy() 中。
  • Post 是唯一线程安全的入口点。 只有 Post(),以及从后台线程调用的 RunOnMain(),可以在主线程之外使用。其他任何方法从工作线程调用都会抛出 InvalidOperationException
  • 回调在帧内运行。 某个回调抛出的异常会被记录并隔离——其他计时器照常触发。但缓慢的回调会像任何主线程工作一样拖慢这一帧。
  • NullScheduler 会静默一切,包括 Post 如果你依赖后台到主线程的分发,把调度器换成 NullScheduler 会静默丢弃那些结果。

相关页面

  • 时间 — 时钟、暂停与时间缩放
  • 补间 — 同一组时钟上的数值动画