UniTaskの実装から見る
使い方と注意点
自己紹介つっきー(Tukky)
趣味
ロードバイクでツーリング(一緒に走れるチームを探してます!!)
Zbrushでモデリング
VRのゲーム作り
仕事
組み込み系技術者 設計・開発
iPhone, Androidを車載に繋げる部分を作ってます
認証サーバー開発
今度 AWS DevDay Tokyo 2018で登壇予定です
https://aws.amazon.com/jp/aws-devday-tokyo-2018/
Twitter
https://twitter.com/tukkyCG1
アジェンダUniTaskについて
UniTaskでHelloWorldの実装
UniTaskの動作に関する問題
デバッグ方法の説明
実装上の注意
Eventを取り扱う (UniRx.Asyncの拡張メソッド)
EventLoopを取り扱う (UniRx.Asyncの拡張メソッド)
Unity非同期の知識について
Unity 非同期完全に理解した勉強会で完全に理解できなかたったので、UniTask
を中心にコードを読み 8割がた非同期を理解したつもりになってます。
この資料自体が上記の勉強会Unityでasync/awaitを使うための方法をしり、独
学で学んだ内容になります。
そのため間違った内容があればどんどん指摘してください

なお、非同期はUniRxから入ったのでコルーチンとの比較には

あえて触れてません、というか説明できませんすいません
Unity 非同期完全に理解した勉強会 : https://connpass.com/event/95696/
UniTaskとは
超究極パフォーマンスのUnityのasync/await統合を提供するライブラリ
標準のSystem.Threading.Tasks.Taskより、かなり軽量
非同期遅延初期化(AsyncLazy)的な使い方ができる
AsyncOperationをawaitできる
例:UnityWebRequest.SendRequestをawaitできる
https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/Async/UnityAsyncExtensions.cs
UnityEventをawaitできる
FixUpdateなどのタイミングをtriggerにawaitできる
https://github.com/neuecc/UniRx/blob/master/Assets/Plugins/UniRx/Scripts/Async/Triggers/AsyncTriggerExtensions.cs
などなど
参照 : http://neue.cc/2018/07/12_567.html
: https://qiita.com/toRisouP/items/4445b6b9bf00e49eb147
UniTaskの導入方法
UniRxをAssetStoreもしくはGithubからDownloadしてAssetとしてUnityに読み込ませる
AssetStore : https://assetstore.unity.com/packages/tools/integration/unirx-reactive-
extensions-for-unity-17276
Github : https://github.com/neuecc/UniRx
Unity 2018.3 以前はC# 7.0を有効にする必要がある
Player Settingsから動作モードを.NET 4.xに切り替える
Incremental Compilerを導入する
Unity 2018.3 以降はそのまま使える (お試しするならUnityHubがおすすめ)
そもそもUniTaskってどんな時使えるの?
参照:https://www.slideshare.net/neuecc/deep-dive-asyncawait-in-unity-with-unitaskunirxasync/11
一回限りの非同期処理を実行
するときに威力を発揮する
個人的には
IObservable<T>のようにイベ
ントを処理するときも使いや
すい
ただし、キャンセルが少し面
倒
標準のTaskはいちいち
ThreadPoolで動作するので
コストが高い
UniTaskの “Hello World”
using UnityEngine;

using UniRx.Async;



public class HelloUniTask : MonoBehaviour

{

private async void Start()

{

var hello = await AsyncHello();

Debug.Log(hello);

}



private async UniTask<string> AsyncHello()

{

await UniTask.Delay(1000);

return "Hello UniTask";

}

}



UnityEngine.NetWorkingをAwaitするusing UnityEngine;

using UniRx.Async;

using UnityEngine.Networking;



using System;

using System.Threading;



public class Demo : MonoBehaviour

