この記事について
古い記事のPVが未だにそれなりにあるので、2021年現在のやり方で書き直してみました。
やりたいこと
「スクリプトの実行タイミングを操作したい」です。
つまり一定時間後に指定した処理を実行するといった処理をどう書くか紹介します。
標準機能のみで書く場合
外部ライブラリを用いず、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
C#のasync/await
という機能をつかってコルーチンに似た処理が書けます。
ただしそのままでは扱いにくいのでasync/await
を使う場合は後述するUniTask
とセットでつかったほうがオススメです。
(とくにキャンセル周りがすごい面倒くさい)
using System;
using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class Sample : MonoBehaviour
{
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private void Start()
{
var ct = _cancellationTokenSource.Token;
// 非同期メソッド実行
_ = DelayAsync(ct);
}
// 非同期メソッド
private async Task DelayAsync(CancellationToken token)
{
transform.position = Vector3.one;
// 3秒間待つ
await Task.Delay(TimeSpan.FromSeconds(3), token);
// 3秒後に原点にワープ
transform.position = Vector3.zero;
}
private void OnDestroy()
{
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
}
}
処理をNフレーム後に実行したい
コルーチン(おすすめ)
Nフレーム後に実行したいという場合はコルーチンを使うのがもっとも簡単です。
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();
}
}
処理を一定間隔で定期的に実行したい
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!");
}
}
}
細かい時間の制御がしたい
Update()
と FixedUpdate()
を行ったり着たりしたい
コルーチンを使うことで実行タイミングを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!");
}
}
Time.timeScaleの影響を受けずに処理を一定時間後に呼び出したい
コルーチンとWaitForSecondsRealtime
を組み合わせることで実現できます。
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;
}
}
UniRxを使う
UniRx をつかったパターンです。
処理をN秒後に実行したい
Observable.Timerを使う
using System;
using UniRx;
using UnityEngine;
public class Sample : MonoBehaviour
{
private void Start()
{
// 3秒後に呼び出す
Observable.Timer(TimeSpan.FromSeconds(3))
.Subscribe(_ => DelayMethod());
// 3秒後に呼び出す
// Time.timeScaleを無視して実時間で計測する
Observable.Timer(TimeSpan.FromSeconds(3), Scheduler.MainThreadIgnoreTimeScale)
.Subscribe(_ => DelayMethod());
}
private void DelayMethod()
{
Debug.Log("Delay call");
}
}
処理をNフレーム後に実行したい
Observable.TimerFrameを使う
using UniRx;
using UnityEngine;
public class Sample : MonoBehaviour
{
private void Start()
{
// 2F後に呼び出す
Observable.TimerFrame(2)
.Subscribe(_ => DelayMethod())
.AddTo(this);
// 2F後のFixedUpdateで実行する
Observable.TimerFrame(2, FrameCountType.FixedUpdate)
.Subscribe(_ => DelayMethod())
.AddTo(this);
}
private void DelayMethod()
{
Debug.Log("Delay call");
}
}
「次のフレーム」と指定したい場合はNextFrame
でもOKです。
using UniRx;
using UnityEngine;
public class Sample : MonoBehaviour
{
private void Start()
{
// 次のフレームで呼び出す
Observable.NextFrame()
.Subscribe(_ => DelayMethod())
.AddTo(this);
// 次のFixedUpdateで実行する
Observable.NextFrame( FrameCountType.FixedUpdate)
.Subscribe(_ => DelayMethod())
.AddTo(this);
}
private void DelayMethod()
{
Debug.Log("Delay call");
}
}
処理を一定間隔で定期的に実行したい
Observable.Timer
/Observable.Interval
/Observable.TimerFrame
/Observable.IntervalFrame
でできます。
using System;
using UniRx;
using UnityEngine;
public class Sample : MonoBehaviour
{
private void Start()
{
// 3秒後に実行したあと、以降1秒毎に実行
Observable.Timer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(1))
.Subscribe(_ => DelayMethod())
.AddTo(this);
// 常に3秒間隔で実行
Observable.Interval(TimeSpan.FromSeconds(3))
.Subscribe(_ => DelayMethod())
.AddTo(this);
}
private void DelayMethod()
{
Debug.Log("Delay call");
}
}
UniTaskを使う
UniTaskとasync/await
を使うパターン。
基本的にはコルーチンと同じです。
処理をN秒後に実行したい
標準Taskを使う場合とあまり変わりません。
違いはCancellationToken
の生成が楽になったくらいです。
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class Sample : MonoBehaviour
{
private void Start()
{
var ct = this.GetCancellationTokenOnDestroy();
// 非同期メソッド実行
DelayAsync(ct).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()
{
var ct = this.GetCancellationTokenOnDestroy();
// 非同期メソッド実行
DelayAsync(ct).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()
{
var ct = this.GetCancellationTokenOnDestroy();
// 非同期メソッド実行
DelayAsync(ct).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()
{
var ct = this.GetCancellationTokenOnDestroy();
// 非同期メソッド実行
DelayAsync(ct).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!");
}
}
まとめ
標準機能のみで扱えてバランスがよいのは「コルーチン」。
コルーチンよりも高機能だけどちょっと難しいのが「async/await
+ UniTask」。
今日においてはUniRxを率先して使う必要はないです。
(Invoke()
は実際のコードで使われてるのを見かけたことないです)