Unityで1秒ごとにメソッドを呼ぶには? ― 6つのパターンと使い分け

広告

Unityでゲームを作っていると、こんな処理が必要になります。

  • 1秒ごとに敵を出す
  • 1秒ごとに残り時間の表示を更新する
  • 1秒ごとに状態を確認する

どの場合も「1秒ごとにメソッドを呼ぶ」という点は同じです。

ただし、Unityには複数の書き方があります。今回は呼び出すメソッドを同じにして、時間の扱い方を比較します。

対象はUnity 6です。まずは基本の3種類を理解し、そのあとポーズ中の動作や、発展的な書き方を見ていきましょう。

同じTick()を、InvokeRepeating(予約)・コルーチン(待機)・Update(時間をためる)の3つの仕組みから呼ぶ図

1. 「何をするか」と「いつ呼ぶか」を分ける

今回、1秒ごとに呼び出すのは、次のメソッドです。

private void Tick()
{
    Debug.Log("1秒ごとの処理");
}

Tickという名前に特別な決まりはありません。ここでは、時計が刻むように、一定間隔で行う処理の名前として付けています。

重要なのは、次の2つを分けて考えることです。

考えること今回の例
何をするかTick()の中に書く
いつ呼ぶかタイマーの仕組みで決める

メソッドを書いただけでは、1秒ごとに実行されません。誰かが呼び出す必要があります。

これから紹介する6つのパターンは、その「呼び出す役」をどう用意するかの違いです。

2. サンプルを試す準備

  1. Hierarchyで空のGameObjectを作り、名前をTimerTestにします。
  2. 試したいサンプルのクラス名と同じ名前でC#スクリプトを作ります。
  3. コードを入れて保存し、TimerTestにアタッチします。
  4. Consoleを開いてPlayします。

たとえば、InvokeRepeatingTimerを試すなら、ファイル名はInvokeRepeatingTimer.csです。

比較するときは、サンプルを1つずつ有効にしてください。 複数を同時に動かすと、それぞれのログが混ざります。ConsoleのCollapseをオフにすると、呼び出しごとのログを追いやすくなります。

今回の例は、基本的に有効になってから約1秒後に初回を実行します。スクリプトを無効にすると止まり、再び有効にすると1秒の待機からやり直します。

3. パターン①:InvokeRepeatingで定期的に呼ぶ

一定間隔でメソッドを呼ぶことが目的なら、短く書けるのがInvokeRepeatingです。

using UnityEngine;

public class InvokeRepeatingTimer : MonoBehaviour
{
    private void OnEnable()
    {
        InvokeRepeating(nameof(Tick), 1f, 1f);
    }

    private void OnDisable()
    {
        CancelInvoke(nameof(Tick));
    }

    private void Tick()
    {
        Debug.Log("1秒ごとの処理");
    }
}

中心になるのは、この1行です。

InvokeRepeating(nameof(Tick), 1f, 1f);
引数意味
nameof(Tick)呼び出すメソッドの名前
最初の1f最初の呼び出しまで待つ秒数
次の1f2回目以降の呼び出し間隔

つまり「1秒後に初回、その後は1秒間隔で呼ぶ」という指定です。

nameof(Tick)は、メソッド名を文字列として取り出すC#の書き方です。"Tick"と直接書くこともできますが、nameofなら名前の入力ミスをコンパイル時に見つけやすくなります。

名前を渡しているので、ここにTick()とは書きません。 Tick()は、その場でメソッドを呼び出す書き方です。

CancelInvoke(nameof(Tick))は、指定したメソッドの予約を取り消します。今回のコードではOnDisableで明示的に停止します。

向いている場面: 引数のないメソッドを、決まった間隔で繰り返したいとき。

覚えておくこと: 時間はTime.timeScaleの影響を受けます。また、実行中に間隔を変更したい場合は、一度取り消して登録し直します。

4. パターン②:コルーチンで「待つ→呼ぶ」を繰り返す

コルーチンを使うと、時間の流れを上から順に書けます。

using System.Collections;
using UnityEngine;

public class CoroutineTimer : MonoBehaviour
{
    private Coroutine timerCoroutine;

    private void OnEnable()
    {
        timerCoroutine = StartCoroutine(Repeat());
    }

    private void OnDisable()
    {
        if (timerCoroutine != null)
        {
            StopCoroutine(timerCoroutine);
            timerCoroutine = null;
        }
    }

    private IEnumerator Repeat()
    {
        while (true)
        {
            yield return new WaitForSeconds(1f);
            Tick();
        }
    }

    private void Tick()
    {
        Debug.Log("1秒ごとの処理");
    }
}

中心になる部分を取り出してみます。