{

private async void Start()

{

await AsyncTextLoad("http://sample.com/sample.txt");

Debug.Log("Downloading is finished...");

}



private async UniTask<string> AsyncTextLoad(string url, CancellationToken ct = default)

{

using (var req = UnityWebRequest.Get(url))

{

await req.SendWebRequest().ConfigureAwait(cancellation: ct);



if(!string.IsNullOrEmpty(req.error))

throw new Exception($"{ url} : { req.responseCode} -- { req.error}");



return req.downloadHandler.text;

}

}

}

UniRx.Asyncで拡張メソッドが定義さ
れている。
今回は詳しく触れないですが Awaitable
になるようにI/Fが定義されている
問題using UniRx.Async;
public class Sample1 : MonoBehaviour

{

private CancellationTokenSource cts;



private async void Awake()

{

await UniTask.Delay(1000); // 1000ms待機する
// A

Debug.Log($"Start {Time.realtimeSinceStartup}");

}
private async void Update()

{

// B
Debug.Log($"Start {Time.realtimeSinceStartup}");

}


AとB何方が先に
呼ばれるでしょうか?
なぜ B の方が先に
よばれるのでしょうか?
解説using UniRx.Async;
public class Sample1 : MonoBehaviour

{

private CancellationTokenSource cts;



private async void Awake()

{

await UniTask.Delay(1000); // 1000ms待機する
// A

Debug.Log($"Start {Time.realtimeSinceStartup}");

}
private async void Update()

{

// B
Debug.Log($"Start {Time.realtimeSinceStartup}");

}


ここまではAwakeの関数として
同期的に呼ばれる
UniTask.Delay(1000)がセット
されてから1000ms経過しているか
確認する、経過していなければ
経過を確認する関数(MoveNext)をPlayerloopにセットして
Awakeから抜ける
次のUpdateのタイミングで再び確認する経過
していなければもう一度経過を確認する関数をセットする
以降繰り返し
1000ms後はじめてAが呼ばれる
Aは1000ms後
PlayerLoopに登録された関数
によって呼ばれる
PlayerLoopとは
MonoBehaviourにUpdateやLateUpdateおなじみのイベント以外の任意のイベントを追加したり、
UpdateやLateUpdateを呼び出さないようにしたりできる。
詳しくは http://tsubakit1.hateblo.jp/entry/2018/04/17/233000
Update
FixUpdate
LateUpdate
Update
FixUpdate
LateUpdate
Update
FixUpdate LateUpdate
1000ms経過したか確認
全てのイベントは同じス
レッドで実行される
awaitの部分が別スレッ
ドになるとは限らない
実際に動作を見て見よう
Debug.Logで関数のCallStackを見る
VisualStudioでUnity Editerのプロセスにアタッチして見る
UniTask Trackerを使う
実際に見て見よう!!
陥りそうな罠using UnityEngine;

using UniRx.Async;

using UnityEngine.Networking;



using System;

using System.Threading;



public class Demo : MonoBehaviour

{

private async void Update()

{

await AsyncTextLoad("http://sample.com/sample.txt");

Debug.Log("Downloading is finished...");

}



private async UniTask<string> AsyncTextLoad(string url, CancellationToken ct = default)

{

using (var req = UnityWebRequest.Get(url))

{

await req.SendWebRequest().ConfigureAwait(cancellation: ct);



if(!string.IsNullOrEmpty(req.error))

throw new Exception($"{ url} : { req.responseCode} -- { req.error}");



return req.downloadHandler.text;

}

}

}

Updateの動作自体をStopしないので
何回もAsyncTextLoadが呼ばれることになる。
UniTask Trackerを使うことをおすすめします
陥りそうな罠2
using System;

using System.Threading;

using UnityEngine;

using UnityEngine.Networking;

using UnityEngine.UI;

using UniRx;

using UniRx.Async;



public class Demo2 : MonoBehaviour

{

[SerializeField]

private Text _text;



[SerializeField]

private Button _button;



private string loadedText;



private async void Start()

{

_button.OnClickAsObservable().Subscribe(_ => _text.text = loadedText).AddTo(gameObject);

loadedText = await AsyncTextLoad("http://sample.com/sample.txt");

}



private async UniTask<string> AsyncTextLoad(string url, CancellationToken ct = default)

{

using (var req = UnityWebRequest.Get(url))

{

await req.SendWebRequest().ConfigureAwait(cancellation: ct);



if (!string.IsNullOrEmpty(req.error))

throw new Exception($"{ url} : { req.responseCode} -- { req.error}");



return req.downloadHandler.text;

}

}

}







loadが完了する前にSubscribeすると
nullを参照する可能性がある
CallBack, IObservable, UniTask使い分け
UnityEvent編(Button.onClick)
非同期のイベントが複数回発生することから
ベストプラクティスに従うとIObserbableで実装すべき
UniTaskのUnityEvent用の拡張を使うことでUniTaskでもイベントを管理できる
CallBackはEvent listener管理しなくてはいけないのであまりおすすめしない
個人的にはUniTaskを使うことをおすすめ、 むしろIObservableを一切つかう
ことがなくなった。(だだし、Cancelは面倒 慣れると部分的にキャンセルとか
できてむしろわかりやすいとおもうのですが…)
EventのAwaitを少しだけ説明
JavaScriptのPromiseのような実装になっている
呼び出し元(await)ではTrySetResultが呼ばれるまで待機する
非同期を実行する側 TrySetResultを呼び出す
例 UniTask.Yield(PlayerLoopTiming.FixedUpdate) なら呼び出
し元のGameObjectのFixUpdateのタイミングでTrySetResult
→ MoveNextと呼び出し 呼び出し元の続きから呼び出す。
Eventの待機はPlayLoopと同期しない (一部例あり)
CallBack関数で実装using UnityEngine;

using UnityEngine.Networking;

using UnityEngine.UI;

using System;

using System.Threading;

using UniRx.Async;



public class CBUnityEvent: MonoBehaviour

{

[SerializeField]

private Button _button;



private async void Start()

{

var loadedText = await AsyncTextLoad("http://sample.com/sample.txt");

_button.onClick.AddListener(() => {

Debug.Log(loadedText);

});

}

}



IObservableで実装using System;

using System.Threading;

using UnityEngine;

using UnityEngine.Networking;

using UnityEngine.UI;

using UniRx;

using UniRx.Async;



public class IObservableUnityEvent : MonoBehaviour

{

[SerializeField]

private Button _button;



private async void Start()

{

loadedText = await AsyncTextLoad(“http://sample.com/sample.txt");
_button.OnClickAsObservable().Subscribe(_ => Debug.Log(loadedText)).AddTo(gameObject);



}



}









buttonのイベントをIObservableに変換
Subscribeを発行してイベントの発行を待つ
呼び出し順番に注意
UniTaskで実装
using UnityEngine;

using UnityEngine.Networking;

using UnityEngine.UI;

using System;

using System.Threading;

using UniRx.Async;



public class CBUnityEvent: MonoBehaviour

{

[SerializeField]

private Button _button;



private string loadedText;

private CancellationTokenSource cts;



private void Start()

{

cts = new CancellationTokenSource();

RunAsync(cts.Token).Forget();

}



private async UniTask RunAsync(CancellationToken ct)

{

loadedText = await AsyncTextLoad("http://sample.com/sample.txt", ct);

await AsyncOnClick(ct);

}



private async UniTask AsyncOnClick(CancellationToken ct = default)

{

bool isCancel;

while(true){

isCancel = await _button.OnClickAsync().SuppressCancellationThrow();

if (isCancel)

break;

Debug.Log(loadedText);

}

return;

}



private void OnDestroy()

{

cts.Cancel();

cts.Dispose();

}

}



一見行数が増えて、一番複雑な
実装に見える
ただ、全て上から下に流れるように
なっているため、同期で書いたソースと
非同期での記述に差がなくて見やすい
非同期遅延初期化を

違和感なく追加することができる
部分的キャンセルや全体キャンセルを
実装することができる。
反論は認めます(汗)
引数として渡しておく
部分キャンセルする場合は
CancellationTokenSource.CreateLinkedTokenSource
トークンを作成すると便利
ルートとなるTokenSourceをonDestroyで呼び出し
CancelとDisposeを呼び出せば
キャンセル漏れが発生しない
asyncのついていない
methodから呼び出すことが
できる
awaitが呼び出し元で実行さ
れても
完了を待たない
LazyLoadの実装
public class UniTask: MonoBehaviour

{

public UniTask<string> lazyTextLoad { get; private set; }



private void Awake()

{

cts = new CancellationTokenSource();

lazyTextLoad = AsyncTextLoad("https://google.com", cts.Token);

lazyTextLoad.Forget(); //ここでロードしておいて

}

private void Start()

{

RunAsync(cts.Token).Forget();

}



private async UniTask RunAsync(CancellationToken ct)

{

List<UniTask> tasks = default;

tasks.Add(AsyncOnClick(ct));

await UniTask.WhenAll(tasks.ToArray()); // 任意タスクを追加できる

}



private async UniTask AsyncOnClick(CancellationToken ct = default)

{



bool isCancel;

var button_cts = CancellationTokenSource.CreateLinkedTokenSource(ct); 

while(true){

isCancel = await _button.OnClickAsync(button_cts.Token).SuppressCancellationThrow(); //button_cts, ctsの何方でもキャンセルできる。

if (isCancel)

break;

var (lazyLoadCancel, loadedText) = await lazyTextLoad.SuppressCancellationThrow(); //ロードが完了していれば キャッシュされた値をそのまま出力する。

Debug.Log(loadedText);

}

return;

}

}





UniRxで例えると PublishLastとConnectを
読んでIObservableをHot変換しておくようなもの
リソース読み込み後
GameObjectのタスクをどんどん詰め込む
WhenAllは全てのUniTaskが終了するまで待機する
リソースの読み込みが終わってなければここで待機する
終わっていれば結果をキャッシュから読み込む
CallBack, IObservable, UniTask使い分け
EventLoop編(FixUpdate)
非同期のイベントが複数回発生することから
ベストプラクティスに従うとIObserbableで実装すべき
UniTaskのUnityEvent用の拡張を使うことでUniTaskでもイベントを管理できる
CallBackはLoad完了のイベントを管理しなくてはいけないのであまりおすす
めしない(陥りそうな罠を参照)
個人的にはUniTaskを使うことをおすすめ、EventLoopの中でさらにawaitして
も問題ない。
UniRx.Async.TriggersでFixUpdateを待つ
using UnityEngine;

using UniRx;

using UniRx.Triggers;

using UniRx.Async;

using UniRx.Async.Triggers;

using System.Threading;



public class FixUpdateTest : MonoBehaviour

{

private CancellationTokenSource cts;



private void Awake()

{

cts = new CancellationTokenSource();

FixUpdateAsync(cts.Token).Forget();

// IObservableでも同じことができる

// Subscribeで非同期関数を呼び出す時は注意

// this.FixedUpdateAsObservable().Subscribe(_ => Debug.Log(Time.realtimeSinceStartup)).AddTo(gameObject);

}



private async UniTask FixUpdateAsync(CancellationToken ct = default)

{

var fixUpdateTrigger = this.GetAsyncFixedUpdateTrigger();

while (!await fixUpdateTrigger.FixedUpdateAsync(ct).SuppressCancellationThrow())

{

// ここでまっても複数回呼ばれることはない

// await UniTask.Delay(200, delayTiming: PlayerLoopTiming.FixedUpdate);

var sinceStartup = Time.realtimeSinceStartup;

Debug.Log($"a :{ sinceStartup} :: {Time.deltaTime}");

}

}

void OnDestroy()

{

cts.Cancel();

cts.Dispose();

}

}

まとめ
今回のLT?でUniTaskの魅力について伝われば幸いです。
UniTaskをどんどん使っていきましょう。
今回はキャンセルとPlayerloopTimingについて詳しく伝えれま
せんでした、次回の機会があれば、キャンセルについて少し話
したいと思います。

Weeyble async 181009_tukky