スケジューラー

ワンショットの遅延実行、繰り返しタイマー、次フレームのコールバック、そしてバックグラウンドスレッドからメインスレッドへの安全な橋渡し。

コードを後で実行します — 遅延後に、一定間隔で、次のフレームで、あるいはメインスレッドに戻して。

インターフェースIScheduler
オフスイッチNullScheduler
アセンブリCommonGameSystem.Core
起動ブート時に自動登録 — セットアップ不要

できること

Scheduler はアクションを後から実行します。遅延後に一度だけ(After)、一定間隔で繰り返し(Every)、次のフレームで(NextFrame)、あるいはバックグラウンドスレッドからポストされたものをメインスレッドで(Post / RunOnMain)実行します。

すべてのタイマーはクロックTime サービスが所有する名前付きの時間ストリーム — にバインドされます。クロックは 3 つあります。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)遅延後に一度だけ、指定したクロック上で発火します。

繰り返しタイマー

タイマーを止めるには、返されたトークンを必ず Dispose してください。

メンバー補足
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 を使ってください。クロックの完全な契約は Time サービスを参照してください。
  • Every は Dispose が必須。 返されたトークンを捨てると、タイマーは永遠に動き続けます。フィールドに保持し、終わったら Dispose() を呼んでください — 通常は OnDestroy() で行います。
  • スレッドセーフな入口は Post だけ。 メインスレッド外で使えるのは Post() と、バックグラウンドスレッドから呼ばれた場合の RunOnMain() だけです。それ以外のメソッドは、ワーカースレッドから呼ばれると InvalidOperationException をスローします。
  • コールバックはフレーム上で実行される。 あるコールバックの例外はログに記録されて封じ込められ、他のタイマーは発火し続けます。ただし遅いコールバックは、他のメインスレッド処理と同様にフレームを遅らせます。
  • NullSchedulerPost も含めてすべてを無効化する。 バックグラウンドからメインへのディスパッチに依存している場合、スケジューラーを NullScheduler に差し替えると、それらの結果は静かに破棄されます。

関連ページ

  • Time — クロック、ポーズ、タイムスケール
  • Tween — 同じクロック上での値アニメーション