いろいろなタイマー

2023年9月29日

Unityで使えるいろいろなタイマーについて見ていきましょう。
今回は、3秒後にメソッドが実行される様子を確認していきます。

サンプル作成に必要な準備

タイマーが正常に動作しているか確認するため、サンプルは、ストップウォッチで時間測定します。

きちんと3秒後にコードが実行できるか確かめるため、C#のStopwatchクラスを使います。ネームスペースSystem.Diagnosticsに実装されていますので、使うには、usingで定義する必要があります。

時間測定用にタイマーを用意

// Stopwatchクラスを使うため、usingを追加
using System.Diagnostics;

// Stopwatchのインスタンスを作成
Stopwatch stopwatch = new Stopwatch();

// ストップウォッチをスタート
stopwatch.Start();

/* ここで何からの処理 */

// ストップウォッチをストップ
stopwatch.Stop();

// コンソールに結果を表示
Debug.Log(“Time “ + stopwatch.Elapsed);

Debugクラスの記述省略

System.Diagnosticsネームスペースには、Debugクラスがあります。Unityにも同じクラスが存在するため、どちらのクラスを使うのか不定なので、System.Diagnostics.Debeg.Log()やUnityEngine.Debug.Log()のようにネームスペースを含めた記述で判別しなければなりませんが、これを省略することができるようになりました。

省略しない場合

using System.Diagnostics;
using UnityEngine;

UnityEngine.Debug.Log(“サンプル”);

省略できる場合

ただし、省略するにはクラスが staticである必要があります。

using System.Diagnostics;
using UnityEngine;
using static UnityEngine.Debug;

Debug.Log(“サンプル”);

InvokePattern

MonobehaviourクラスのInvokeメソッドを使って遅延した処理を実装してみましょう。

Invoke

設定した時間(単位は秒)にメソッドを呼び出します。
例えば次のコードは、3秒後にHogeメソッドが呼び出されます。

Invoke("Hoge", 3f);

第1引数の“Hoge”は、引数なしのメソッド名を記述します。なのでHoge()メソッドを呼び出すわけですが、マジックワード(コード中に文字列を直接記述すること)があると綴りミスでもコンパイルエラーにならないため、メソッド名を文字列で取得することができるnameofメソッドに置き換えるといいでしょう。

Invoke(nameof(Hoge), 3f);

MonobehaviourPattern.cs

using System.Diagnostics;
using UnityEngine;
using static UnityEngine.Debug;

public class MonobehaviourPattern : MonoBehaviour
{
    readonly Stopwatch stopwatch = new Stopwatch();

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            stopwatch.Start();
            Invoke(nameof(Hoge), 3f);
        }
    }

    void Hoge()
    {
        stopwatch.Stop();
        Log("MonobehaviouPattern " + stopwatch.Elapsed);
    }
}

結果

MonobehaviouPattern 00:00:02.9610232

実行できるコードにまとめると次のようになります。

第2引数に繰り返し間隔を記述すると一定時間ごとに呼び出すこともできます。次のサンプルでは、3秒後から繰り返す→以降3秒ごとに繰り返すとなります

InvokeRepeating(nameof(Hoge), 3f, 3f);

CoroutinePattern

Coroutine

メソッドを呼び出しですが、今度はコルーチンを使ってみます。
コルーチンは、遅延実行が呼ばれるメソッド側で可能です。

StartCoroutine(Hoge());

呼び出されるメソッド側に、次のコードを記述するとここに到達したら、3秒経過するまで次のコードは実行しないようにすることができます。ただし、その間、スレッドがロックされることはないにで、他のコードに制御が渡され(例えば、他のスクリプトのUpdateメソッド)継続して処理を進めることができます。

yield return new WaitForSeconds(3f);

実行できるコードにまとめると次のようになります。

using System.Collections;
using System.Diagnostics;
using UnityEngine;
using static UnityEngine.Debug;

public class CoroutinePattern : MonoBehaviour
{
    readonly Stopwatch stopwatch = new Stopwatch();

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            stopwatch.Start();
            StartCoroutine(Hoge());
        }
    }

    IEnumerator Hoge()
    {
        yield return new WaitForSeconds(3f);
        stopwatch.Stop();
        Log("CoroutinePattern " + stopwatch.Elapsed);
    }
}

結果

CoroutinePattern 00:00:02.9782285

UpdatePattern

Update, Time.deltatime

Updateメソッドは、1フレームごとに実行されるメソッドになります。
60fpsだと、1秒間に60回実行されます。

次のコードでは、タイマー値をTime.deltaTime分(1フレームあたりの処理時間。つまりUpdateメソッドが実行される間隔)足し込んでいき、3秒が経過すると、メソッドが呼び出されるようにしています。

float timer;

void Update()
{
    timer += Time.deltaTime;

    if (timer >= 3f)
    {
        Hoge();
    }
}

実行できるコードにまとめると次のようになります。

using System.Diagnostics;
using UnityEngine;
using static UnityEngine.Debug;


public class UpdatePattern : MonoBehaviour
{
    readonly Stopwatch stopwatch = new Stopwatch();

    float timer;

    bool IsStart;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            stopwatch.Start();
            IsStart = true;
        }

        if (!IsStart)
        {
            return;
        }

        timer += Time.deltaTime;

        if (timer >= 3f)
        {
            Hoge();
            timer = -100000;
        }
    }

    void Hoge()
    {
        stopwatch.Stop();
        Log("UpdatePattern " + stopwatch.Elapsed);
    }
}

結果

UpdatePattern 00:00:02.9597800

TaskPattern

Task, await, async

Taskは、簡易に非同期プログラムが作成できるクラスになります。

