はじめに
ついにUnity2023.1よりUnity公式版UniTask
が出ました。(結構語弊がありそうだか...)
ただ現段階ではUniTask
と同等・もしくはそれ以上な機能を持っているわけではなく、軽く触った限りはまだまだAPIが足りず発展途上かなといった感じです。
しかし段々と使いやすくなるのは間違いないでしょうし、外部ライブラリをなるべく使いたくない方にとって魅力的なことも確かです。今後利用されるケースも増えるかと思いますので、今のうちから触っておくのも悪くないかと思います。
ちなみに現段階ではしっかりとしたドキュメントはなさそうです...。
環境
Unity2023.1.0
ライフサイクルに紐づいたCancellationToken
MonoBehavior
のライフサイクルと紐づいたCancellationToken
がMonoBehavior
のプロパティとして実装されました。
その名もMonoBehaviour.destroyCancellationToken
です。
docs.unity3d.com
// 重要な箇所以外省略 namespace UnityEngine { public class MonoBehaviour : Behaviour { private CancellationTokenSource m_CancellationTokenSource; /// <summary> /// <para>Cancellation token raised when the MonoBehaviour is destroyed (Read Only).</para> /// </summary> public CancellationToken destroyCancellationToken { get { if (this.m_CancellationTokenSource == null) { this.m_CancellationTokenSource = new CancellationTokenSource(); this.OnCancellationTokenCreated(); } return this.m_CancellationTokenSource.Token; } } } }
MonoBehavior
のDestroy
のタイミングでCancel
されるので、コルーチンのようにGameObject
が破棄されたタイミングでも処理が止まってくれるようになります。(ちゃんとCancellationToken
を扱っていればだが)
public class Sample : MonoBehaviour { private async Awaitable Start() { // 1秒待つ await Awaitable.WaitForSecondsAsync(1, destroyCancellationToken); } }
またPlayMode
終了時(Editor
上での話)もしくはアプリケーション終了時にキャンセルを発行するApplication.exitCancellationToken
なるものも用意されています。
CancellationToken token = Application.exitCancellationToken;
使い方
現在実装されているAwaitable
のstatic
メソッドについて列挙します。
private async Awaitable Start() { // 1秒待つ // Task.Delayと異なり、Time.timeScaleの影響を受ける await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken); // 現在のフレームの最後まで待つ await Awaitable.EndOfFrameAsync(destroyCancellationToken); // 次のFixedUpdateまで待つ await Awaitable.FixedUpdateAsync(destroyCancellationToken); // 次のフレームまで待つ await Awaitable.NextFrameAsync(destroyCancellationToken); // 別スレッドに移動 await Awaitable.BackgroundThreadAsync(); // メインスレッドに移動 await Awaitable.MainThreadAsync(); }
Awaitable.WaitForSecondsAsync
はちゃんとTime.timeScale
の影響を受けてくれるみたいですね。
PlayerLoopの場所について
ここはマニアックなので読み飛ばしてもらって大丈夫です。
PlayerLoopの細かいイベントたちはUniTask
の作者さんがGitHubに載せてくれているので載せておきます。(一部UniTask
のイベントが入っていますが今回は無視してください)
Unityマニアな人は実際PlayerLoop
のどこの箇所で実行されるか気になると思うので、それぞれの処理の前後にDebug.Log
を仕組むように改造してみました。
public struct CustomEvent { } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Init() { var playerLoop = PlayerLoop.GetCurrentPlayerLoop(); Set(ref playerLoop); PlayerLoop.SetPlayerLoop(playerLoop); } private static void Set(ref PlayerLoopSystem playerLoop) { playerLoop.subSystemList ??= Array.Empty<PlayerLoopSystem>(); var type = playerLoop.type; playerLoop = new PlayerLoopSystem { type = playerLoop.type, updateDelegate = playerLoop.updateDelegate, subSystemList = playerLoop.subSystemList .Prepend(new PlayerLoopSystem { type = typeof(CustomEvent), updateDelegate = () => Debug.Log($"{type} : Start"), }) .Append(new PlayerLoopSystem { type = typeof(CustomEvent), updateDelegate = () => Debug.Log($"{type} : End"), }) .ToArray(), updateFunction = playerLoop.updateFunction, loopConditionFunction = playerLoop.loopConditionFunction, }; if (playerLoop.subSystemList.Length == 2) return; for(var i = 0; i < playerLoop.subSystemList.Length; i++) { Set(ref playerLoop.subSystemList[i]); } }
PlayerLoopSystem
が構造体なので少し手間取りましたが、ちゃんと計測できていそうです。
結果としては以下のようになりました。
関数名 | await後が実行されるタイミング |
---|---|
WaitForSecondsAsync | ScriptRunBehaviourUpdate とScriptRunDelayedDynamicFrameRate の間。 |
EndOfFrameAsync | PostLateUpdate の後。つまり本当に最後。 |
FixedUpdateAsync | DirectorFixedUpdatePostPhysics とScriptRunDelayedFixedFrameRate の間。 |
NextFrameAsync | ScriptRunBehaviourUpdate とScriptRunDelayedDynamicFrameRate の間。 |
返り値を持つ場合
Awaitable<T>
がちゃんと存在します。
private async Awaitable Start() { var value = await HogeAsync(destroyCancellationToken); // 1 Debug.Log(value); } private static async Awaitable<int> HogeAsync(CancellationToken token) { return 1; }
エラーの面白い性質
C#
ではOperationCanceledException
というエラーを特殊に扱っています。
具体的には呼び出し元のメソッドがTask
かAwaitable
を返す場合はエラーが出力されるわけではありません。
private async Awaitable Start() { // ログ出力もされず、アプリが止まるわけでもない throw new OperationCanceledException(); } private async Task Start2() { // ログ出力もされず、アプリが止まるわけでもない throw new OperationCanceledException(); } private async void Start3() { // エラーが出力される throw new OperationCanceledException(); } private void Start4() { // エラーが出力される throw new OperationCanceledException(); }