while (true)
{
    yield return new WaitForSeconds(1f);
    Tick();
}

意味は次のとおりです。

  1. WaitForSeconds(1f)で1秒待つ。
  2. Tick()を呼ぶ。
  3. ループの先頭に戻り、また1秒待つ。

yield returnで待っている間も、Unityはほかの更新や描画を続けます。

while (true)があっても、毎回待機するため、ここではメインスレッドを回り続けて占有しません。待機のない無限ループとは動作が違います。

また、コルーチンは、それだけで別スレッドになる仕組みではありません。 Tick()の中に重い処理を書けば、その処理中はゲームの動作に影響します。

初回の実行タイミングは、順番で変わる

今回の順番は「待つ→呼ぶ」です。

yield return new WaitForSeconds(1f);
Tick();

これを「呼ぶ→待つ」にすると、開始時に初回が実行されます。

Tick();
yield return new WaitForSeconds(1f);

どちらも繰り返しは1秒待機ですが、最初の1回が違うことに注意しましょう。

向いている場面: 「待つ→敵を出す→さらに待つ→演出する」のように、順番のある処理を書きたいとき。普通のメソッド呼び出しなので、Tick(10)のように引数を渡すこともできます。

覚えておくこと: スクリプトのチェックを外して無効にするだけでは、コルーチンは自動停止しません。今回の例ではOnDisableで止めています。GameObjectの非アクティブ化ではコルーチンが停止しますが、再アクティブ化で続きから自動再開するわけではありません。

5. パターン③:Updateで経過時間をためる

Updateの中で時間を足し、1秒に達したら呼び出す方法です。

using UnityEngine;

public class UpdateTimer : MonoBehaviour
{
    private float elapsed;

    private void OnEnable()
    {
        elapsed = 0f;
    }

    private void Update()
    {
        elapsed += Time.deltaTime;

        if (elapsed >= 1f)
        {
            elapsed -= 1f;
            Tick();
        }
    }

    private void Tick()
    {
        Debug.Log("1秒ごとの処理");
    }
}

Time.deltaTimeは、前のフレームから今回のフレームまでに進んだ時間を秒で表した値です。

elapsed += Time.deltaTime;

この行で、小さな時間を毎フレーム足しています。

if (elapsed >= 1f)

合計が1秒以上になったら、Tick()を呼びます。Update自体が1秒ごとになるわけではなく、毎フレームの確認で、呼ぶタイミングを判断しているのです。

なぜ0に戻さず、1秒を引くのか?

たとえば、判定時にelapsedが1.02秒になっていたとします。

更新の書き方更新後の値
elapsed = 0f;0秒
elapsed -= 1f;0.02秒

1秒を引けば、超過した0.02秒を次の判定に持ち越せます。

0に戻す書き方も用途によっては使えますが、一定間隔を保ちたいなら、余った時間を捨てない書き方が役立ちます。

処理が遅れたとき、まとめて呼ぶか?

今回のifは、1フレームに最大1回だけ呼びます。1秒以上の持ち越しがあれば、次のフレーム以降で処理します。

経過した回数分を同じフレームで呼びたい場合は、次のようにwhileにできます。

while (elapsed >= 1f)
{
    elapsed -= 1f;
    Tick();
}

ただし、敵の生成などでは、遅れた分が一度に実行されて困ることもあります。大量に繰り返す場合は、1フレームの実行回数に上限を付ける設計も必要です。

なお、Time.deltaTimeには長いフレームでの時間の上限があるため、現実に経過した時間がすべてそのまま積算されるとは限りません。このコードはゲーム内の時間を使うタイマーです。

向いている場面: 残り時間を画面に表示したいとき、条件に応じて時間の加算を止めたいとき、経過時間を自分で管理したいとき。

経過時間が1.02秒のとき、0に戻すと余った0.02秒が消え、1秒を引くと0.02秒を次の判定へ持ち越せることを比べた図

6. パターン④:ポーズ中も進めるならWaitForSecondsRealtime

ここまでの3種類は、ゲーム内の時間を使っています。

通常のTime.timeScale = 1fでは、ゲーム内の1秒と現実の1秒はおおむね対応します。

ところが、Time.timeScale = 0fでポーズすると、これらのタイマーも進まなくなります。Updateは呼ばれ続けますが、Time.deltaTimeが0になるため、時間の加算が止まります。

ゲームがポーズ中でも、時間を進めたいなら、WaitForSecondsRealtimeを使えます。

パターン②の待機部分を、次のように変更してください。

private IEnumerator Repeat()
{
    while (true)
    {
        yield return new WaitForSecondsRealtime(1f);
        Tick();
    }
}

変更するのは、この1行です。

// ゲーム内の時間で待つ
yield return new WaitForSeconds(1f);