非同期とは、コードの処理が順番に実行される同期処理と違い、並行して実行されるコードになります。ちなみに1つのプロジェクトで複数のスクリプトが実行されたとき、Updateメソッドが同期処理にように実行されると勘違いしてしまいそうですが、これはそれぞれのUpdateが順番に実行されているためで、同時にUpdataメソッドが実行されているわけではありません。

呼び出し側のコード

引数は、デリゲートになります。(今回は、ラムダ式を使ってメソッドを呼び出します。無名メソッドでも構いません)

Task.Run(() => HogeAsync());

呼び出されるコード

await就職誌をつけることで処理待ちのメソッドを表しています。
メソッド名には、async就職誌をつけて同期処理メソッドであることを明示します。(しないとエラーになりますが・・)
また、戻り値は、Task型にしておきます。

async Task HogeAsync()
{
    await Task.Delay(3000);
    Log("TaskPattern “);
}

実行できるコードにまとめると次のようになります。

using System.Diagnostics;
using System.Threading.Tasks;
using UnityEngine;
using static UnityEngine.Debug;

public class TaskPattern : MonoBehaviour
{
    readonly Stopwatch stopwatch = new Stopwatch();

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            stopwatch.Start();
            Task.Run(() => HogeAsync());
        }
    }

    async Task HogeAsync()
    {
        await Task.Delay(3000);
        stopwatch.Stop();
        Log("TaskPattern " + stopwatch.Elapsed);
    }
}
using System.Diagnostics;
using System.Threading.Tasks;
using UnityEngine;
using static UnityEngine.Debug;

public class TaskPattern : MonoBehaviour
{
    readonly Stopwatch stopwatch = new Stopwatch();

    async void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            stopwatch.Start();
            await Task.Delay(3000);
            stopwatch.Stop();
            Log("TaskPattern " + stopwatch.Elapsed);
        }
    }
}

結果

TaskPattern 00:00:03.0210585

DoTween

アセットストアの無料ツールを使って遅延実行を実現してみます。

DOVirtual.DelayedCall

遅延ようにメソッドが用意されています。

呼び出し側のコード

第1引数に遅延させる秒数、第2引数にデリゲートを記述します。(今回は、ラムダ式を使っていますが、無名メソッドでも実現できます。

DOVirtual.DelayedCall(3f, () => Hoge());

呼び出されるコード

void Hoge()
{
    Log("DoTweenPattern ";
}

実行できるコードにまとめると次のようになります。

using System.Diagnostics;
using DG.Tweening;
using UnityEngine;
using static UnityEngine.Debug;

public class DoTweenPattern : MonoBehaviour
{
    readonly Stopwatch stopwatch = new Stopwatch();

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            stopwatch.Start();
            DOVirtual.DelayedCall(3f, () => Hoge());
        }
    }

    void Hoge()
    {
        stopwatch.Stop();
        Log("DoTweenPattern " + stopwatch.Elapsed);
    }
}

結果

DoTweenPattern 00:00:03.0138715

UniRxPattern

アセットのUniRxを使って非同期プログラムで実現してみましょう。

Observable.Timer

遅延ようにメソッドが用意されています。

呼び出し側のコード

Timerメソッドで、3000ms後に遅延実行されるように指示します。続けてメソッドチェーンで、Hogeメソッドが実行されるように記述します。

Observable.Timer(TimeSpan.FromMilliseconds(3000))
        .Subscribe(_ => Hoge());

呼び出されるコード

void Hoge()
{
    Log("UniRxPattern ");
}

実行できるコードにまとめると次のようになります。

using System;
using System.Diagnostics;
using UniRx;
using UnityEngine;
using static UnityEngine.Debug;

public class UniRxPattern : MonoBehaviour
{
    readonly Stopwatch stopwatch = new Stopwatch();

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            stopwatch.Start();
            Observable.Timer(TimeSpan.FromMilliseconds(3000))
                    .Subscribe(_ => Hoge());
        }
    }

    void Hoge()
    {
        stopwatch.Stop();
        Log("UniRxPattern " + stopwatch.Elapsed);
    }
}

結果

DoTweenPattern 00:00:03.0138715

使い分け

Unityにおいて、コルーチンと Task.Delay() は、両方とも時間的な制御を実現するために使用されることがありますが、違いがあります。

まず、コルーチンはUnityの独自の機能であり、Unityが提供する多くの機能に対応しています。コルーチンは非同期処理を実現するために使用され、yield return文を使って、処理を一時停止し、一定時間待機したり、次のフレームまで処理を遅延したりすることができます。コルーチンは、物理的なオブジェクトの動きを制御するためにも使用されます。

一方、Task.Delay() は、C#の標準機能であり、async/await構文と組み合わせて使用することができます。Task.Delay()は、指定した時間間隔後に、非同期操作を継続するために使用されます。Task.Delay()は、単純な遅延処理を実現するために使用されますが、物理挙動を制御するためには使用されません。

コルーチンとTask.Delay()の使い分けは、主に以下のような要因によって決まります。

  1. Unityの機能を使用する場合は、コルーチンを使用することが適しています。
  2. C#の標準機能を使用する場合は、Task.Delay()を使用することが適しています。
  3. 物理的なオブジェクトの動きを制御する場合は、コルーチンを使用することが適しています。
  4. 単純な時間的な遅延処理を実現する場合は、Task.Delay()を使用することが適しています。

総合すると、コルーチンはUnityの独自の機能であり、物理挙動を制御するために使用されることが多い一方、Task.Delay()はC#の標準機能であり、単純な遅延処理を実現するために使用されることが多いです。

C#,Unity

Posted by hidepon