Unityで簡単にHTTPリクエスト(GET, POST) をできる拡張コード

## 経緯
UnityでHTTPリクエストを投げるのって割と面倒なんですよね。
[UnityWebRequest で簡単 HTTP(POST)通信 | 株式会社ビヨンド](https://beyondjapan.com/blog/2020/05/unitywebrequest/)
こちらでUnityWebRequestを使って簡単にPOSTリクエストを行う方法が解説されていました。
これをベースにGETリクエストも行えるようにして、細かい部分を自分なりに使いやすいようカスタマイズしたところ、割と実用的なレベルになったのではと思うので、公開します。
(元記事の作成者である株式会社ビヨンドの松山賢勝さんに許可をいただいております。ありがとうございます！)

https://github.com/mega-yadoran/UnityWebRequest

基本的な動作の流れや利用している技術は [元記事](https://beyondjapan.com/blog/2020/05/unitywebrequest/) とほとんど変わっていないため、そちらをご参照ください。
本記事では使い方のみ記載します。元記事に書いていない処理で気になるところがあれば質問していただけたらと思います。


## 使い方

### 階層説明

```
Assets/
　├ Api/
　│　├ Base/*.cs           // 基底クラスなど
　│　└ SampleApi/          // コールしたいAPIの情報をまとめているディレクトリ
　│　　├ GetWeatherApi.cs  // GETリクエストのサンプルAPI情報
　│　　└ PostSampleApi.cs  // POSTリクエストのサンプルAPI情報
　└ SceneScript.cs        // APIコール実行のサンプルコード
```

### コールしたいAPIのクラスを作成

まずコールしたいAPIのエンドポイント1つにつき1つのクラスを作成します。
必要なコーディングは

- リクエストパラメータの構造体
- レスポンスデータの構造体
- APIクラスのコンストラクタ

最低この3つがあればOKです。
以下、サンプルコードを見ながら使い方を説明していきます。

#### GETリクエスト


```c#:GetWeatherApi.cs
using System;

namespace Api
{
    /// <summary>
    /// 天気予報 API（livedoor 天気互換）(https://weather.tsukumijima.net/)から現在の天気情報を取得します
    /// </summary>
    public class GetWeatherApi : BaseApi
    {
        /// リクエストパラメータ
        [Serializable]
        public struct Request
        {
            public int city;
        }
        public Request request;

        /// レスポンスデータ
        [Serializable]
        public struct Response
        {
            public string publicTimeFormatted;
            public string title;
            public Description description; // 階層構造になっている場合は子も構造体で定義する
        }
        public Response response;

        [Serializable]
        public struct Description
        {
            public string publicTimeFormatted;
            public string headlineText;
            public string text;
        }

        // コンストラクタ
        public GetWeatherApi(int city)
        {
            BaseUrl = "https://weather.tsukumijima.net/api";
            EndPoint = "/forecast";
            request.city = city;
        }
    }
}
```

順番に見ていきましょう。

```c#
/// リクエストパラメータ
[Serializable]
public struct Request
{
    public int city;
}
public Request request;
```

この書き方で int型の `city` をリクエストパラメータとして送ることになります。

```c#
/// レスポンスデータ
[Serializable]
public struct Response
{
    public string publicTimeFormatted;
    public string title;
    public Description description; // 階層構造になっている場合は子も構造体で定義する
}
public Response response;

[Serializable]
public struct Description
{
    public string publicTimeFormatted;
    public string headlineText;
    public string text;
}
```

レスポンスもリクエストパラメータの方と基本的に書き方は同じです。
データが階層構造になっている場合は構造体(struct)で定義してください。

```c#
// コンストラクタ
public GetWeatherApi(int city)
{
    BaseUrl = "https://weather.tsukumijima.net/api";
    EndPoint = "/forecast";
    request.city = city;
}
```

コンストラクタ内でリクエスト内容の設定を行います。
`BaseUrl`, `EndPoint` にはそれぞれ名前の通りベースとなるURLとエンドポイントの情報をセットしてください。
今回のサンプルでは [天気予報 API（livedoor 天気互換）](https://weather.tsukumijima.net/api/forecast?city=400040) を使わせていただきました。
最後に引数から受け取った `city` をリクエストパラメータにセットしています。
(ヘッダー情報のセットも可能ですが、POSTの方で説明します。)

以上でGETリクエストを行う場合のAPIクラスは完成です。


#### POSTリクエスト

POSTリクエストもGETリクエストと全く同じ書き方です。

```c#:PostSampleApi.cs
using System;

namespace Api
{
    /// <summary>
    /// POSTリクエストのサンプルAPIをコールします
    /// 存在しないURLにリクエストを投げるため、失敗します
    /// </summary>
    public class PostSampleApi : BaseApi
    {
        /// リクエストパラメータ
        [Serializable]
        public struct Request
        {
            public string param1;
        }
        public Request request;

        /// レスポンスデータ
        [Serializable]
        public struct Response
        {
            public int res1;
        }
        public Response response;

        // コンストラクタ
        public PostSampleApi(string param1)
        {
            BaseUrl = "http://post-sample.example.com";
            EndPoint = "/test";
            Headers.Add("content-type", "application/json");
            request.param1 = param1;
        }
    }
}
```

GETと同じですね。
POSTは登録不要で使えるAPIを見つけられなかったので、ダミーのURLでセットしています。
ヘッダー設定は
`Headers.Add("content-type", "application/json");`
このようにDectionary型で追加してください。

### APIリクエスト実行側の書き方

各APIのクラスを作成したら、実際にAPIのコールを実行する側の処理を書きます。

```c#:SceneScript.cs
using UnityEngine;

public class SceneScript : MonoBehaviour
{
    void Start()
    {
        // GETリクエストのサンプル
        // 天気予報 API（livedoor 天気互換）(https://weather.tsukumijima.net/) をコールします
        // 特に問題がなければリクエストに成功するはずです
        int cityId = 400040; // 福岡県久留米の地域ID
        Api.GetWeatherApi getApi = new Api.GetWeatherApi(cityId);
        getApi.Get<Api.GetWeatherApi.Request>(ref getApi.request, result =>
        {
            if (result.isSuccess)
            {
                // リクエストに成功した場合
                getApi.response = getApi.Response<Api.GetWeatherApi.Response>();
                Debug.Log("天気予報APIのリクエストに成功しました");
                Debug.Log(getApi.response.publicTimeFormatted);
                Debug.Log(getApi.response.description.text);
            }
            else
            {
                // リクエストに失敗した場合
                Debug.Log("天気予報APIリクエストに失敗しました");
                Debug.Log(result.error);
            }
        });

        // POSTリクエストのサンプル
        // PostSampleApi をコールします
        // 存在しないURLなので失敗します
        string param1 = "test";
        Api.PostSampleApi postApi = new Api.PostSampleApi(param1);
        postApi.Post<Api.PostSampleApi.Request>(ref postApi.request, result =>
        {
            if (result.isSuccess)
            {
                getApi.response = getApi.Response<Api.GetWeatherApi.Response>();

                Debug.Log("PostSampleApiのリクエストに成功しました");
            }
            else
            {
                Debug.Log("PostSampleApiリクエストに失敗しました");
                Debug.Log(result.error);
            }
        });
    }
}

```

こちらも順番に見ていきましょう。

```c#
int cityId = 400040; // 福岡県久留米の地域ID
Api.GetWeatherApi getApi = new Api.GetWeatherApi(cityId);
```

久留米の地域IDをリクエストパラメータにセットしてAPIのインスタンスを初期化しています。

```c#
getApi.Get<Api.GetWeatherApi.Request>(ref getApi.request, result =>
{
    if (result.isSuccess)
    {
        // リクエストに成功した場合
        getApi.response = getApi.Response<Api.GetWeatherApi.Response>();
        Debug.Log("天気予報APIのリクエストに成功しました");
        Debug.Log(getApi.response.publicTimeFormatted);
        Debug.Log(getApi.response.description.text);
    }
    else
    {
        // リクエストに失敗した場合
        Debug.Log("天気予報APIリクエストに失敗しました");
        Debug.Log(result.error);
    }
});
```

`getApi.Get<Api.GetWeatherApi.Request>(ref getApi.request, result =>`
この書き方にちょっとクセがありますね。
基底クラスでGETリクエストの実行を `Get<T>()` として定義しており、TはRequestの構造体を指定します。
リクエストに成功した場合は `result.isSuccess` にTrueがセットされるので、 `if (result.isSuccess)` の中の処理が走ります。
`getApi.response = getApi.Response<Api.GetWeatherApi.Response>();` でJson形式のレスポンスをAPIクラスの中で定義したResponseの構造体に変換しています。
`else` にはリクエストに失敗した場合のエラー処理を書いてください。

```c#
string param1 = "test";
Api.PostSampleApi postApi = new Api.PostSampleApi(param1);
postApi.Post<Api.PostSampleApi.Request>(ref postApi.request, result =>
{
    if (result.isSuccess)
    {
        getApi.response = getApi.Response<Api.GetWeatherApi.Response>();

        Debug.Log("PostSampleApiのリクエストに成功しました");
    }
    else
    {
        Debug.Log("PostSampleApiリクエストに失敗しました");
        Debug.Log(result.error);
    }
});
```

リクエストの実行が `Post<T>()` になっているだけで、GETとほとんど同じですね。
ちなみにコメントにも書いてある通り、こちらはダミーのURLを叩いているためリクエストは失敗します。

### 実行結果

![2021070401.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265114/85de6007-8d89-d470-4c2b-e161c7eb0ce2.png)
ちゃんとGETの方は成功して、POSTの方は失敗してますね。
GETの方よりPOSTのログが先に出力されているのは、APIのコール処理を非同期で行っているためです。
処理の順番が重要な場合はこの辺も気にする必要があります。
詳しくはこの記事が参考になります！

https://qiita.com/4_mio_11/items/3a0b06819d9591c5d410

## まとめ

GET/POSTのどちらの場合でもほとんど同じ記法でリクエストが行えるようにしました。
「とりあえずUnityでHTTPリクエストをしたい」という方には十分ではないでしょうか。

https://github.com/mega-yadoran/UnityWebRequest

ソースコードを公開しているので自由にお使いください。
実現できなかったこととして

* レスポンスデータがJSON形式以外のときの対応
* async/await の利用

などがあります。どなたかチャレンジいただければ非常に嬉しいです(プルリクお待ちしてます…！)

最後に、あらためて素晴らしい元記事の作成者である株式会社ビヨンドの松山賢勝さんにお礼を申し上げます！ありがとうございました！！
[UnityWebRequest で簡単 HTTP(POST)通信 | 株式会社ビヨンド](https://beyondjapan.com/blog/2020/05/unitywebrequest/)