スケジューラー
ワンショットの遅延実行、繰り返しタイマー、次フレームのコールバック、そしてバックグラウンドスレッドからメインスレッドへの安全な橋渡し。
コードを後で実行します — 遅延後に、一定間隔で、次のフレームで、あるいはメインスレッドに戻して。
| インターフェース | 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>();
}
}
SL は ServiceLocator の短いエイリアスです: 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());
これですべてのスケジューリングが無効化されます。After、Every、NextFrame は何も登録せず、何もしないトークンを返します。Post と RunOnMain はアクションを破棄します。PendingCount は 0 です。ストレステストや、スケジュール済みロジックの一時的な無効化に便利です。最初の呼び出しで警告がログに出るため、誤った差し替えにも気づけます。
よくある落とし穴
- 正しいクロックを選ぶ。 間違ったクロックは静かに失敗します — エラーは出ず、ポーズ時の挙動だけが間違います。クールダウンや AI には
Clock.Gameplay(デフォルト)を、ポーズ中も動き続けるべきメニューや HUD のアニメーションにはClock.UIを、音楽のフェードのような常時実行の処理にはClock.Backgroundを使ってください。クロックの完全な契約は Time サービスを参照してください。 Everyは Dispose が必須。 返されたトークンを捨てると、タイマーは永遠に動き続けます。フィールドに保持し、終わったらDispose()を呼んでください — 通常はOnDestroy()で行います。- スレッドセーフな入口は
Postだけ。 メインスレッド外で使えるのはPost()と、バックグラウンドスレッドから呼ばれた場合のRunOnMain()だけです。それ以外のメソッドは、ワーカースレッドから呼ばれるとInvalidOperationExceptionをスローします。 - コールバックはフレーム上で実行される。 あるコールバックの例外はログに記録されて封じ込められ、他のタイマーは発火し続けます。ただし遅いコールバックは、他のメインスレッド処理と同様にフレームを遅らせます。
NullSchedulerはPostも含めてすべてを無効化する。 バックグラウンドからメインへのディスパッチに依存している場合、スケジューラーをNullSchedulerに差し替えると、それらの結果は静かに破棄されます。