初めまして、D-kunです。
A-kun ひとり Advent Calendar 2024 4日目の記事です。
はじめに
生成AIなどを用いたUnityアプリを作成する際、UnityだけではなくPythonのライブラリを用いたライブラリを使いたいことがあります。その際、UnityからPythonのサーバーにリクエストを送る必要があります。今回は、その際にFastAPIを用いたサーバーを作成し、Unityからリクエストを送る方法について書きます。
サンプルとなるリポジトリを作っています。以下のリンクからクローンして使って下さい。
この記事を読んで欲しい人
- ハッカソンなどで短時間で開発したい人
- UnityとPythonを使ったアプリを作りたい人
- 昨日のご飯がハンバーグだった人
リポジトリの概要
補足に重要なコードの全体を記載しています。リポジトリをクローンせずとも、補足にあるコードをコピーすることで(Pythonについては必要なモジュールをInstallしてもらうだけで)動くようになります。画像については送受信する画像を用意し、Pythonのパスを変更・UnityのInspectorからアタッチして下さい。
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ファイルがあるディレクトリで下記コマンドを実行することで、サーバーが立ち上がります。
unicorn main:app --reload
出力が下記のように出ます。
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を再生せずに実行できるようになるので、デバッグが楽になります。
こんな感じ
[ContextMenu("hogehoge実行!")]
public void hogehoge(){
Debug.Log("hogehoge");
}
今回のサンプルでは全ての関数にContextMenuを設定しています。コンポーネントの上部を右クリックすると、実行できる関数が表示されます。
String型ののGETとPOST
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側のコードは以下の通りです。
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側のコードは以下の通りです。
@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側のコードは以下の通りです。
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の構造はある程度制限されます。詳細は公式ドキュメントを参照して下さい。
また、Jsonの構造は事前に決めておく必要があります。今回はPython側ではmain.py内に、Unity側ではData.cs内に構造を定義しています。Python側とUnity側の構造が一致していないと、データの受け渡しができません。
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側のコードは以下の通りです。
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コードの全貌
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#コードの全貌
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!");
}
}
}
}