はかせのラボ

私の頭の中を書いていく雑記ブログです

Unity C# async/await Unityの入力を待ってみる

あいさつ

どうも、はかせです。
前回async/awaitの基礎をやりました。
今回はその応用例を出したいと思います。

今回の応用例

ゲーム作ったりしてると「Aボタンが押されるまで処理を待ちたい」みたいな要件が出ると思います。
そしておそらく愚直に作るとコルーチン使ってなんやかんやすると思います。

それがダメとは言いません。ただ別の方法もあるよってことで今回は、
async/await + Taskで入力を待ってみたいと思います。

Taskって?

しれっと出しましたが説明してないやつがいますね。
前回awaitのところでもひょっこりいたやつです。
まずはTaskの説明をサクッとします。

簡単に言うとTaskはタスクです。まんまですね。
ただそれ以上でも以下でもないんです。

前回

await Task.Delay(1000);

こんなコード書きました。
Task.Delay(1000)は1000ミリ秒待機するタスクです。
このタスクの終了をawaitで待っているのが上のコードです。

仕事は終わったら上司に報告しますよね?
Taskは終わったら我々プログラマーに報告してくれます。そういうやつです。

詳しく知りたい方はリファレンスをどうぞ→
Task Class (System.Threading.Tasks) | Microsoft Docs

入力待ち

Taskの説明も終わったところで本題です。
入力待ちというのは「入力されたら処理を終了し報告するタスク」です。
さっきの説明と合わせて何となく何するかわかりましたか?
それではコードです。

 Debug.Log("SecondAsync Start!");
 Debug.Log("Please press spacekey");
 //スペースキーの入力待ち 
 var isInput = false;
 await Task.Run(() => 
{
     while(!input.GetKeyDown(KeyCode.Space)){}
})
 Debug.Log("SecondAsync Done!");

Task.Runは渡された処理を実行するタスクです。
処理としてはスペースキーが押されていない間無限ループさせています。

実は・・・

実はこのコード実行するとこんなんでます。
f:id:hakase0274:20181207195716p:plain

実はUnityのAPIってメインスレッドからしか呼べないんですよねw

解決策

解決策自体はたくさんあります。
今回はSynchronizationContextというものを使ってみます。
SynchronizationContextは簡単に言うと処理をどのスレッドでやるか指定するやつです。
詳しくはリファレンスを
SynchronizationContext Class (System.Threading) | Microsoft Docs

こいつを使ってUnityAPIをメインスレッドでやります。
コードです。

public class AsyncAwaitTest : MonoBehaviour
{
    private SynchronizationContext context;
    // Use this for initialization
    async void Start()
    {
        context = SynchronizationContext.Current;
        async FirstAsync();
        async SecondAsync();
    }

    private async Task FirstAsync()
    {
        Debug.Log("FirstAsync Start!");
        //1秒待つ
        await Task.Delay(1000);
        Debug.Log("FirstAsync Done!");
    }

    private async Task SecondAsync()
    {
        Debug.Log("SecondAsync Start!");
        Debug.Log("Please press spacekey");
        //スペースキーの入力待ち 
      //直接書けないので値を保持するフィールド用意
        var isInput = false;
        await Task.Run(() => 
        {
         //入力されていなければ現在の入力を取得
            while (!isInput)
            {
                context.Post(__ => isInput = Input.GetKeyDown(KeyCode.Space), null);
            }
        } );
        Debug.Log("SecondAsync Done!");
    }

}

実行結果です。
f:id:hakase0274:20181207200945g:plain

無事できました。

SynchronizationContextをメンバで持ちStartで取得する理由ですが、
Unityのメインスレッドで取得したいからです。
ほかのスレッドで取得すると結局別スレッドなのでさっきのエラーが出ます。
(というか別スレッドだとnull入った気がする・・・)

あとがき

今回はasync/await応用例でした。
UnityAPIが絡んでくるとピュアC#のasync/awaitでは冗長になってしまいますね・・・
(UniRx使えば解決なんですが記事にもしてないですしアセットなんで今回は見送りました)

コルーチンと比較してやりやすいのは、
値を返すだとかが絡んできた時ですかね。
次回はその辺書こうかなと思っています。

それでは今回はこの辺でノシ