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以上

# コールバックの場合

以下は[エンタイトルメントチェックのベストプラクティス](https://developer.oculus.com/documentation/unity/ps-setup/#entitlement-check-best-practices)のサンプルコードです。Oculus Platform APIは`IsUserEntitledToApplication()`のように`OnComplete()`で結果が返ってくるので同様の呼び出しが続くとコールバック地獄になって処理の流れが分かりにくくなります。

```csharp
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`を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/d7c2d14a-7463-2ce5-89f1-d39a328dc04f.png)

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

```csharp
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と組み合わせるとタイムアウトも簡単に扱えます。

```csharp
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()`が返す`Task`は`TaskCompletionSource`で作られてprivateフィールドにあるのですが`TrySetCanceled()`を呼ぶメソッドが用意されていないので`CancellationToken`でキャンセルできない事です。そのため、UniTaskのタイムアウトなどと組み合わせるなどの工夫が必要です。

# 参考

- [C#のasync/await再考, タイムアウト処理のベストプラクティス, UniTask v2.2.0](http://neue.cc/2021/02/26_599.html)