// timeScaleの影響を受けない時間で待つ
yield return new WaitForSecondsRealtime(1f);

Update方式でも、次のように変えれば同じ考え方で時間を加算できます。

elapsed += Time.unscaledDeltaTime;

向いている場面: ポーズ画面の表示更新など、ゲームの時間とは別に進めたい処理。

ただし、ポーズ中に進むことと、スクリプトが無効でも進むことは別です。パターン②の停止処理はそのまま使うため、無効化すれば止まります。また、アプリのバックグラウンド動作や、アプリを閉じたあとの実行を保証する仕組みでもありません。

7. パターン⑤:Invokeで「次の1回」を予約する

Invokeは、指定した秒数のあとにメソッドを1回だけ呼ぶ仕組みです。

呼び出されたメソッドの最後で、次の1回を予約すれば、繰り返しにもできます。

using UnityEngine;

public class InvokeTimer : MonoBehaviour
{
    private void OnEnable()
    {
        Invoke(nameof(Repeat), 1f);
    }

    private void OnDisable()
    {
        CancelInvoke(nameof(Repeat));
    }

    private void Repeat()
    {
        Tick();

        if (isActiveAndEnabled)
        {
            Invoke(nameof(Repeat), 1f);
        }
    }

    private void Tick()
    {
        Debug.Log("1秒ごとの処理");
    }
}

ここでは、Repeatが自分自身をその場で直接呼んでいるわけではありません。

Invoke(nameof(Repeat), 1f);

で、Unityに「1秒後に、もう一度呼んでください」と予約しています。

isActiveAndEnabledの確認は、Tick()の処理でスクリプトやGameObjectを無効にした場合に、新しい予約を追加しないためです。

向いている場面: 次回の待機時間を毎回変えたいとき。「今回は1秒後、次は2秒後」という制御もできます。

一定間隔で繰り返すだけなら、InvokeRepeatingのほうが意図を短く表せます。次の予約を忘れると繰り返しが止まる点にも注意しましょう。

8. パターン⑥:async/awaitとTask.Delayで待つ【発展】

C#のasync/awaitでも、「待つ→呼ぶ」を書けます。

今回は、C#標準のTask.Delayを使った例です。初学者は、先に基本の3種類を理解すれば十分です。

using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AsyncTimer : MonoBehaviour
{
    private CancellationTokenSource timerCancellation;

    private async void OnEnable()
    {
        // この実行専用の停止通知を作る
        var source = new CancellationTokenSource();
        timerCancellation = source;
        CancellationToken token = source.Token;

        try
        {
            while (true)
            {
                // Task.Delayの指定はミリ秒。1000ミリ秒 = 1秒
                await Task.Delay(1000, token);

                // 待機完了直後に無効化された場合も確認する
                token.ThrowIfCancellationRequested();

                Tick();
            }
        }
        catch (OperationCanceledException)
            when (token.IsCancellationRequested)
        {
            // 無効化による停止なので、正常な終了として扱う
        }
        catch (Exception exception)
        {
            Debug.LogException(exception);
        }
        finally
        {
            if (ReferenceEquals(timerCancellation, source))
            {
                timerCancellation = null;
            }

            source.Dispose();
        }
    }

    private void OnDisable()
    {
        var source = timerCancellation;
        timerCancellation = null;
        source?.Cancel();
    }

    private void Tick()
    {
        Debug.Log("1秒ごとの処理");
    }
}

Task.Delay(1000)は、1000ミリ秒の待機です。WaitForSeconds(1f)とは単位が違います。

Task.DelayはUnityのTime.timeScaleを参照しないため、ゲーム内のポーズ中にも待機が進みます。

また、非同期処理は、GameObjectの無効化に合わせて自動的に止まるわけではありません。 この例では、OnDisableからキャンセルを通知して止めています。有効化のたびに停止通知を作り直し、終了した実行の通知はfinallyで後片付けします。

コードが長いのは、待機だけでなく、停止と後片付けも含めているためです。

この例はUnityのメインスレッドからOnEnableを実行し、通常のUnityの同期コンテキストに戻る前提です。Task.RunやConfigureAwait(false)を追加して、別スレッドからUnityのオブジェクトを操作する形には変えないでください。

async voidはここではUnityから呼ばれる入口に使っています。自分で設計する非同期メソッドは、呼び出し側が完了を待てるTaskやAwaitableを返す形を基本にしましょう。

Unity 6のAwaitableについて

Unity 6では、Awaitable.WaitForSecondsAsyncなど、Unity向けの待機APIも利用できます。こちらはメインスレッドから呼び出し、メインスレッドで完了するAPIです。

