はじめに
Task、async/awaitの話をする際、非同期とスレッドを切り分けて説明した方がスッキリします。
ここでは非同期に絞って紹介します。
unity 2018.1以降、C#6が正式にサポートされるようです。
Updated scripting runtime in Unity 2018.1: What does the future hold? – Unity Blog
業務ではUnity2017を使用していますが、一足先にasync/awaitを使っています。
unity2017で async/awaitを使うには PlayerSettings > Scripting Runtime Version を .NET 4.6 Equivalent にする必要があります。
これからの説明で実行する処理
ゲーム制作でありそうな例です。
- 読み込み中の表示を始める
- masterデータを読む
- masterデータを元に3Dモデルを読む
- userデータとマップデータを同時に読む
- 読み込み中の表示を終了する
これらを上から順に実行します。
callbackを使った非同期
async/awaitもCoroutineも使わない場合、callbackを使う事になります。
下記のようにネストが深くなりがちです。
ネストを嫌ってメソッドを分割すると、今度は実行順と記述順がバラバラになり、順番の把握が難しくなります。
callbackが大量に連鎖する場合、callback地獄と呼ばれるような事になり、非常に読みづらくメンテしにくいコードになってしまいます。
private void Test(Action onEnd)
{
// 1. 読み込み中の表示を始める
ShowLoader(
// 2. masterデータを読む
LoadMasterData((res) =>
{
// 3. masterデータを元に3Dモデルを読む
Load3DModel(res, () =>
{
// 4. userデータとマップデータを同時に読む
LoadUserDataAndMapData(() =>
{
// 5. 読み込み中の表示を終了する
HideLoader(onEnd);
});
});
});
);
}
private void LoadUserDataAndMapData(Action callback)
{
// ... ユーザーデータとマップデータを同時に読む処理。両方終わったらcallback実行
}
async/await で同期のように非同期を書く
このように記述できます。
private async void Test()
{
// 1. 読み込み中の表示を始める
await ShowLoaderAsync();
// 2. masterデータを読む
int res = await LoadMasterDataAsync();
// 3. masterデータを元に3Dモデルを読む
await Load3DModelAsync(res);
// 4. userデータとマップデータを同時に読む
await Task.WhenAll(new Task[]{LoadUserDataAsync(), LoadMapDataAsync()});
// 5. 読み込み中の表示を終了する
await HideLoaderAsync();
}
記述した順に実行されるため、非常に読みやすいものになります。
コールバック関数もなく、ネストも必要ありません。
Coroutineの置き換え
同期風に書くのはCoroutineでも可能です。
Coroutineにも色々な使い方があると思いますが、ここで扱うのは同期風に書く用途でのCoroutineの話です。
しかしCoroutineには下記のような欠点があります。
Coroutineの欠点
-
値を返すのが面倒
- 型変換をして取得する事は可能
- または、参照を渡す等
-
ry-catch内にyieldを入れることはできないので、例外を処理することが出来ない
-
例外が発生した場合、スタックトレースは例外がスローされたCoroutineを通知するだけ
-
何もしなくても、yield returnしたら 1フレーム待つ
private IEnumerator CoroutineTest()
{
UnityEngine.Debug.Log("Coroutine before: " + Time.frameCount);
yield return StartCoroutine(CoroutineA());
// 1フレーム経過している
UnityEngine.Debug.Log("Coroutine after: " + Time.frameCount);
}
private IEnumerator CoroutineA()
{
if (false)
{
yield return null;
}
}
- delegateで実行出来ない
// NG. deleagte内ではyield return出来ない
Func<IEnumerator> callback = () =>
{
yield return null;
};
StartCoroutine(callback());
- callback形式のメソッドをCoroutineに対応させるのが面倒
async/awaitは上記をカバーできます。
Coroutineをasync/awaitに対応させる
Unity3dAsyncAwaitUtilというユーティリティが公開されています。
https://github.com/svermeulen/Unity3dAsyncAwaitUtil
async Task RunMultipleThreadsTestAsyncWait()
{
UnityEngine.Debug.Log("RunMultipleThreadsTestAsyncWait1");
await new WaitForSeconds(1.0f);
UnityEngine.Debug.Log("RunMultipleThreadsTestAsyncWait2");
}
yield return する箇所を、そのまま awaitで置き換え可能です。
これにより、過去に作成したメソッドをそのまま活用でき、置き換えが容易になります。
既存クラスをasycn/awaitに対応させる
DOTweenでやってみる
拡張メソッドを使い、既存クラスも簡単にawaitに対応させる事が可能です。
非同期メソッドの内部実装 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
下記拡張メソッドを用意します。
// TweenerをAwaitableにする
public static TaskAwaiter<bool> GetAwaiter(this Tweener tweener)
{
var tcs = new TaskCompletionSource<bool>();
TweenCallback callback = null;
callback = () =>
{
tweener.onComplete -= callback;
tcs.SetResult(true);
};
tweener.onComplete += callback;
return tcs.Task.GetAwaiter();
}
// SequenceをAwaitableにする
public static TaskAwaiter<bool> GetAwaiter(this Sequence seq)
{
var tcs = new TaskCompletionSource<bool>();
TweenCallback callback = null;
callback = () =>
{
seq.onComplete -= callback;
tcs.SetResult(true);
};
seq.onComplete += callback;
return tcs.Task.GetAwaiter();
}
これにより
await image.rectTransform.DOScale(new Vector3(0, 1, 1), 1f);
var seq = DOTween.Sequence();
seq.Append(_img.DOFade(1, 1f));
seq.Append(_img.DOColor(Color.black, 0.5f));
seq.Append(_img.rectTransform.DOScale(new Vector3(2, 2, 2), 0.5f));
seq.Append(_img.rectTransform.DOScale(new Vector3(1, 1, 1), 0.5f));
seq.OnComplete(() =>
{
Debug.Log("DOTween.Sequence end in OnComplete");
});
await seq;
と記述できるようになります。
(実際はTweenerとSequence同時に使うとコンパイラのwarningが出てしまうため、もうちょっと工夫する必要があります。)
注意点
Taskを返しているのに awaitしていない場合、処理中に例外が起きてもunity editorのlogに出力されません。
// log出ない例
private async Task ExceptionTest()
{
Image img = null;
img.gameObject.SetActive(true);
}
ExceptionTest();
awaitしない場合、awaitするか、戻り値を async void にする必要があります。
// log出る
private async void ExceptionTest()
{
Image img = null;
img.gameObject.SetActive(true);
}
ExceptionTest();
これはコンパイラがwarningで指摘してくれます。
[CS4014] The statement is not awaited and execution of current method continues before the call is completed. Consider using `await' operator or calling `Wait' method
Destroy・非アクティブにされてもCoroutineのように止まらない
これは良し悪しありますが、Coroutineと同じように止めたい場合は面倒です。
他の気になる点
デバッグ
C# Riderを使用していますが、問題なくブレークポイントを張りデバッグ出来ています。
終わりに
他にはチュートリアルなど、順に実行されるものなどが楽に記述できそうな予感がします。
参考URL
下記blog記事を非常に参考にしています。この記事の半分は下記blogの紹介です。
How to use Async-Await instead of coroutines in Unity3d 2017 | Steve Vermeulen
その他参考URL
[雑記] 非同期制御フロー - C# によるプログラミング入門 | ++C++; // 未確認飛行 C