Unity向けタイマーライブラリ「UniTimer」をリリースしました

Git のリポジトリは以下。

github.com

使い方

まんまコピペだけど。

using System;
using UnityEngine;

namespace Takap.Utility.Timers.Demo
{
    public class SampleScript : MonoBehaviour
    {
        [SerializeField, Range(0.5f, 2f)] float timeScale = 1f;

        private void Start()
        {
            //
            // パッケージを取り込むと Monobehavior に以下の 4つのメソッドが追加される
            // 
            // (1) RegisterTimer: タイマーを待機状態で登録
            // (2) StartTimer: タイマーを開始した状態で登録
            // (3) DelayOnce: 指定した時間遅延して処理を1回実行する
            // (4) GetTimers: このコンポーネントから登録したタイマーを全て取得する
            // 

            MyLog.Log("Start timers.");

            Time.timeScale = this.timeScale;

            // ---------- Case.1 ----------
            // 1秒間隔で実行されるタイマーを登録した後に開始する
            IUniTimerHandle h1 = this.RegisterTimer(1f, _ => MyLog.Log("Case.1"));
            h1.Start();

            // ---------- Case.2 ----------
            // 1秒間隔で実行するタイマー登録して即座に開始する
            IUniTimerHandle h2 = this.StartTimer(1f, _ => MyLog.Log("Case.2"));

            // ---------- Case.3 ----------
            // LastUpdate で実行されるタイマー登録を行う
            // 通常は Update でタイマーが実行される
            IUniTimerHandle h3 = this.StartTimer(1f, _ => MyLog.Log("Case.3"), true);

            // ---------- Case.4 ----------
            // 1秒間隔で実行されるタイマーを登録して各種オプションを設定する
            IUniTimerHandle h4 =
                this.StartTimer(1f, _ => MyLog.Log("Case.4"))
                    // Time.timeScale を無視するタイマーに変更する
                    .SetIgnoreTimeScale(true)
                    // 5回だけ実行するように実行回数を指定する
                    .SetExecCount(5)
                    // 実行が終わったときにコールバックを呼び出す
                    .OnComplete(_ => MyLog.Log("Case.4 complete."));

            // ---------- Case.5 ----------
            // 1秒間隔で実行されるタイマーを登録して途中から実行間隔を変更する
            IUniTimerHandle h5 = this.StartTimer(1f, h =>
            {
                MyLog.Log("Case.5(1)");

                // 3回実行されたらインターバルを2秒間隔に変更して
                // 2回実行したら完了イベントを受け取るように変更する
                if (h.CurrentExecCount >= 3)
                {
                    h.ChangeInterval(2f)
                        .SetExecCount(2)
                        .ChangeElapsedHanlder(_ => MyLog.Log("Case.5(2)"))
                        // 全て実行が完了したら完了通知を行う
                        .OnComplete(_ => MyLog.Log("Case.5(Complete)"));
                }
            });

            // ---------- Case.6 ----------
            // 2秒後に指定した処理を1度だけ実行する
            IUniTimerHandle h6 =
                this.DelayOnce(2f, _ => MyLog.Log("Case.6(Once)"))
                    .OnComplete(_ => MyLog.Log("Case.6(Complete)"));

            // ---------- Case.7 ----------
            // タイムスケールを無視して2秒後に指定した処理を1度だけ実行する
            IUniTimerHandle h7 =
                this.DelayOnce(2f, _ => MyLog.Log("Case.7(Once)"))
                    .SetIgnoreTimeScale(true)
                    .OnComplete(_ => MyLog.Log("Case.7(Complete)"));

            IUniTimerHandle[] timers = this.GetTimers();
            Debug.Log("TimerCount=" + timers.Length);

            // ---------- Case.8 ----------
            // 終了時に何らかの条件次第でタイマーを延長する
            int i = 0;
            IUniTimerHandle h8 =
                this.StartTimer(1f, _ => MyLog.Log("Case.8"))
                    .SetExecCount(3)
                    .OnComplete(h =>
                    {
                        if (i++ < 2) // 2回延長したら終了
                        {
                            MyLog.Log("Case.8 add count");
                            h.AddExecCount(2); // 終了時にタイマーを2回追加
                        }
                    });

            // 
            // 補足:
            // 
            // (★1)
            // 登録したタイマーは Component や GameObject が破棄されたら同時に破棄されるので
            // OnDestroy に破棄するコードなどは書かなくてよい
            // 
            // (★2) 
            // また、コンポーネントの enabled や
            // gameObject.activeInHierarchy によって停止・再開は自動で行われるため
            // OnEnable に最下位処理は書かなくてよい
            // 
            // (★3)
            // 登録したときに得られる IUniTimerHandle 経由でタイマーの設定を後から変更できるため
            // 操作が発生する場合は戻り値のオブジェクトを状況に応じてフィールドに保存しておく
            // 
            // (★4)
            // 登録後に即座に実行するケースには対応しないので、自分で登録する前に1度呼び出してください
            // 

            //
            // 特記:
            // 
            // このタイマーライブラリで対応しない事:
            //   * FixedUpdate のサポートは対応しない
            //   * フレーム単位の実行のタイマーは対応しない
            //   * 途中から変更 Update ⇔ FixedUpdate の区分変更はできない
            //   * コルーチンの入れ子と同等機能のサポートはしない
            //     * 但し OnComplete で概ね代替している
            //
        }

        // Called by UnityEvent
        public void DeleteTimers()
        {
            foreach (IUniTimerHandle hnd in this.GetTimers())
            {
                using (hnd)
                {
                    // all delete
                }
            }
        }

        private IEnumerator printMessage()
        {
            for (int i = 0; i < 3; i++)
            {
                Debug.Log("Count=" + i);
                yield return new WaitForSeconds(1.0f);
            }
        }
    }

    public static class MyLog
    {
        public static void Log(string msg) => Debug.Log($"[{DateTime.Now:HH:mm:ss.fff}] {msg}");
    }
}

残件・メモ

仕様上でちょっとアレなところ

  • どう考えても UniTask の方が書き方がイケてる
    • コールバックをデリゲートでメソッドに渡すとか前時代的すぎてやばい
    • async → Forget で投げられそうなので今後要調査とする
  • コルーチンほど自由な感じにはなっていない
    • 特にフレーム数でタイマー動作とか、FixUpdate を途中で同期するとかは実現方法が思いつかなかった

作業中に思った事

  • リリースする単位と開発する単位が違う
    • 開発用のリポジトリから必要な分をリリース用のリポジトリにコピーしてるけど手間がかかって面倒
  • package.json の書き方はネットにいっぱいあってすごく簡単
    • これ配置するるだけで Unity で自動で認識するのはとっても楽
  • PackageManager 向け以外のエクスポートパッケージ作るのがだるい
    • 準備できたらタグ切る → リリース作成するは必須だと思う
  • ちゃんとライセンスを置くこと
    • LICENSE.md と拡張しつけないと github が認識しない
  • PackageManager 経由だとシーンファイルが配れないのでデモシーンとか配れない
    • 説明は別のところでする必要がある
    • リファレンス実装を同時に入れておくとか?
  • Assembly Definition はちゃんと作成してリポジトリに入れておくこと

以上です。