はじめに
ゲーム開発エンジン「Unity」と、Googleにより開発されたプログラミング言語「Go」を用いて、凄くシンプルなREST APIを実装したいと思います。
Unityについて:https://unity.com/ja
Goについて:https://golang.org/
開発環境
- MacBook Pro (15-inch, 2018)
- macOS Catalina
- Unity 2019.1.1f1 Personal
- Go 1.13.5 darwin/amd64
システム構造
Unityをクライアントサイド、APIサーバ(Goで実装)をサーバサイドとして作りました。
内容
サーバサイドで保存されているデータに対して、Unity側からデータのID(一意なもの)を指定して対象データを取得します。データに関しては、予めいくつか用意しておきます。
今回はDBを使わないので、オンメモリのデータストア(リストを使います)に格納しておこうと思います。なので、サーバを停止させるとデータは吹き飛びますw
実装
サーバサイド
まずサーバサイドから実装していきます。
といっても、Goには便利なパッケージがたくさんあり、かつネットには参考になる情報が大量にあるため、それらをめっちゃ活用しました。
コードは以下の通りです。
package main
import (
"log"
"net/http"
"strconv"
"github.com/ant0ine/go-json-rest/rest"
)
type Monster struct {
ID int
Name string
}
// オンメモリのデータストア.
var dataStore = map[int]*Monster{}
func main() {
api := rest.NewApi()
api.Use(rest.DefaultDevStack...)
router, err := rest.MakeRouter(
rest.Get("/getAllData", GetAllData),
rest.Post("/postData", PostData),
rest.Get("/getData/:id", GetData),
)
if err != nil {
log.Fatal(err)
}
api.SetApp(router)
log.Printf("Server Started.")
// APIサーバを起動.
log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}
// データを新しく作成する.
func PostData(w rest.ResponseWriter, r *rest.Request) {
monster := Monster{}
err := r.DecodeJsonPayload(&monster)
if err != nil {
rest.Error(w, err.Error(), http.StatusInternalServerError)
return
}
dataStore[monster.ID] = &monster
w.WriteJson(&monster)
}
// 指定IDのデータを取得する.
func GetData(w rest.ResponseWriter, r *rest.Request) {
id, _ := strconv.Atoi(r.PathParam("id"))
var monster *Monster
if dataStore[id] != nil {
monster = &Monster{}
*monster = *dataStore[id]
}
if monster == nil {
rest.NotFound(w, r)
return
}
w.WriteJson(monster)
}
// 全データを取得する.
func GetAllData(w rest.ResponseWriter, r *rest.Request) {
allData := make([]Monster, len(dataStore))
i := 0
for _, data := range dataStore {
allData[i] = *data
i++
}
w.WriteJson(&allData)
}
こちらのコードを作成するために、以下の資料を大変参考にさせていただきました!
golangでREST APIをやってみた①
今回は、簡単にRESTfulなAPIサーバを構築することができる以下のパッケージを使っています。
Go-Json-Rest
コードに関して大事なところは、以下の箇所かと思います。
router, err := rest.MakeRouter(
rest.Get("/getAllData", GetAllData),
rest.Post("/postData", PostData),
rest.Get("/getData/:id", GetData),
)
ここでは、go-json-rest
のrestパッケージに実装されているMakeRouter
を使って、ルーティングパスを3つ設定しています。それぞれ以下のハンドラと結びつきます。
-
/getAllData
- GetAllData : データストアに格納されている全データを取得するハンドラ.
-
/postData
- PostData : データストアに新しくデータを格納するハンドラ.
-
/getData/:id
- GetData : 指定idを持つデータを取得するハンドラ.
今後新しくルーティングを作成していきたい場合は、MakeRouter
にHTTPリクエストメソッドに対して、ルーティングパスとハンドラを結び付けて定義してあげればいいということですね。
クライアントサイド
サーバサイドに対して処理を要求するクライアントサイドの実装を行います。
クライアントはUnityを使うので、最初はEditorで以下の画面を作成しました。
Hierarchy
ビューを見ていただくと、InputArea
とOutputArea
というオブジェクトがあります。これらはそれぞれ、Game
ビューにおける下側の要素と上側の要素を指しています。
また、とても重要なのがHierarchy
ビューのClient
オブジェクトです。これはEmpty Objectなのですが、以下のスクリプトがアタッチされています。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
public class ApiClient : MonoBehaviour
{
public InputField inputField;
public Text outputText;
public Image monsterImage;
private string baseURL = "http://127.0.0.1:8080";
public void GetDataFromAPIServer()
{
StartCoroutine(GetData());
}
IEnumerator GetData()
{
string url = baseURL + "/getData/" + int.Parse(inputField.text);
UnityWebRequest request = UnityWebRequest.Get(url);
yield return request.SendWebRequest();
if (request.isNetworkError)
{
Debug.Log(request.error);
}
else
{
if(request.responseCode == 200)
{
string rawData = request.downloadHandler.text;
MonsterData monsterData = JsonUtility.FromJson<MonsterData>(rawData);
string imgPath = "monster_" + monsterData.ID;
outputText.text = monsterData.Name;
monsterImage.sprite = Resources.Load<Sprite>(imgPath);
}
}
}
}
上記コードは以下の資料を参考にさせていただきました!
UnityでHTTPに接続する
このスクリプトでは、GetDataFromAPIServer()
メソッドが実行されるとGetData()
が動きます。
GetData()
内で、リクエスト対象のURLを作成後、以下の箇所でサーバ側への送信とレスポンス受付けを行います。
yield return request.SendWebRequest();
レスポンスのステータスコードが200ならば、返ってきたJSONデータ(string型)をプログラム内の変数に格納します。
その後、JSONデータをパースして対応するクラスインスタンスに情報を入れていきます。以下の箇所です。
MonsterData monsterData = JsonUtility.FromJson<MonsterData>(rawData);
このMonsterData
クラスですが、以下のようにしました。データを一意に保つためのID
と、データの名前Name
を持ちます。なので返却されるJSONは、この構造を持っている必要があります。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class MonsterData
{
public int ID;
public string Name;
}
上のソースコード(ApiClient
クラスのほう)をClient
オブジェクトにアタッチしたのが、以下の状態です。
いくつかInspector
ビューから紐づけているコンポーネントがあります。
-
InputField
- クライアント側から指定するIDを入力するフィールド.
-
OutputText
- サーバ側から返ってきたデータの
Name
変数を格納する.
- サーバ側から返ってきたデータの
-
MonsterImage
- 取得データのIDを使ってクライアント側に置いてある対応する画像をセットする.
これらは以下のように紐づけています。
最後に、今回はGUIのボタンが押されたらサーバ側に処理を依頼するようにしようと思ったので、Hierarchy
ビュー上に置かれているボタンオブジェクトに対して、ClientオブジェクトにアタッチしたApiClientクラスが持つGetDataFromAPIServer()
メソッドを割り当てています。
これで準備OKです!
動作確認
まずAPIサーバを立てておきます。以下の状態で待機します。
$ go run server.go
2019/12/15 23:25:10 Server Started.
ターミナルで別タブを開いて、いくつかデータを格納(POST)しておきます。
$ curl -i -H "Content-Type: application/json" \
-d '{"ID": 1, "Name": "スライム"}' http://127.0.0.1:8080/postData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: xxxxx
Content-Length: 39
{
"ID": 1,
"Name": "スライム"
}%
$ curl -i -H "Content-Type: application/json" \
-d '{"ID": 2, "Name": "ソルジャー"}' http://127.0.0.1:8080/postData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: yyyyy
Content-Length: 42
{
"ID": 2,
"Name": "ソルジャー"
}%
以下のREST APIを叩くと、結果が全件返ってきてくれました!
$ curl -i http://127.0.0.1:8080/getAllData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: xxxxx
Content-Length: 103
[
{
"ID": 1,
"Name": "スライム"
},
{
"ID": 2,
"Name": "ソルジャー"
}
]%
次にクライアントサイドを起動して動作をみてみます。
まず、IDが1
のデータを取得してみます。(格納しておいたデータでいうと、スライムが該当します)
以下のように入力して「データを取得」を押すと、、、
無事データを取得して、画面に反映されました!(画像データはあらかじめ自分で作ったものを参照してますw)
同じように、IDを2
にしてやってみます。以下のようにして「データを取得」を押すと、、、
IDが2のデータ(ソルジャー)が取得できました!
終わりに
今回、UnityとGoを用いて簡単なAPI通信を実装してみました。
今後は今回学んだことも踏まえながら、より発展的な内容にも取り組んでいけたらなと思っています!
ありがとうございました!
参考
golangでREST APIをやってみた①
UnityでHTTPに接続する
UnityWebRequest
UnityWebRequestの使い方【Unity】
Go 言語の値レシーバとポインタレシーバ