FastAPIで作ったサーバーにUnityからリクエストを送る

初めまして、D-kunです。

A-kun ひとり Advent Calendar 2024 4日目の記事です。

## はじめに
生成AIなどを用いたUnityアプリを作成する際、UnityだけではなくPythonのライブラリを用いたライブラリを使いたいことがあります。その際、UnityからPythonのサーバーにリクエストを送る必要があります。今回は、その際にFastAPIを用いたサーバーを作成し、Unityからリクエストを送る方法について書きます。

サンプルとなるリポジトリを作っています。以下のリンクからクローンして使って下さい。

https://github.com/fukuda-A-HU/FastAPI-Unity-Sample.git

## この記事を読んで欲しい人
- ハッカソンなどで短時間で開発したい人
- UnityとPythonを使ったアプリを作りたい人
- 昨日のご飯がハンバーグだった人

## リポジトリの概要

補足に重要なコードの全体を記載しています。リポジトリをクローンせずとも、補足にあるコードをコピーすることで（Pythonについては必要なモジュールをInstallしてもらうだけで）動くようになります。画像については送受信する画像を用意し、Pythonのパスを変更・UnityのInspectorからアタッチして下さい。

## Pythonでサーバーを立ち上げる

まずは下記についてです。

```python
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel
import shutil

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, World!"}
```

あとは.pyファイルがあるディレクトリで下記コマンドを実行することで、サーバーが立ち上がります。

```bash
unicorn main:app --reload
```

出力が下記のように出ます。

```bash
INFO:     Will watch for changes in these directories: ['/Users/fukuda/FastAPI-Unity-Sample/FastAPI']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [84239] using StatReload
INFO:     Started server process [84241]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
```

http://127.0.0.1:8000/ にアクセスすると、{"message": "Hello, World!"} が表示されます。また、http://127.0.0.1:8000/docs にアクセスすると、Swagger UIが表示されます。Swagger UIでは、APIのリクエストを試しに送ることができます。

また、FastAPIはホットリロードに対応しているので、ファイルを変更すると自動でサーバーが再起動します。とても便利。

## Unityからリクエストを送る
サンプルのUnityプロジェクトにもありますが、ContextMenuを設定することで、public関数をInspectorから関数を実行できるようになります。Unityを再生せずに実行できるようになるので、デバッグが楽になります。

こんな感じ
```csharp
[ContextMenu("hogehoge実行！")]
public void hogehoge(){
    Debug.Log("hogehoge");
}
```

