【Unity】一定時間後にスクリプトの処理を呼び出す方法まとめ

# この記事について

古い記事のPVが未だにそれなりにあるので、~~2021年現在のやり方で書き直してみました。~~ 2024年版として書き直しました。

# やりたいこと

「スクリプトの実行タイミングを操作したい」です。
つまり**一定時間後に指定した処理を実行する**といった処理をどう書くか紹介します。

### 先に結論

[UniTask](https://github.com/Cysharp/UniTask)を使いましょう。

Unity標準機能だけでも実現はできますが、柔軟性が無かったりパフォーマンスが出ない場合があります。
UniTaskを使えばだいたい解決します。


# 標準機能のみで書く場合

外部ライブラリを用いず、UnityやC#の機能のみで記述する場合のやり方。

## 処理をN秒後に実行したい

### Invokeを使う

`MonoBehaviour.Invoke()`メソッドを使うことで指定した処理を一定秒数後に呼び出すことができます。
引数には対象のメソッド名を指定します（`nameof`で指定すると便利）

実行をキャンセルする場合は`CancelInvoke()`を使います。

```cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        //DelayMethodを3.5秒後に呼び出す
        Invoke(nameof(DelayMethod), 3.5f);
    }

    void DelayMethod()
    {
        Debug.Log("Delay call");
    }

    private void OnDestroy()
    {
        // Destroy時に登録したInvokeをすべてキャンセル
        CancelInvoke();
    }
}
```

### コルーチン

コルーチンを利用することでも一定時間後の処理を実行するという書き方ができます。
`Invoke()`よりも細かい制御が効きます。

```cs
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // コルーチンの起動
        StartCoroutine(DelayCoroutine());
    }

    // コルーチン本体
    private IEnumerator DelayCoroutine()
    {
        transform.position = Vector3.one;

        // 3秒間待つ
        yield return new WaitForSeconds(3);

        // 3秒後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```

また、「デリゲート」という機能を組み合わせることで使い回しが効くようにできます。

```cs
using System;
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        transform.position = Vector3.one;

        // コルーチンの起動
        StartCoroutine(DelayCoroutine(3, () =>
        {
            // 3秒後にここの処理が実行される
            transform.position = Vector3.zero;
        }));
    }

    // 一定時間後に処理を呼び出すコルーチン
    private IEnumerator DelayCoroutine(float seconds, Action action)
    {
        yield return new WaitForSeconds(seconds);
        action?.Invoke();
    }
}
```

### async/await + 標準Task(ValueTask)

C#の`async/await`という機能をつかってコルーチンに似た処理が書けます。
（ただしキャンセル周りが若干ややこしいので注意 ：[参考](https://qiita.com/toRisouP/items/60673e4a39319e69fbc0)）

```cs
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class TaskSample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        // 「destroyCancellationToken」でこのGameObjectに紐づいた
        // CancellationTokenが取得できる
        _ = DelayAsync(destroyCancellationToken);
    }

    // 非同期メソッド
    private async ValueTask DelayAsync(CancellationToken token)
    {
        transform.position = Vector3.one;

        // 3秒間待つ
        await Task.Delay(TimeSpan.FromSeconds(3), token);

        // 3秒後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```

### async/await + Awaitable(Unity 2023.1~)

Unity 2023.1以降のバージョンであれば`Awaitable`というAPIが利用できるようになっています。
こちらを使うことで標準Taskよりは（おそらく）高パフォーマンスで処理が書けると思います。

```cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AwaitableSample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        // 「destroyCancellationToken」でこのGameObjectに紐づいた
        // CancellationTokenが取得できる
        _ = DelayAsync(destroyCancellationToken);
    }

    // 非同期メソッド
    // ここの戻り値がTask/ValueTaskなのは一緒
    private async ValueTask DelayAsync(CancellationToken token)
    {
        transform.position = Vector3.one;

        // 3秒間待つ
        // awaitする対象が異なる
        await Awaitable.WaitForSecondsAsync(3, token);

        // 3秒後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```



## 処理をNフレーム後に実行したい

### コルーチン

コルーチンでNフレーム待つという処理がかけます。
`yield return null`を1回実行すると1フレーム待つため、これを繰り返すことで指定フレーム数待機できます。

```cs
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // コルーチンの起動
        StartCoroutine(DelayCoroutine());
    }

    // コルーチン本体
    private IEnumerator DelayCoroutine()
    {
        transform.position = Vector3.one;

        // 10フレーム待つ
        for (var i = 0; i < 10; i++)
        {
            yield return null;
        }

        // 10フレーム後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```

デリゲートを使うとこう書けます。

```cs
using System;
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        transform.position = Vector3.one;

        // コルーチンの起動
        StartCoroutine(DelayCoroutine(10, () =>
        {
            // 10F後にここの処理が実行される
            transform.position = Vector3.zero;
        }));
    }

    // 一定フレーム後に処理を呼び出すコルーチン
    private IEnumerator DelayCoroutine(int delayFrameCount, Action action)
    {
        for (var i = 0; i < delayFrameCount; i++)
        {
            yield return null;
        }

        action?.Invoke();
    }
}
```

また`WaitForFixedUpdate`を使うことでFixedUpdateタイミングで待機することもできます。

```cs
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // コルーチンの起動
        StartCoroutine(DelayCoroutine());
    }

    // コルーチン本体
    private IEnumerator DelayCoroutine()
    {
        transform.position = Vector3.one;

        // FixedUpdateで10フレーム待つ
        for (var i = 0; i < 10; i++)
        {
            yield return new WaitForFixedUpdate();
        }

        // 10フレーム後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```

### async/await + Awaitable

Awaitableを使って指定フレーム数待つことができます。

```cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class AwaitableSample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        _ = DelayAsync(destroyCancellationToken);
    }

    // 非同期メソッド
    private async ValueTask DelayAsync(CancellationToken token)
    {
        transform.position = Vector3.one;

        // 10フレーム待つ
        for (var i = 0; i < 10; i++)
        {
            await Awaitable.NextFrameAsync(token);

            // こうすればFixedUpdate
            // await Awaitable.FixedUpdateAsync(token);
        }

        // 10フレーム後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```



## 処理を一定間隔で定期的に実行したい

### InvokeRepeating

`MonoBehaviour.InvokeRepeating`を使うことで処理を一定間隔で実行することができます。

```cs
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        //DelayMethodを3.5秒後に呼び出し、以降は１秒毎に実行
        InvokeRepeating(nameof(DelayMethod), 3.5f, 1.0f);
    }

    void DelayMethod()
    {
        Debug.Log("Delay call");
    }

    private void OnDestroy()
    {
        // Destroy時に登録したInvokeをすべてキャンセル
        CancelInvoke();
    }
}
```


### コルーチン

コルーチンと`while`/`for`を組み合わせるパターンです。

```cs
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // コルーチンの起動
        StartCoroutine(DelayCoroutine());
    }

    // コルーチン本体
    private IEnumerator DelayCoroutine()
    {
        while (true) // このGameObjectが有効な間実行し続ける
        {
            yield return new WaitForSeconds(1);

            // １秒毎に実行する
            Debug.Log("Do!");
        }
    }
}
```

### async/await

`async/await`でも同様のことができます。

```cs
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        _ = LoopAsync(destroyCancellationToken);
    }

    // 非同期メソッド
    private async ValueTask LoopAsync(CancellationToken token)
    {
        // キャンセルされるまで繰り返し
        while (!token.IsCancellationRequested)
        {
            // 1秒待つ
            await Task.Delay(TimeSpan.FromSeconds(1), token);

            // Awaitableが使えるならこっちでもOK
            // await Awaitable.WaitForSecondsAsync(1, token);

            // １秒毎に実行する
            Debug.Log("Do!");
        }
    }
}
```

## 細かい時間の制御がしたい

### `Update()` と `FixedUpdate()` を行ったり着たりしたい

#### コルーチン


```cs
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // コルーチンの起動
        StartCoroutine(DelayCoroutine());
    }

    // コルーチン本体
    private IEnumerator DelayCoroutine()
    {
        // デフォルトは呼び出したタイミング（Start()はUpdate()と同等）
        Debug.Log("OnUpdate!");

        // 次のFixedUpdateタイミングまでまつ
        yield return new WaitForFixedUpdate();
        
        // ここの処理はFixedUpdateのタイミングと同等
        Debug.Log("OnFixedUpdate!");

        // 1フレーム待って次のUpdate()タイミングに移す
        yield return null;
        
        // ここはUpdateタイミング
        Debug.Log("OnUpdate!");
    }
}
```

#### async/await + Awaitable

実行タイミングの切り替えは`Awaitable`で実現できます。

```cs
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        _ = SwitchAsync(destroyCancellationToken);
    }

    // 非同期メソッド
    private async ValueTask SwitchAsync(CancellationToken token)
    {
        // デフォルトは呼び出したタイミング（Start()はUpdate()と同等）
        Debug.Log("OnUpdate!");

        // 次のFixedUpdateタイミングまでまつ
        await Awaitable.FixedUpdateAsync(token);

        // ここの処理はFixedUpdateのタイミングと同等
        Debug.Log("OnFixedUpdate!");

        // 1フレーム待って次のUpdate()タイミングに移す
        await Awaitable.NextFrameAsync(token);

        // ここはUpdateタイミング
        Debug.Log("OnUpdate!");
    }
}
```

### Time.timeScaleの影響を受けずに処理を一定時間後に呼び出したい

#### コルーチン

`WaitForSecondsRealtime`を使うことで`timeScale`の影響を無視した実時間での計測ができます。

```cs
using System.Collections;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // コルーチンの起動
        StartCoroutine(DelayCoroutine());
    }

    // コルーチン本体
    private IEnumerator DelayCoroutine()
    {
        transform.position = Vector3.one;
        
        // 3秒間待つ
        // Time.timeScale の影響を受けずに実時間で3秒待つ
        yield return new WaitForSecondsRealtime(3);

        // 3秒後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```

#### async/await

`Task.Delay`と`Awaitable.WaitForSecondsAsync`のそれぞれで挙動が異なります。

`Task.Delay`は常にtimeScaleの影響を受けません。  
`Awaitable.WaitForSecondsAsync`は常にtimeScaleの影響を受けます。


```cs
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 時間の進みを1/10にする
        Time.timeScale = 0.1f;
        
        // 非同期メソッド実行
        _ = DelayAsync(destroyCancellationToken);
    }

    // 非同期メソッド
    private async ValueTask DelayAsync(CancellationToken token)
    {
        Debug.Log("One!");

        // Task.Delay は timeScaleの影響を受けない
        await Task.Delay(TimeSpan.FromSeconds(1), token);

        Debug.Log("Two!");

        // Awaitable.WaitForSecondsAsync は timeScaleの影響を受ける
        await Awaitable.WaitForSecondsAsync(1, token);

        Debug.Log("Three!");
    }
}
```


# R3を使う

[R3](https://github.com/Cysharp/R3) をつかったパターンです。

## 処理をN秒後に実行したい

### Observable.Timerを使う

```cs
using System;
using R3;
using UnityEngine;
using Debug = UnityEngine.Debug;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 5秒待つ（Time.timeScaleの影響あり）
        Observable
            .Timer(TimeSpan.FromSeconds(5),
                destroyCancellationToken)
            .Subscribe(_ => { Debug.Log("Done"); });

        // 5秒待つ（Time.timeScaleの影響なし）
        Observable
            .Timer(TimeSpan.FromSeconds(5),
                UnityTimeProvider.UpdateIgnoreTimeScale,
                destroyCancellationToken)
            .Subscribe(_ =>
            {
                Debug.Log("Done"); 
                
            });
    }
}
```

## 処理をNフレーム後に実行したい

### Observable.TimerFrameを使う

```cs
using R3;
using UnityEngine;
using Debug = UnityEngine.Debug;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 2F後に呼び出す
        Observable.TimerFrame(2, destroyCancellationToken)
            .Subscribe(_ =>
            {
                Debug.Log("Done");
            });

        // FixedUpdateで2F後に実行する
        Observable.TimerFrame(2, UnityFrameProvider.FixedUpdate, destroyCancellationToken)
            .Subscribe(_ =>
            {
                Debug.Log("Done");
            });
    }
}
```


## 処理を一定間隔で定期的に実行したい

`Observable.Timer`/`Observable.Interval`/`Observable.TimerFrame`/`Observable.IntervalFrame`でできます。

```cs
using System;
using R3;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 3秒後に実行したあと、以降1秒毎に実行
        Observable.Timer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(1), destroyCancellationToken)
            .Subscribe(_ => DelayMethod());

        // 常に3秒間隔で実行
        Observable.Interval(TimeSpan.FromSeconds(3), destroyCancellationToken)
            .Subscribe(_ => DelayMethod());
    }

    private void DelayMethod()
    {
        Debug.Log("Delay call");
    }
}
```

# UniTaskを使う（一番オススメ）

標準機能だけでなく、[UniTask](https://github.com/Cysharp/UniTask)を使うパターンです。
**これが一番推奨です。**

Task/ValueTask/Awaitableでも時間待機やフレーム待機処理は実現できましたが、`UniTask`が一番柔軟に記述できてかつ高パフォーマンスです。


## 処理をN秒後に実行したい

標準Taskを使う場合とほとんど変わりません。

```cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        DelayAsync(destroyCancellationToken).Forget();
    }

    // 非同期メソッド
    private async UniTask DelayAsync(CancellationToken token)
    {
        transform.position = Vector3.one;

        // 3秒間待つ
        await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken: token);

        // 3秒後に原点にワープ
        transform.position = Vector3.zero;
    }
}
```

## 処理をNフレーム後に実行したい

`UniTask.Yield`や`UniTask.DelayFrame`でフレーム単位での待機ができます。

```cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        DelayAsync(destroyCancellationToken).Forget();
    }

    // 非同期メソッド
    private async UniTask DelayAsync(CancellationToken token)
    {
        transform.position = Vector3.one;

        // 5F間待つ
        await UniTask.DelayFrame(5, cancellationToken: token);

        // 5F後に原点にワープ
        transform.position = Vector3.zero;

        // 1F待つ
        await UniTask.Yield(PlayerLoopTiming.Update, token);

        Destroy(gameObject);
    }
}
```

## 細かい時間の制御がしたい

`UniTask.Yield`や`UniTask.NextFrame`の引数でタイミングを指定できます。

`UniTask.Yield`や`UniTask.NextFrame`の違いは必ず1F待機するかどうかです。
（`UniTask.Yield`の場合はタイミングの指定によっては1F待たずに実行される場合があります）

```cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        DelayAsync(destroyCancellationToken).Forget();
    }

    // 非同期メソッド
    private async UniTask DelayAsync(CancellationToken token)
    {
        // Updateタイミングを待つ
        await UniTask.Yield(PlayerLoopTiming.Update, token);

        Debug.Log("OnUpdate!");
        
        // 次のFixedUpdateを待つ
        await UniTask.WaitForFixedUpdate(token);
        
        Debug.Log("OnFixedUpdate!");
        
        // 次のPostLateUpdateタイミングを待つ
        await UniTask.NextFrame(PlayerLoopTiming.PostLateUpdate, token);

        Debug.Log("OnPostLateUpdate!");

    }
}
```

### Time.timeScaleの影響を受けずに処理を一定時間後に呼び出したい

`UniTask.Delay`の引数で`DelayType.UnscaledDeltaTime`または`DelayType.Realtime`を指定してください。
（`DelayType.UnscaledDeltaTime`はUnitTestでは正しく動作しませんが、`DelayType.Realtime`はUnitTestでも利用できます）

```cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class Sample : MonoBehaviour
{
    private void Start()
    {
        // 非同期メソッド実行
        DelayAsync(destroyCancellationToken).Forget();
    }

    // 非同期メソッド
    private async UniTask DelayAsync(CancellationToken token)
    {
        // Time.timeScaleを無視して3秒後のUpdate()タイミングで実行
        await UniTask.Delay(
            TimeSpan.FromSeconds(3),
            DelayType.UnscaledDeltaTime,
            PlayerLoopTiming.Update, token);

        Debug.Log("OnPostLateUpdate!");
    }
}
```

# まとめ

それぞれを比較すると次になります。

|書き方|メリット|デメリット|備考|
|:--|:--|:--|:--|
|Invoke|標準機能で最初から使える|使い辛い|おすすめしない|
|コルーチン|標準機能で最初から使える<br>ある程度の柔軟性がある<br>GameObjectを破棄したら勝手に止まる|async/awaitほど柔軟性がない<br>非同期処理が連鎖するような処理は苦手<br>yieldをまたいでtry-catchが使えない|かなりレガシーな機能<br>async/awaitの記法を覚えてそっちを使うべき|
|async/await + Task|標準機能で最初から使える|パフォーマンスがそこまで高くない<br>細かい制御はできない|UniTaskを使うべき|
|async/await + Awaitable|Unity2023.1~なら標準で使える<br>標準Taskより柔軟性あり|UniTaskほど細かい制御はできない|UniTaskが導入できない環境ではこれが第一候補になる|
|async/await + UniTask|高パフォーマンス<br>一番柔軟に処理が書ける|外部ライブラリの導入が必要|**これが一番おすすめ**|
|R3|Observableとして処理が書ける|手続き的な処理が苦手|今回の問題解決のためにわざわざ導入するライブラリではない<br>もし既に導入していたならば選択肢としてありかも、くらい|


`UniTask`使っておくのが安牌です。次点で`Awitable`。 async/awaitの記法がわからない初学者ならコルーチンも選択肢に入るかも。