ただし、Task.Delayと名前が似ていても、時間やライフサイクルの扱いまで同じと決めつけないことが大切です。非同期処理は、キャンセルの設計と合わせて、別の段階で学習すると理解しやすくなります。

向いている場面: 通信やファイル読み込みなど、すでにasync/awaitを使っている処理と組み合わせたいとき。

9. 使い分けを比較する

パターン得意なこと時間の基準この例での停止
InvokeRepeating固定間隔で短く書くゲーム内の時間OnDisableでCancelInvoke
コルーチン+WaitForSeconds待機を含む順番のある処理ゲーム内の時間OnDisableでStopCoroutine
Update+Time.deltaTime経過時間を自分で管理するゲーム内の時間無効中はUpdateが呼ばれない
コルーチン+WaitForSecondsRealtimeポーズ中も時間を進めるtimeScaleに依存しない時間OnDisableでStopCoroutine
Invokeで次回予約次回の待機時間を変えるゲーム内の時間OnDisableでCancelInvoke
async/await+Task.Delay非同期処理と組み合わせるtimeScaleに依存しない時間OnDisableでキャンセル通知

迷ったら、次の3つから選びましょう。

  • 決まった間隔で呼ぶだけ → InvokeRepeating
  • 「待つ→処理する」という流れを書く → コルーチン
  • 時間の値を使って、表示や条件判定もする → Update

書き方の種類をすべて暗記する必要はありません。作りたい動作に合わせて、必要な仕組みを選べれば大丈夫です。

Time.timeScaleが0のとき、WaitForSecondsは止まり、WaitForSecondsRealtimeは進むことを比べた図

10. よくある勘違い

「Updateで60回数えれば1秒」ではない

実際のフレーム数は、端末や処理の重さで変わります。フレーム数を数える方法では、30FPSのときと60FPSのときで時間が変わってしまいます。

秒数を扱うなら、Time.deltaTimeなどの時間を使いましょう。

FixedUpdateも、1秒ごとに呼ばれるメソッドではない

FixedUpdateは物理演算の更新に合わせた処理を書くための入口です。固定時間ステップは1秒とは限りません。

物理処理に関係しない、単なる毎秒の呼び出しなら、まず今回の基本3種類を検討できます。

Thread.Sleepで待たない

メインスレッドでThread.Sleep(1000)を実行すると、Unityの更新や描画まで止めてしまいます。

ゲームを動かしながら待つには、コルーチンや非同期の待機を使います。

「1秒ごと」は、厳密に1.000秒ごとという意味ではない

フレームのタイミングや負荷により、呼び出しが遅れることがあります。

特にコルーチンのWaitForSecondsは、待機開始フレームの終わりから時間を数え、指定時間を過ぎた最初のフレームで再開します。

また、コルーチン、Invokeの次回予約、今回の非同期ループは、処理を終えてから次の待機に進む書き方です。処理時間や再開時の遅れが、長時間ではずれとして積み重なる可能性があります。

たとえば時計や制限時間の表示では、Tick()が呼ばれるたびに単純に1秒減らすより、選んだ時間の基準から経過時間を計算し、表示を更新する設計が適しています。

11. 学習のポイント

今回のコードは、どれも最後に同じTick()を呼んでいます。

違うのは、その呼び出しを行う仕組みです。

Tick()の中身を、ログ出力から敵の生成に変えれば、毎秒敵を出す処理になります。画面の更新に変えれば、毎秒表示を更新する処理になります。

「何をするか」と「いつ呼ぶか」を分けると、同じ処理を別のタイミングでも使いやすくなります。

まずは1つのパターンを動かし、次の点を確認してみてください。

  1. 初回は開始直後か、1秒後か。
  2. スクリプトを無効にすると止まるか。
  3. 有効に戻すとどうなるか。
  4. ゲーム内の時間と、ポーズ中も進む時間のどちらが必要か。

コードを置き換えて比較すると、書き方の違いが動作の違いとして見えてきます。

関連記事

  • Unityで一定時間ごとに実行されるメソッドの実装方法まとめ
  • コルーチンの使い方
  • Unityにおける非同期処理入門(async/await)

参考資料

仕様確認にはUnity 6.0(6000.0)の公式資料と、Microsoftの資料を使用しています。

  • Unity:InvokeRepeating
  • Unity:Invoke
  • Unity:WaitForSeconds
  • Unity:WaitForSecondsRealtime
  • Unity:Time.deltaTime
  • Unity:Time.unscaledDeltaTime
  • Unity:コルーチン
  • Unity:Awaitable.WaitForSecondsAsync
  • Unity:Awaitableの完了と継続
  • Microsoft:Task.Delay
訪問数 6 回, 今日の訪問数 6回

広告

Unity,Unity6

Posted by hidepon