今回のサンプルでは全ての関数にContextMenuを設定しています。コンポーネントの上部を右クリックすると、実行できる関数が表示されます。
![alt text](https://github.com/fukuda-A-HU/zenn-books/blob/main/images/fastapi-unity/image-1.png?raw=true)

## String型ののGETとPOST

Python側のコードは以下の通りです。

```python
@app.get("/text")
def get_text():
    return {"message": "Hello from the server!"}

@app.post("/text")
def post_text(text: str = Form(...)):
    print(f"Received text: {text}")
    return {"message": f"Received text: {text}"}
```

Unity側のコードは以下の通りです。

```csharp
IEnumerator GetText() 
{
    UnityWebRequest www = UnityWebRequest.Get(url + "/text");
    yield return www.SendWebRequest();

    if (www.result != UnityWebRequest.Result.Success) {
        Debug.Log(www.error);
    }
    else {
        // 結果をテキストで表示
        Debug.Log(www.downloadHandler.text);

        // または、バイナリデータとして結果を取得します。
        byte[] results = www.downloadHandler.data;

        // バイナリデータをテキストに変換して表示

        Debug.Log("Text Downloaded : "+System.Text.Encoding.UTF8.GetString(results));
    }
}

IEnumerator PostText()
{
    List<IMultipartFormSection> formData = new List<IMultipartFormSection>();
    formData.Add(new MultipartFormDataSection("text", postText));

    UnityWebRequest www = UnityWebRequest.Post(url + "/text", formData);
    yield return www.SendWebRequest();

    if (www.result != UnityWebRequest.Result.Success)
    {
        Debug.Log(www.error);
    }
    else
    {
        Debug.Log("Text Post complete!");
    }
}
```

## TextureのGETとPOST

python側のコードは以下の通りです。

```python

@app.get("/texture")
def get_texture():
    return FileResponse("eto_hebi_hat.png")

@app.post("/texture")
async def post_texture(texture: UploadFile = File(...)):
    with open(f"uploaded_{texture.filename}", "wb") as buffer:
        shutil.copyfileobj(texture.file, buffer)
    return {"message": "Texture uploaded successfully"}
```

Unity側のコードは以下の通りです。

```csharp
IEnumerator GetTexture() 
{
    UnityWebRequest www = UnityWebRequestTexture.GetTexture(url + "/texture");
    yield return www.SendWebRequest();

    if (www.result != UnityWebRequest.Result.Success) {
        Debug.Log(www.error);
    }
    else {
        Texture myTexture = ((DownloadHandlerTexture)www.downloadHandler).texture;
        
        Debug.Log("Texture Downloaded");
    }
}

IEnumerator PostTexture()
{
    List<IMultipartFormSection> formData = new List<IMultipartFormSection>();
    formData.Add(new MultipartFormFileSection("texture", postTexture.EncodeToPNG(), "texture.png", "image/png"));

    UnityWebRequest www = UnityWebRequest.Post(url + "/texture", formData);
    yield return www.SendWebRequest();

    if (www.result != UnityWebRequest.Result.Success)
    {
        Debug.Log(www.error);
    }
    else
    {
        Debug.Log("Texture Post complete!");
    }
}
```

## JSONのGETとPOST

JsonUtilityを用いているため、用いることのできるJsonの構造はある程度制限されます。詳細は公式ドキュメントを参照して下さい。

https://docs.unity3d.com/ScriptReference/JsonUtility.FromJson.html

また、Jsonの構造は事前に決めておく必要があります。今回はPython側ではmain.py内に、Unity側ではData.cs内に構造を定義しています。Python側とUnity側の構造が一致していないと、データの受け渡しができません。

Python側のコードは以下の通りです。Python側では、データの受け取り時に自動でクラスのインスタンスに変換してくれるため、クラスを定義するだけで受け取ることができます。

```python

class Data(BaseModel):
    id: int
    name: str
    value: float

@app.get("/json")
def get_json():
    data = Data(id=1, name="test", value=3.14)
    return data

@app.post("/json")
def post_json(data: Data):
    print(f"Received data: {data.id}, {data.name}, {data.value}")
    return {"message": f"Received data: {data}"}
```

Unity側のコードは以下の通りです。

```csharp
IEnumerator GetJson() 
{
    UnityWebRequest www = UnityWebRequest.Get(url + "/json");
    yield return www.SendWebRequest();

    if (www.result != UnityWebRequest.Result.Success) {
        Debug.Log(www.error);
    }
    else {

        Debug.Log(www.downloadHandler.text);

        Data data = JsonUtility.FromJson<Data>(www.downloadHandler.text);

        Debug.Log("Json Data : "+data.id+" "+data.name+" "+data.value);
    }
}

IEnumerator PostJson()
{
    Data data = new Data();
    data.id = 1;
    data.name = "Akun";
    data.value = 3.14f;

    string json = JsonUtility.ToJson(data);

    UnityWebRequest www = new UnityWebRequest(url + "/json", "POST");
    byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
    www.uploadHandler = new UploadHandlerRaw(bodyRaw);
    www.downloadHandler = new DownloadHandlerBuffer();
    www.SetRequestHeader("Content-Type", "application/json");

    yield return www.SendWebRequest();

    if (www.result != UnityWebRequest.Result.Success)
    {
        Debug.Log(www.error);
    }
    else
    {
        Debug.Log("Json Post complete!");
    }
}
```

## おわりに
これをコピーしていただければとりあえず動くようにはなるはずです。もちろん、実際にはエラーハンドリングや、セキュリティ対策なども必要ですが、デモアプリなどを作る際には十分使えると思います。

## 補足：Pythonコードの全貌

```python
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import FileResponse
from pydantic import BaseModel
import shutil

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hello, World!"}

@app.get("/text")
def get_text():
    return {"message": "Hello from the server!"}

@app.post("/text")
def post_text(text: str = Form(...)):
    print(f"Received text: {text}")
    return {"message": f"Received text: {text}"}

@app.get("/texture")
def get_texture():
    return FileResponse("eto_hebi_hat.png")

@app.post("/texture")
async def post_texture(texture: UploadFile = File(...)):
    with open(f"uploaded_{texture.filename}", "wb") as buffer:
        shutil.copyfileobj(texture.file, buffer)
    return {"message": "Texture uploaded successfully"}

class Data(BaseModel):
    id: int
    name: str
    value: float

@app.get("/json")
def get_json():
    data = Data(id=1, name="test", value=3.14)
    return data

@app.post("/json")
def post_json(data: Data):
    print(f"Received data: {data.id}, {data.name}, {data.value}")
    return {"message": f"Received data: {data}"}

```

## 補足：C#コードの全貌

```csharp
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Networking;

namespace AkunClient
{
    public class Main : MonoBehaviour
    {

        [SerializeField] private string url = "http://127.0.0.1:8000";
        [SerializeField] private Texture2D postTexture;
        [SerializeField] private string postText = "Hello World";

        [ContextMenu("ExecuteGetText")]
        public void ExecuteGetText()
        {
            StartCoroutine(GetText());
        }

        IEnumerator GetText() 
        {
            UnityWebRequest www = UnityWebRequest.Get(url + "/text");
            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success) {
                Debug.Log(www.error);
            }
            else {
                // 結果をテキストで表示
                Debug.Log(www.downloadHandler.text);

                // または、バイナリデータとして結果を取得します。
                byte[] results = www.downloadHandler.data;

                // バイナリデータをテキストに変換して表示

                Debug.Log("Text Downloaded : "+System.Text.Encoding.UTF8.GetString(results));
            }
        }

        [ContextMenu("ExecutePostText")]
        public void ExecutePostText()
        {
            StartCoroutine(PostText());
        }
        IEnumerator PostText()
        {
            List<IMultipartFormSection> formData = new List<IMultipartFormSection>();
            formData.Add(new MultipartFormDataSection("text", postText));

            UnityWebRequest www = UnityWebRequest.Post(url + "/text", formData);
            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.Log(www.error);
            }
            else
            {
                Debug.Log("Text Post complete!");
            }
        }

        [ContextMenu("ExecuteGetTexture")]
        public void ExecuteGetTexture(){
            StartCoroutine(GetTexture());
        }

        IEnumerator GetTexture() 
        {
            UnityWebRequest www = UnityWebRequestTexture.GetTexture(url + "/texture");
            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success) {
                Debug.Log(www.error);
            }
            else {
                Texture myTexture = ((DownloadHandlerTexture)www.downloadHandler).texture;
                
                Debug.Log("Texture Downloaded");
            }
        }

        [ContextMenu("ExecutePostTexture")]
        public void ExecutePostTexture()
        {
            StartCoroutine(PostTexture());
        }

        IEnumerator PostTexture()
        {
            List<IMultipartFormSection> formData = new List<IMultipartFormSection>();
            formData.Add(new MultipartFormFileSection("texture", postTexture.EncodeToPNG(), "texture.png", "image/png"));

            UnityWebRequest www = UnityWebRequest.Post(url + "/texture", formData);
            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.Log(www.error);
            }
            else
            {
                Debug.Log("Texture Post complete!");
            }
        }

        [ContextMenu("ExecuteGetJson")]
        public void ExecuteGetJson()
        {
            StartCoroutine(GetJson());
        }

        IEnumerator GetJson() 
        {
            UnityWebRequest www = UnityWebRequest.Get(url + "/json");
            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success) {
                Debug.Log(www.error);
            }
            else {

                Debug.Log(www.downloadHandler.text);

                Data data = JsonUtility.FromJson<Data>(www.downloadHandler.text);

                Debug.Log("Json Data : "+data.id+" "+data.name+" "+data.value);
            }
        }

        [ContextMenu("ExecutePostJson")]
        public void ExecutePostJson()
        {
            StartCoroutine(PostJson());
        }

        IEnumerator PostJson()
        {
            Data data = new Data();
            data.id = 1;
            data.name = "Akun";
            data.value = 3.14f;

            string json = JsonUtility.ToJson(data);

            UnityWebRequest www = new UnityWebRequest(url + "/json", "POST");
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
            www.uploadHandler = new UploadHandlerRaw(bodyRaw);
            www.downloadHandler = new DownloadHandlerBuffer();
            www.SetRequestHeader("Content-Type", "application/json");

            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.Log(www.error);
            }
            else
            {
                Debug.Log("Json Post complete!");
            }
        }
    }
}
```