この記事について
古い記事のPVが未だにそれなりにあるので、2021年現在のやり方で書き直してみました。 2024年版として書き直しました。
やりたいこと
「スクリプトの実行タイミングを操作したい」です。
つまり一定時間後に指定した処理を実行するといった処理をどう書くか紹介します。
先に結論
UniTaskを使いましょう。
Unity標準機能だけでも実現はできますが、柔軟性が無かったりパフォーマンスが出ない場合があります。
UniTaskを使えばだいたい解決します。
標準機能のみで書く場合
外部ライブラリを用いず、UnityやC#の機能のみで記述する場合のやり方。
処理をN秒後に実行したい
Invokeを使う
MonoBehaviour.Invoke()
メソッドを使うことで指定した処理を一定秒数後に呼び出すことができます。
引数には対象のメソッド名を指定します(nameof
で指定すると便利)
実行をキャンセルする場合はCancelInvoke()
を使います。
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()
よりも細かい制御が効きます。
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;
}
}
また、「デリゲート」という機能を組み合わせることで使い回しが効くようにできます。
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
という機能をつかってコルーチンに似た処理が書けます。
(ただしキャンセル周りが若干ややこしいので注意 :参考)
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よりは(おそらく)高パフォーマンスで処理が書けると思います。
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フレーム待つため、これを繰り返すことで指定フレーム数待機できます。
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;
}
}
デリゲートを使うとこう書けます。
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タイミングで待機することもできます。
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を使って指定フレーム数待つことができます。
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
を使うことで処理を一定間隔で実行することができます。
using UnityEngine;
public class Sample : MonoBehaviour
{
private void Start()
{
//DelayMethodを3.5秒後に呼び出し、以降は1秒毎に実行
InvokeRepeating(nameof(DelayMethod), 3.5f, 1.0f);
}
void DelayMethod()
{
Debug.Log("Delay call");
}
private void OnDestroy()
{
// Destroy時に登録したInvokeをすべてキャンセル
CancelInvoke();
}
}
コルーチン
コルーチンとwhile
/for
を組み合わせるパターンです。
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);
// 1秒毎に実行する
Debug.Log("Do!");
}
}
}
async/await
async/await
でも同様のことができます。
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);
// 1秒毎に実行する
Debug.Log("Do!");
}
}
}
細かい時間の制御がしたい
Update()
と FixedUpdate()
を行ったり着たりしたい
コルーチン
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
で実現できます。
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
の影響を無視した実時間での計測ができます。
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の影響を受けます。
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 をつかったパターンです。
処理をN秒後に実行したい
Observable.Timerを使う
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を使う
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
でできます。
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を使うパターンです。
これが一番推奨です。
Task/ValueTask/Awaitableでも時間待機やフレーム待機処理は実現できましたが、UniTask
が一番柔軟に記述できてかつ高パフォーマンスです。
処理をN秒後に実行したい
標準Taskを使う場合とほとんど変わりません。
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
でフレーム単位での待機ができます。
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待たずに実行される場合があります)
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でも利用できます)
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 | 標準機能で最初から使える | 使い辛い | おすすめしない |
コルーチン | 標準機能で最初から使える ある程度の柔軟性がある GameObjectを破棄したら勝手に止まる |
async/awaitほど柔軟性がない 非同期処理が連鎖するような処理は苦手 yieldをまたいでtry-catchが使えない |
かなりレガシーな機能 async/awaitの記法を覚えてそっちを使うべき |
async/await + Task | 標準機能で最初から使える | パフォーマンスがそこまで高くない 細かい制御はできない |
UniTaskを使うべき |
async/await + Awaitable | Unity2023.1~なら標準で使える 標準Taskより柔軟性あり |
UniTaskほど細かい制御はできない | UniTaskが導入できない環境ではこれが第一候補になる |
async/await + UniTask | 高パフォーマンス 一番柔軟に処理が書ける |
外部ライブラリの導入が必要 | これが一番おすすめ |
R3 | Observableとして処理が書ける | 手続き的な処理が苦手 | 今回の問題解決のためにわざわざ導入するライブラリではない もし既に導入していたならば選択肢としてありかも、くらい |
UniTask
使っておくのが安牌です。次点でAwitable
。 async/awaitの記法がわからない初学者ならコルーチンも選択肢に入るかも。