Natural Software

KinectなどのDepthセンサーを中心に活動しています。最近はHoloLensになりました。

HoloLensのUnity/UWPでasync/awaitを使う

HoloLensはUnityでアプリを作りますが、Unityエディターから直接実行ファイルを生成するのではなく、一度UWPへ変換してVisual Studioでビルド後にHoloLens実機に配置、実行する流れになります。

このため、Unityの.NET 3.5とUWPのコードが共存することになります。特に問題になるのが、UWPではasync/awaitを多用しますが、Unityの環境ではC#のバージョン的にそもそもasync/awaitが使えないため、コードの多くがこれを想定していません。

ここではUnityのアプリでasync/awaitを使う方法について紹介します。またasync/awaitでマルチスレッドになるので、同期の方法についても合わせて紹介します。おかしなところがあったら教えてください。

UWPではないUnityでのasync/awaitはこちらが詳しいです。

 

サンプルプログラム

UWPにビルドしたコード上でasync/awaitを使う方法を2つ紹介します。

  1. Task.Run()内でasync/awaitする(UnityEngine.WSA.Application.InvokeOnAppThreadは後述します)
  2. StartCoroutine()してyield return new WaitWhile()する

リポジトリはこちら

ちなみにUWP内のみコードを有効にするには #if UNITY_UWP をつけます。他にもいくつか定義がありますが、UWPビルドのどこかたタイミングで有効になるので、ビルドエラーになる場合もあります(細かくは調べていません)。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

#if UNITY_UWP
using System.Threading.Tasks;
#endif

public class AsycnAwaitTest : MonoBehaviour {
    public TextMesh textMeshAynsc;
    public TextMesh textMeshCoroutine;

    // Use this for initialization
    void Start() {
#if UNITY_UWP
        // Task.Runでasyncする
        Task.Run(async () => {
            UnityEngine.WSA.Application.InvokeOnAppThread(()=>{
                textMeshAynsc.text = "Task.Run before : " + Time.time;
            }, true);

            await Task.Delay(5000);

            UnityEngine.WSA.Application.InvokeOnAppThread(() => {
                textMeshAynsc.text = "Task.Run after  : " + Time.time;
            }, true);
        });

        StartCoroutine(HeavyTask());
#endif
    }

    private IEnumerator HeavyTask()
    {
#if UNITY_UWP
        // WaitWhileでまつ
        textMeshCoroutine.text = "Task.Run before : " + Time.time;

        var task = Task.Delay(5000);
        yield return new WaitWhile( () => !task.IsCompleted);

        textMeshCoroutine.text = "Task.Run after  : " + Time.time;
#else
        yield return null;
#endif
    }
}

Task.Run()内でasync/awaitする

Task.Run()で非同期タスクを起こし、この中のメソッドをasyncで定義すればその中ではawaitが使えます。これがシンプルだと思います。

StartCoroutine()してyield return new WaitWhile()する

StartCoroutine()で非同期処理を開始します。 コルーチンはasyncにできずawaitが使えないので、非同期メソッドのTaskを受け取りyield return new WaitWhile()で待ちます。従来のコードとの整合をとりつつ非同期メソッドを使う感じでしょうか。

マルチスレッド

上記2つの最大の違いはTask.Run()の場合はマルチスレッド、StartCoroutine()の場合はシングルスレッドになります(スレッドを生成せずメインスレッドを分割する)。

これによって何が変わるかというとTask.Run()内ではGameObjectへのアクセスができなくなります。WPFなどと同様にUIのオブジェクトはメインスレッドのみアクセスが可能です。そのため、ここで使っている3D TextのTextMeshをTask.Run()内で直接触ると例外が発生します。通常はSynchronizationContextで制御しますが、Unityにはこの仕組みがありません。

ではどうするかというと、UnityEngine.WSA.Application.InvokeOnAppThread()を使います。これによってアプリスレッド(Unityのメインスレッド)で処理されます。

もう一つ、UnityEngine.WSA.Application.InvokeOnUIThread()というのもありますが、こちらはXAMLのメインスレッドのようです。

ドキュメントはこちら docs.unity3d.com