Graffity でUnity エンジニアをやっている小林です。
社内システムや、小規模サービスを開発するときに GoogleCloudFunction
をはじめとしたサーバレス環境でバックエンドを開発したい場合はままある話だと思います。
しかし、今回はこのバックエンドのAPIを叩くフロント側が Unity 製アプリ
というのがなかなかニッチなお話になります。
なんでこういうことやろうと思ったの
A. 筆者がUnity開発歴10年近く、Unityの開発が慣れてるから。
& Unity でUI作っちゃえばAndroid / iOS 向けにアプリを簡単に作れる
& やっぱタッチ操作で便利なUXを提供したい
つまづいたところ
GCF はHTTPトリガーにすれば、curl 等で呼び出せます。
つまりはUnityでいうところの UnityWebRequest
でも呼べるはずです。
さらに言えば、任意のクラスを用意してそれをJSON にして送ることも理論上は可能なはずです。
よって以下のコードで実行できる...はずでした...
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace Graffity.GCP
{
public static class CloudFunction
{
public static async UniTask<TResult> CallGcfAsync<TResult, TData>(string uri, TData postData, CancellationToken token)
{
bool invalidUri = string.IsNullOrEmpty(uri);
string sendData = sonUtility.ToJson(postData);
bool invalidPostData = string.IsNullOrEmpty(sendData);
if (invalidUri || invalidPostData)
{
if(invalidUri) Debug.LogError($"Invalid URI: {uri} ");
if(invalidPostData) Debug.LogError($"Invalid POST Data: {sendData} ");
return default(TResult);
}
try
{
UnityWebRequest www = UnityWebRequest.Post(uri, sendData);
await www.SendWebRequest().WithCancellation(token);
if (www.IsResultError())
{
Debug.LogError($"Invoke {uri} was failed.!!");
Debug.LogError(www.error);
return default(TResult);
}
var response = www.downloadHandler.text;
TResult ret = JsonUtility.FromJson<TResult>(response);
return ret;
}
catch (OperationCanceledException e)
{
Debug.LogWarning($"Invoke {uri} is canceled.!!");
throw;
}
catch (Exception e)
{
Debug.LogError($"Invoke {uri} was failed.!!");
throw;
}
}
}
}
上記Scriptで実行した結果
400. Bad Request
まぁ泣きたくなりますよ。
CURLでは200帰ってきて同じエンドポイント叩いてるのにダメなんですから。
そこで日本語記事をググっても Unity CloudFunction だと 私の記事 がヒットするくらいで、まぁ情報がありません。
そこで海外に目を向けて調べたところ1件だけ同様の現象に遭遇している方がいらっしゃいました。
解決方法
解決方法としては至って単純で
- DownloadHandler とUploadHandler を設定する
- jsonをバイナリでupload
- ヘッダを正しく設定
で無事動きました。
具体的には以下のような形です
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace Graffity.GCP
{
public static class CloudFunction
{
public static async UniTask<TResult> CallGcfAsync<TResult, TData>(string uri, TData postData, CancellationToken token)
{
bool invalidUri = string.IsNullOrEmpty(uri);
string sendData = sonUtility.ToJson(postData);
bool invalidPostData = string.IsNullOrEmpty(sendData);
if (invalidUri || invalidPostData)
{
if(invalidUri) Debug.LogError($"Invalid URI: {uri} ");
if(invalidPostData) Debug.LogError($"Invalid POST Data: {sendData} ");
return default(TResult);
}
try
{
UnityWebRequest www = new UnityWebRequest(uri, UnityWebRequest.kHttpVerbPOST);
www.SetRequestHeader("Content-Type", "application/json");
byte[] byteSendData = new System.Text.UTF8Encoding ().GetBytes (sendData);
www.uploadHandler = (UploadHandler)new UploadHandlerRaw (byteSendData);
www.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
await www.SendWebRequest().WithCancellation(token);
if (www.IsResultError())
{
Debug.LogError($"Invoke {uri} was failed.!!");
Debug.LogError(www.error);
return default(TResult);
}
var response = www.downloadHandler.text;
TResult ret = JsonUtility.FromJson<TResult>(response);
return ret;
}
catch (OperationCanceledException e)
{
Debug.LogWarning($"Invoke {uri} is canceled.!!");
throw;
}
catch (Exception e)
{
Debug.LogError($"Invoke {uri} was failed.!!");
throw;
}
}
}
}
正直、なぜこれでうまく行くのかは全く不明なのですが、外部サーバーのEndpoint を叩くときにうまくいかなければ上記のようにやればうまく行く場合があります。(少なくともこれでGCFは叩けるようになりました)