Oculus Platform APIをコールバックからTaskへ変換する


はじめに

Oculus Storeからアプリを配布する時はOculus Platform APIが必須になっています。
そのOculus Platform APIは結果をコールバックで返すので処理が冗長になってしまいますが簡単にTask化できたので紹介します。

開発環境

  • System.Threading.Tasksを扱える設定およびバージョンのUnity
    • Unity2017 + Experimental (.NET 4.6 Equivalent)
    • Unity2018 + .NET 4.x Equivalent
    • Unity2019以上
  • Oculus Integration v1.41.0以上

コールバックの場合

以下はエンタイトルメントチェックのベストプラクティスのサンプルコードです。Oculus Platform APIはIsUserEntitledToApplication()のようにOnComplete()で結果が返ってくるので同様の呼び出しが続くとコールバック地獄になって処理の流れが分かりにくくなります。

using UnityEngine;
using Oculus.Platform;

public class AppEntitlementCheck: MonoBehaviour {

  void Awake ()
  {
    try
    {
      Core.AsyncInitialize();
      Entitlements.IsUserEntitledToApplication().OnComplete(EntitlementCallback); // ここがコールバック
    }
    catch(UnityException e)
    {
      Debug.LogError("Platform failed to initialize due to exception.");
      Debug.LogException(e);
      // Immediately quit the application.
      UnityEngine.Application.Quit();
    }
  }

  void EntitlementCallback (Message msg)
  {
    if (msg.IsError)
    {
      Debug.LogError("You are NOT entitled to use this app.");
      UnityEngine.Application.Quit();
    }
    else
    {
      Debug.Log("You are entitled to use this app.");
    }
  }
}

Taskへ変換する

スクリプティング定義シンボルにOVR_PLATFORM_ASYNC_MESSAGESを追加します。

するとTaskを返すGen()メソッドがOculus.Platform.Requestクラスで使えるようになるので以下のように書く事ができます。

private async void Awake()
{
    try
    {
        if (!Core.Initialized())
        {
            var initialized = await Core.AsyncInitialize().Gen(); // ここをTask化
            if (initialized.IsError)
            {
                Debug.Log($"failed initialize: {initialized.GetError().Message}");
                return;
            }
        }

        var entitlements = await Entitlements.IsUserEntitledToApplication().Gen(); // ここをTask化
        if (entitlements.IsError)
        {
            Debug.Log($"failed entitlement: {entitlements.GetError().Message}");
            return;
        }

        var user = await Users.GetLoggedInUser().Gen(); // ここをTask化
        if (user.IsError)
        {
            Debug.Log($"failed get user: {user.GetError().Message}");
            return;
        }
        Debug.Log($"{user.Data.ID}, {user.Data.OculusID}");
    }
    catch (UnityException e)
    {
        Debug.LogException(e);
        Application.Quit();
    }
}

更にUniTaskと組み合わせるとタイムアウトも簡単に扱えます。

var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(10));
try
{
    var initialized = await Core.AsyncInitialize().Gen().AsUniTask().AttachExternalCancellation(cts.Token);
}
catch (OperationCanceledException e)
{
    if (ex.CancellationToken == cts.Token)
    {
        Debug.Log("Timeout");
    }
}

1つ残念なのはGen()が返すTaskTaskCompletionSourceで作られてprivateフィールドにあるのですがTrySetCanceled()を呼ぶメソッドが用意されていないのでCancellationTokenでキャンセルできない事です。そのため、UniTaskのタイムアウトなどと組み合わせるなどの工夫が必要です。

参考