Unity

Unityでasync,awaitと一緒に簡単に使える超簡易RESTClientを作った

Unityの中でもHTTPリクエストを飛ばすことはよくあること。特に、ランキング処理などをサーバーでやっている場合、それらをいちいちソケット通信などで取ってくるのは馬鹿馬鹿しい。
Webベースで設計されているならUnityWebRequestとコルーチンを用いれば非同期処理の上で書くことができる。

でも、せっかくUnityは.Net 4.x系列をサポートしたのだ。async,awaitで使いたいじゃないか。

作ったもの

RESTClient
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

/**
 * Useful utility to use REST access to server
 */
public class RESTClient
{
    /**
     * Host name of server
     * This must be endpoint URI of the server
     */
    public readonly string hostName;

    /**
     * Header that is included in every request send from this class
     */
    public readonly Dictionary<string, string> headers = new Dictionary<string, string>();

    /**
     * Constructor
     */
    public RESTClient(string hostName)
    {
        this.hostName = hostName;
    }

    /**
     * Perform GET request to the path
     */
    public Task<T> Get<T>(string path)
    {
        UnityWebRequest req = UnityWebRequest.Get(this.getURL(path));
        return ProcessRequest<T>(req);
    }

    /**
    * Perform PUT request to the path
    * You can specify content also
    */
    public Task<T> Put<T>(string path, string content = "")
    {
        UnityWebRequest req = UnityWebRequest.Put(this.getURL(path), content);
        return ProcessRequest<T>(req);
    }

    /**
    * Perform DELETE request to the path
    */
    public Task<T> Delete<T>(string path)
    {
        UnityWebRequest req = UnityWebRequest.Delete(this.getURL(path));
        return ProcessRequest<T>(req);
    }

    /**
    * Perform HEAD request to the path
    */
    public Task<T> Head<T>(string path)
    {
        UnityWebRequest req = UnityWebRequest.Head(this.getURL(path));
        return ProcessRequest<T>(req);
    }

    /**
    * Perform POST request to the path
    * You can specify parameter as dictioanry
    */
    public Task<T> Post<T>(string path, Dictionary<string, string> param = null)
    {
        if (param == null)
        {
            param = new Dictionary<string, string>();
        }

        UnityWebRequest req = UnityWebRequest.Post(this.getURL(path), param);
        return ProcessRequest<T>(req);
    }

    private async Task<T> ProcessRequest<T>(UnityWebRequest req)
    {
        SetHeaders(req);
        String reqResult = await ExecuteRequestToGetTask(req);
        AssertRequestErrors(req);
        return JsonUtility.FromJson<T>(reqResult);
    }

    private void SetHeaders(UnityWebRequest req)
    {
        foreach (KeyValuePair<string, string> pair in this.headers)
        {
            req.SetRequestHeader(pair.Key, pair.Value);
        }
    }

    private void AssertRequestErrors(UnityWebRequest req)
    {
        if (req.isHttpError || req.isNetworkError)
        {
            throw new Exception(req.error);
        }
    }

    private Task<String> ExecuteRequestToGetTask(UnityWebRequest req)
    {
        var asyncReq = req.SendWebRequest();
        TaskCompletionSource<String> tcs = new TaskCompletionSource<String>();
        asyncReq.completed += (e) => { tcs.SetResult(req.downloadHandler.text); };
        return tcs.Task;
    }

    private string getURL(string path)
    {
        return $"{this.hostName}{path}";
    }
}

使い方

レスポンスが全てJSON形式であるという想定が勝手にされている。モダンなAPIなら問題はないだろう。

class TheMonoBehaviour:MonoBehaviour{
   public RESTClient client = new RESTClient("https://api.github.com/");

   void Start(){
       this.ExecuteInAsync();
   }

   async void ExecuteInAsync(){
       ISchema data = await this.client.Get<ISchema>(""); // この場合、エンドポイントのルートになる。あらかじめJSONの形式に会うインターフェース、ISchemaを定義しておかねばならない
       await Task.WaitAll([await this.client.Get<ISchema2>("/users/"),await this.client.Post<ISchema3>("~~~")]); // タスクなのでまとめたり、一番最初い終わったタスクを利用したりなども容易

   }
}

適当設計だが、割と単純にRESTAPIをasync,awaitで叩けるので便利。 UnityのPlayer設定を変えて、.Net4.x系を選択しなければ動かないことに注意