UniTaskと非同期処理
UniTaskとは
UniTaskは、Unityにおいて非同期処理をうまく扱うためのライブラリです。MITライセンスで配布されているオープンソースのライブラリであり、ライセンスが要求する条件に従っていれば、商用利用の有無に関わらず無料で利用することができます。
会話シーンの実装には非同期処理が多用され、そしてScenarioFlowは非同期処理を扱うためにUniTaskを使用します。そのため、ScenarioFlowを活用するためにはUniTaskの習得が必須となります。
非同期処理とは
非同期処理は、実行完了を待たずに次の処理が実行される処理のことです。
通常、プログラムは1つずつ順番に実行され、これは同期処理と呼ばれます。同期処理の場合、ある処理を実行すると、その処理が完了するまで次の処理は実行されません。これに対して、ある非同期処理を実行すると、その実行の完了は待たずに次の処理の実行が開始されます。
非同期処理を使用する、最もわかりやすい例はロード画面です。ゲームにおいて何かしらのデータをロードするのはよくある処理ですが、その際に、データのロードを同期的に行った場合、ロードが完了するまで他の処理は実行できず、画面がフリーズしたような状態になってしまいます。
そこでロードを非同期的に行うと、ロード処理の実行中に、その完了を待たずに別の処理を実行できるので、例えばロードの進捗バーを表示してユーザーの待ち時間に対するストレスを軽減することができます。
非同期処理は主に、「時間のかかる処理」に適用されると考えて問題ありません。
なぜUniTaskを使うのか
Unityにおいて非同期処理を扱う方法としては、UniTaskのほかにもコルーチンを使う方法や、C#標準のTaskを使う方法などがあります。
その中でUniTaskを使用するのは、UniTaskは標準のTaskをUnity向けに最適化したものであるためパフォーマンスが高いこと、ゲーム開発のために有用な機能を多数そろえていることの2点が主な理由です。
UniTaskのインポート
この後、実際にコードを書いてUniTaskの使い方を学びますが、まずはライブラリをプロジェクトにインポートしてUniTaskを使えるようにしましょう。
UniTaskを導入するもっとも簡単な方法は、.unitypackageファイルをプロジェクトにインポートすることです。下のリンクからGitHubの配布ページにアクセスすると、Releaseにおいてある.unitypackageファイルが入手できます。
返り値のないUniTask
遅延付きメッセージ表示プログラム
UniTaskを使った最初の非同期プログラミングとして、秒数とメッセージを指定し、その秒数が経過した後にメッセージをコンソールに表示する非同期メソッドを作成してみます。
次の手順に従い、サンプルコードを実行しましょう。
- ヒエラルキーウィンドウに新しい空オブジェクトを配置
- 以下のC#スクリプトを作成し、オブジェクトにアタッチ
- 実行
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
Debug.Log("Program started...");
LogDelayedMessageAsync(3.0f, "Hello, UniTask1!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask3!").Forget();
}
private async UniTask LogDelayedMessageAsync(float waitTime, string message)
{
await UniTask.Delay(TimeSpan.FromSeconds(waitTime));
Debug.Log(message);
}
}
C#上記の手順に従うと、実行開始から3秒後に3つのメッセージがコンソールへ出力されます。
指定する時間を変えると、メッセージが表示されるタイミングが変化することも確認しておきましょう。
private void Start()
{
Debug.Log("Program started...");
LogDelayedMessageAsync(3.0f, "Hello, UniTask1!").Forget();
LogDelayedMessageAsync(2.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(1.0f, "Hello, UniTask3!").Forget();
}
C#非同期処理は、その実行の完了を待たずに次の処理を開始することができます。
1つ目の例では3つのメッセージが同時に表示されており、2つ目の例では3つのメッセージがプログラム順とは逆順で表示されています。これは、後続のプログラムが前のプログラムの完了を待たずに実行されていることを示しています。
非同期メソッドのメソッド名
内部で非同期処理を行う非同期メソッドの名前について、その末尾に”Async”を付けて明示的に非同期メソッドであることを示すことは、よくある慣習です。
UniTask構造体
UniTaskにおいて非同期メソッドを実装するときは、基本的にはその返り値の型をUniTask
とします。UniTask
はC#標準のTask
をUnity向けに最適化したもので、Cysharp.Threading.Tasks
名前空間で定義されています。
非同期メソッド、つまり非同期処理は、その完了を待たずに次の処理を開始できることが特徴ですが、UniTask
をメソッドの返り値の型とすることで、その非同期メソッドの完了を待機することもできます。そして、返り値の型がUniTask
であるような非同期メソッドのことを、ここでは単にUniTaskと呼びます。
ここまでで、UniTaskといったときに、文脈によって以下の三つの意味に分かれることに注意してください。
- ライブラリとしてのUniTask
- 返り値の型としての
UniTask
- 非同期処理の一種としてのUniTask
サンプルコードで使用されているUniTaskは、LogDelayedMessageAsync
とUniTask.Delay
の2つです。後者はライブラリのUniTaskによって提供されている、指定した時間だけの遅延を挿入するUniTaskです。前述の通り、UniTaskは完了を待機することも、待機せずに次の処理を実行することもできますが、サンプルプログラムは、前者の呼び出しに関しては完了を待機せず、後者の呼び出しに関しては完了を待機するようにしています。
UniTaskと非同期メソッドは等価ではない
UniTaskは非同期メソッドの一種であるといえますが、非同期メソッドならば必ずUniTaskというわけではありません。例えば、UniTask
のもととなっているTask
型を返り値とするメソッドも非同期メソッドになり、void
型が返り値の型であるメソッドも、非同期メソッドになりえます。
ちなみに、void
型の非同期メソッドはTask
型やUniTask
型のそれとは異なり、その完了を待機することができません。
UniTaskの完了を待機しない
UniTaskを呼び出す際、その完了を待機しない場合は呼び出すUniTaskの後ろにForget
を付ける必要があります。これをつけないと、警告が出されます。
Forget
の代わりに、アンダースコア(_
)に対してUniTaskを代入しても良いです。
private void Start()
{
Debug.Log("Program started...");
_ = LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
LogDelayedMessageAsync(2.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(1.0f, "Hello, UniTask3!").Forget();
}
C#UniTaskの完了を待機する
UniTaskの完了を待機するには、await
を使用します。
LogDelayedMessageAsync
の中では、UniTask.Delay
の次にDebug.Log
が呼び出されていますが、UniTask.Delay
をawait
付きで呼び出しているため、その非同期処理が完了するまで、つまり指定の秒数が経過するまでDebug.Log
は呼び出されません。
また、あるメソッドの中でawaitを用いて何かしらの非同期処理の完了を待機する場合、そのメソッドにはasync
を付ける必要があります。
試しに、LogDelayedMessageAsync
の完了を待機してみましょう。サンプルコードのStart
を以下のように変更します。
private async void Start()
{
Debug.Log("Program started...");
await LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
await LogDelayedMessageAsync(2.0f, "Hello, UniTask2!");
await LogDelayedMessageAsync(1.0f, "Hello, UniTask3!");
}
C#await
を内部で使用しているため、Start
にはasync
が付与されています。
実行結果は1つ目の例、2つ目の例とは異なり、メッセージがプログラム順の通りに表示されるようになります。これは、各LogDelayedMessageAsync
の呼び出しについて、その完了がawait
によって待機されるようになったからです。
await可能なオブジェクト
UniTaskの完了をawait
で待機できるのは、UniTask
型に対して、Awaiterと呼ばれるオブジェクトを返すためのGetAwaiter
メソッドが実装されているからです。Awaiter
とGetAwaiter
はC#の機能であるため、UniTaskオブジェクト以外でも、このメソッドを実装するオブジェクトはawait
によってその完了を待つことができます。
UniTaskは様々なオブジェクトに対してのAwaiter
およびGetAwaiter
を提供しているため、UniTaskオブジェクト以外にも、いろいろなオブジェクトをawaitすることが可能です。例えば、アニメーションをスクリプトベースで作成するためのライブラリであるDOTweenとの連携は非常に強力です。
変数を介したawait
UniTaskは、一度変数に格納してから、その完了を待機することもできます。以下のコードを試してみましょう。
private async void Start()
{
Debug.Log("Program started...");
var task1 = LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
var task2 = LogDelayedMessageAsync(2.0f, "Hello, UniTask2!");
var task3 = LogDelayedMessageAsync(1.0f, "Hello, UniTask3!");
await task1;
await task2;
await task3;
}
C#この例ではawaitで完了を待機しているのにもかかわらず、 前セクションの例とは異なり、メッセージの表示はプログラム順でなくなってしまいました。
これは、変数にUniTaskを代入した時点でその実行が開始されてしまっているからです。基本的に、UniTaskの実行が開始されるのはそれをawait
した時点ではなく、メソッドを呼び出した時点です。await
の役割は、あくまでawaitされているUniTaskが完了するまで待機させることだけです。
メソッドの呼び出しと同時にその完了を待機したい場合は直接メソッド呼び出しをawaitし、メソッド呼び出しとその完了を待機するタイミングをずらしたい場合は、変数を介してawaitするとよいでしょう。
awaitは1度まで
1つのUniTaskは、基本的に1度までしかawaitできません。以下のようなコードはエラーとなります。どうしても同一のUniTaskを複数回awaitする必要がある場合は、Preserve
という特別なメソッドを使用する必要があります。
var task1 = LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
await task1;
await task1;
C#await時点で初めて実行する
UniTask.Defer
を使用すると、UniTaskがawaitされるまで、その実行タイミングを遅らせることができます。以下のコードは、前セクションの例と同様の結果になります。ちなみに、複数回awaitしたい場合はUniTask.Lazy
というメソッドを使用します。
private async void Start()
{
Debug.Log("Program started...");
var task1 = UniTask.Defer(() => LogDelayedMessageAsync(3.0f, "Hello, UniTask1!"));
var task2 = UniTask.Defer(() => LogDelayedMessageAsync(2.0f, "Hello, UniTask2!"));
var task3 = UniTask.Defer(() => LogDelayedMessageAsync(1.0f, "Hello, UniTask3!"));
await task1;
await task2;
await task3;
}
C#UniTaskVoid
あるUniTaskについて、そのUniTaskがawaitされることがない場合、より軽量なUniTaskVoid
を使用することができます。例えば、次のコードは正常に動作し、 初めの例と同じ結果になります。返り値の型をUniTaskVoid
とした場合、呼び出し側でawaitすることはできないことに注意してください。
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private void Start()
{
Debug.Log("Program started...");
LogDelayedMessageAsync(3.0f, "Hello, UniTask1!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask2!").Forget();
LogDelayedMessageAsync(3.0f, "Hello, UniTask3!").Forget();
}
private async UniTaskVoid LogDelayedMessageAsync(float waitTime, string message)
{
await UniTask.Delay(TimeSpan.FromSeconds(waitTime));
Debug.Log(message);
}
}
C#Start
メソッド内で何かしらの非同期処理をawaitする場合、返り値の型はUniTaskVoid
とすることができます。例えば、以下のコードは、返り値をUniTaskとした場合と同じ結果になります。
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
await LogDelayedMessageAsync(3.0f, "Hello, UniTask1!");
await LogDelayedMessageAsync(2.0f, "Hello, UniTask2!");
await LogDelayedMessageAsync(1.0f, "Hello, UniTask3!");
}
C#UniTaskVoid
は、void
のUniTask版であるといえます。
返り値のあるUniTask
遅延付き加算器の実装
前章では、指定した秒数だけ遅延をさせてメッセージを表示させるUniTaskを実装しました。このUniTaskは、呼び出しとその完了の待機ができるだけで、何も値を返しません。
この章では、返り値のあるUniTaskを扱います。
例として、遅延付きの加算メソッドを実装します。以下のスクリプトを作成し、実行してみましょう。
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var result = await AddNumbersAsync(1, 2);
Debug.Log($"Result: {result}");
}
private async UniTask<int> AddNumbersAsync(int n1, int n2)
{
await UniTask.Delay(TimeSpan.FromSeconds(2.0f));
return n1 + n2;
}
}
C#AddNumbersAsync
の完了には数秒かかり、その後はパラメーターとして渡した二つの整数が足された結果が取得できていることがわかります。
UniTask<T>ジェネリック構造体
返り値のあるUniTaskを扱うには、メソッドの返り値の型をUniTask<T>
とします。T
には、返り値の型を指定します。
async
とawait
の扱いは、返り値のないUniTaskと同様です。メソッド内でawait
を使用する場合、そのメソッドにはasync
を付ける必要があります。
返り値は、呼び出したUniTaskにawait
を付けて変数に代入することで受け取ることができます。
返り値のないUniTaskの扱いを理解できていれば、返り値のあるUniTaskの扱いはそう難しくはありません。UniTask<T>
は実行結果を受け取れるというだけで、処理が実行されるタイミングや、await
の回数に関する制約などは同じです。
変数を介したawait
返り値のないUniTask同様、返り値のあるUniTaskも変数を介してその完了を待機することが可能です。次のサンプルコードは、先ほどの例 と同じ結果になります。
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var task = AddNumbersAsync(1, 2);
var result = await task;
Debug.Log($"Result: {result}");
}
C#返り値のあるUniTaskの開始タイミングを遅らせる
AddNumberAsync
は、task
に代入した時点で処理が開始されています。awaitされるまで処理の開始を遅らせたい場合は、ジェネリック版のUniTask.Defer
であるUniTask.Defer<T>
を使用します。(UniTask.Lazy<T>
も使えます。)
演習
名前付きタイマー
整数で秒数を指定し、その分だけカウントを行うタイマーを作りましょう。1秒ごとに、残り秒数とタイマー名をコンソールに表示させてください。
以下のテンプレートを使用します。
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var timerA = StartTimerAsync("Timer A", 3);
var timerB = StartTimerAsync("Timer B", 5);
await timerA;
await timerB;
Debug.Log("Program finished!");
}
private async UniTask StartTimerAsync(string name, int seconds)
{
}
}
C#n入力遅延付き加算器
任意の数の小数を引数にとり、それらをすべて足した結果を返す加算器を作りましょう。ただし、計算には引数として与えた小数1つごとに1秒の遅延がかかるものとします。
以下のテンプレートを使用します。
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class SampleManager : MonoBehaviour
{
private async UniTaskVoid Start()
{
Debug.Log("Program started...");
var result1 = await SumAsync(new float[] { 1.6f, 5.4f, 3.0f });
Debug.Log($"Result1: {result1}");
var result2 = await SumAsync(new float[] { 2.2f, -5.3f, 2.5f, -7.9f, -6.5f });
Debug.Log($"Result2: {result2}");
Debug.Log("Program finished!");
}
private async UniTask<float> SumAsync(IEnumerable<float> numbers)
{
}
}
C#解答例
名前付きタイマー
private async UniTask StartTimerAsync(string name, int seconds)
{
Debug.Log($"{name} started...");
while (seconds > 0)
{
Debug.Log($"{name}: {seconds} sec.");
seconds--;
await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
}
Debug.Log($"{name} finished!");
}
C#n入力遅延付き加算器
private async UniTask<float> SumAsync(IEnumerable<float> numbers)
{
await UniTask.Delay(TimeSpan.FromSeconds(1.0f * numbers.Count()));
return numbers.Sum();
}
C#
コメント