🙌

「待つ」処理を超簡単にかけるUniTask導入

2022/10/16に公開

はじめに

こんにちは、まつさこ です。

本投稿は、Iwaken Lab.開発合宿ブログリレー16日目の記事です。

普段のUnity開発において、処理を「待つ」という動作を簡潔に実現できる『UniTask』 というライブラリを重宝しています。
今回は、そのUniTaskの便利さを共有し、多くの人がより効率よくUnity開発できるようになる手助けが出来たらと思います。

開発環境

筆者の開発環境は以下です。

  • Unity 2021.3.5f1
  • Rider 2022.2.3
  • UniTask 2.3.1

UniTaskの導入

UniTaskのReleaseページから最新のunitypackageをダウンロードし、プロジェクトにインポートします。
Assets/Plugins ディレクトリ内に UniTask ディレクトリが出来ていたら成功です。

やったね

(パッケージ導入の手法としてOpenUPMってのもあるらしい。いいかも。)

UniTaskを扱う前に...

UniTaskを使うことでいかに簡潔に「待つ」処理が出来るようになるかということを実感するために、まずはあえてUniTaskを使わない実装を考えてみます。
UniTaskをまだ知らないみなさん、以下のような動作をさせたい場合どのような実装をしますか?

ゲームを再生して3秒経過した後、"Hello universe" というデバッグログが出現する

「3秒経過した後」というのはつまり「3秒待った後」ということですので、主に以下2通りが思い浮かぶと思います。

  1. Updateメソッド 内で3秒時間計測をして、3秒を超えた時にデバッグログを一度だけ出す。
  2. コルーチンを使用して3秒待った後にデバッグログを出す。

前者の「Updateメソッドで完結させる」実装をやってみましょう。

UpdateTimer.cs
using UnityEngine;

public class UpdateTimer : MonoBehaviour
{
    //経過時間
    float _time;
    //待つ時間を変数として定義
    float _waitTime = 3f;
    //デバッグログを一度だけ出すためのフラグ
    bool _flag;
    
    void Update()
    {
        //経過時間が指定時間を超えるかつフラグが立っていないときに、デバッグログを出す
        if (_time >= _waitTime && !_flag)
        {
            Debug.Log("Hello universe");
            _flag = true;
        }
        //1フレームあたりの秒数を加算する
        _time += Time.deltaTime;
    }
}

このような感じで書けると思います。経過時間を保持しておくための変数 _time や、デバッグログを一度だけ出すためのフラグ変数 _flag を用意しなければならず、ちょっと冗長です。

UniTaskで実装してみる

前項でUpdateメソッド実装したものと同じ動作をUniTaskで記述すると、以下のようになります。

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

public class UniTaskTimer : MonoBehaviour
{
    //待つ時間を変数として定義
    float _waitTime = 3f;
    
    async void Start()
    {
        //_waitTime秒待つ
        await UniTask.Delay(TimeSpan.FromSeconds(_waitTime));
        Debug.Log("Hello universe");
    }
}

驚きの短さ。これで、Updateメソッドで実装したときと全く同じ動作をします。
ここでの筆者的UniTask推しポイントは3点あります。

  1. Startメソッドに記述している。つまりゲーム実行直後一度だけ呼ばれている。
  2. 時間経過を保持する変数や、時間経過後の処理を一度だけ起こすためのフラグ変数が不要
  3. 時系列に沿って上から順番に記述できるので、動作が直感的。

複雑な「待ち」処理で真価を発揮するUniTask

前項で紹介したUniTaskの推しポイントによる恩恵をより実感するために、さらに発展して、

Spaceキーを押すたびに、順番に「こんにちは」「お元気ですか」「さようなら」とデバッグログ表示する

という実装をする場合を考えてみましょう。

これをUpdateメソッド内で書こうとすると...
サンプルのコードすら書きたくないですね😇
if文による条件分岐と、フラグ変数の乱立が予想され、もう帰りたい気分になります。

UniTask様を使うと以下のように書けます。

UniTaskDialogue.cs
using Cysharp.Threading.Tasks;
using UnityEngine;

public class UniTaskDialogue : MonoBehaviour
{
    async void Start()
    {
        await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space));
        Debug.Log("こんにちは");
        await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space));
        Debug.Log("お元気ですか");
        await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space));
        Debug.Log("さようなら");
    }
}

全米が涙する簡潔さで実現できました。
スペースキー入力を取得する Input.GetKeyDown(KeyCode.Space) が、謎の使われ方をしていますね。

await UniTask.WaitUntil(() => ほげほげ)

このようにWaitUntilというUniTaskメソッドをつかうことで、「ほげほげ」がtrueになるまで待ち続けるという処理を実現できます。

止まらない絶対神をなだめる

あ、一点だけ。
CancellationTokenは必ず使いましょう。
ギリシャ神話における絶対神ゼウスのごとく、UniTaskはその威力をとどめることを知りません。シーンの切り替えやオブジェクトの破棄があってもUniTaskは止まりません
コルーチンはGameObjectの破棄と同時に処理も止まるのですが、それと同じノリでUniTaskを実装すると、参照切れやメモリリークなどの問題を引き起こす実装をしてしまうことがあります。

前項の実装も、GameObjectが破棄された時に自動的にキャンセルを飛ばすtokenを引数に渡してあげることで、安心してUniTaskを利用できます。

UniTaskDialogue.cs
using Cysharp.Threading.Tasks;
using UnityEngine;

public class UniTaskDialogue : MonoBehaviour
{
    async void Start()
    {
        //GameObjectが破棄された時にキャンセルを飛ばすトークンを作成
        var token = this.GetCancellationTokenOnDestroy();
        //UniTaskメソッドの引数にCancellationTokenを入れる
        await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space), cancellationToken: token);
        Debug.Log("こんにちは");
        await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space), cancellationToken: token);
        Debug.Log("お元気ですか");
        await UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Space), cancellationToken: token);
        Debug.Log("さようなら");
    }
}

まとめ

UniTaskというライブラリの神っぷりが伝わりましたでしょうか?

尚、UniTaskライブラリの詳細な仕様や使い方は本記事では割愛しました。
UniTaskの様々な機能や使い方については、toRisouPさんの一連の記事がとても分かりやすいです。いつもありがとうございます。
https://qiita.com/toRisouP/items/4445b6b9bf00e49eb147

Discussion