vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Unityでasync / await

はじめに

Unity2017以降、C#6の機能を使うことができるようになりました。

今回のテーマである async / await はモバイル環境だとまだ課題があるようですが、
とにかく触ってみることにしました。というお話。

準備

Unity2017.1.1f1 時点では、 C#6 (というか .net 4.6)はデフォルトでは有効になっておらず、
Experimental という扱いになっています。

有効にするには、 Player Settings > Other Settings > Configuration > Scripting Runtime Version を、
Experimental(.Net 4.6 Equivalent) に切り替えて、 Unity Editor を再起動します。

非対応

…とここまでワクテカしながら読み進めてきた方がいましたら残念なお知らせなのですが、
Unity のクラス(というか MonoBehaviour を継承しているクラス)のメソッドは、
今まで通り MainThread 以外からの実行はできません。

後述しますが、 SendMessage や ExecuteEvents.Execute も、定義しているクラス自体は MonoBehaviour を継承していないものの、
別 Thread から実行するとエラーになります。
(おそらく最後、指定したメソッドを実行するところでエラーになっているのではないかと思います)

ということで、ファイルの読み込みや HttpClient など、時間のかかる、かつ C# の機能を使って行われる処理に対して使うことができる状況です。

なお UniRx と組み合わせるとより便利になりそうではあるのですが、
今回はそのまま使ってみることにします。

とにかくやってみる

ということでまずは使ってみましょう。

なお async / await / Task の使い方は参照のリンク先におまかせします(丸投げ)。

using UnityEngine;
using UnityEngine.UI;
using System.Threading;
using System.Threading.Tasks;

public class MainController : MonoBehaviour {
    public Text ResultText;
    private void Start ()
    {
        CallAsyncMethod();

    // CallAsyncMethod() を呼び終わったら(処理の完了を待たずに)実行される.
        Debug.Log("Start()");
    }

    private async void CallAsyncMethod()
    {
        var myTask = await GenerateTextAsync();
        // GenerateTextAsync() の処理が完了したら実行される.
        Debug.Log(myTask);
    }

    private async Task< string> GenerateTextAsync()
    {
    // 非同期処理を定義して返す.        
        return await Task.Run< string>( () => {
            // 三秒間だけ待ってやる.
            Thread.Sleep( 3000 );
            const string sampleText = "Hello\nworld\nおはようございます!\n";

            Debug.Log("GenerateTextAsync()");
            return sampleText;
        } );
    }
}
  • Task を返す GenerateTextAsync() を呼び出すとき、 await をつけ忘れて myTask.Result とかしてしまうと、
    Unity Editor がフリーズするので気をつけましょう。
  • Debug.Log が実行される順番は 1. Start() 2. GenerateTextAsync() 3. myTask ( GenerateTextAsync() の戻り値)です。
  • Debug.Log は Unity の処理ですが、別 Thread から呼び出しても特にエラーにはなりません。

async の Unity 定義メソッドへの適用

await をつけて GenerateTextAsync() を呼ぶためには、そのメソッドに async をつける必要があります。

これは Start() など Unity のメソッドにも適用できます。

ただし、非同期処理を呼んだ後の処理は(例: CallAsyncMethod() の Debug.Log() )、それが完了するまで呼ばれないため、
上記のように別のメソッドに切り分けたほうが良いと思います。

TaskからMainThreadを呼ぶ

さて、前述の通り、 GenerateTextAsync() の Task.Run< string> の中で ResultText (UnityEngine.UI.Text) のテキストを変更することはできません。

Task からこのような処理を行うにはどうすれば良いでしょうか。

Androidには別 Thread から UIThread を呼び出す方法があります。

mainActivity.runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // MainThread での処理.
    }
});

これと同様の仕組みが、 C# にも存在します。

using UnityEngine;
using UnityEngine.UI;
using System.Threading;
using System.Threading.Tasks;

public class MainController : MonoBehaviour {
    public Text ResultText;
    private void Start ()
    {
        CallAsyncMethod();
  }

    private async void CallAsyncMethod()
    {
        var myTask = await GenerateTextAsync();
    }

    private async Task< string> GenerateTextAsync()
    {
    
    // MainThreadのコンテキストを取得.
        var context = SynchronizationContext.Current;
    

        return await Task.Run< string>( () => {
            Thread.Sleep( 3000 );
            const string sampleText = "Hello\nworld\nおはようございます!\n";


      // MainThreadのコンテキストを通して、Textを変更する.
            context.Post((state) =>
            {
                ResultText.text = sampleText;
            }, null);
      
            return sampleText;
        } );
    }
}

SynchronizationContext を使って MainThread のコンテキストを取得することで、
別 Thread から Unity の( MainThread からの実行が必要な)処理を実行することができます。

おわりに

C#6 の機能が使えるようになったことで、細かい部分でも色々便利になって良いですね☺

async / await は、将来的に Unity の処理にも使うことができるようになるのかはわかりませんが、
時間のかかる処理を行う場合には是非とも活用していきたいところです。

とりあえず非同期処理のかんたんなところに触れてみましたが、
もう少し突っ込んで触ってみたくもあります。

ということで、次回に続く。。。かもしれない。

参照