Unreal Engine 5を使ってみる その14(CSVファイルへの保存、Node-RED)
はじめに
今回はUnreal Engine 5(UE5)をNode-REDと連携させて、UE5の情報をCSVファイルに保存してみました。
UE5をシミュレーション用途で使っていると、CSVファイルに保存してグラフ化したいのに、CSVファイルに保存するノードが無いらしいです。知らないだけかもしれませんが、確かにUE5にはファイルに保存するためのノードが見当たりません。ゲームとして配布したときにファイルのパスをどうするのかという問題はありそうです。
▼HTTP Blueprintプラグインを追加した後だと思うのですが、JSON形式ならファイルに保存できます。
私はUE5とNode-REDで通信して処理していたので、その仕組みを利用してCSVファイルに保存してみました。
▼以前の記事はこちら
実装の方針
今回は題材として、「UE5でのプレイヤーの移動の軌跡を3次元空間上にプロットする」ことに取り組んでみます。
そのためにUE5でのプレイヤーの位置をHTTP通信のPOSTリクエストで送信し、Node-REDで受信します。JSON形式で送られてくるので、受信後にCSV形式に変換してファイルに保存するようにします。
▼以下のような構成です。
なお、今回はWindows 11のノートPCでUnreal Engine 5.3を実行しています。
HTTP Blueprintが実験段階なので、他のバージョンだと扱いが異なるかもしれません。
Node-REDで受信して保存する
先にNode-REDのセットアップを行います。
▼Node-REDのインストールについて、Windowsの場合は以下のページをご覧ください。Node.jsが必要です。
https://nodered.jp/docs/getting-started/windows
▼私はNode-REDをExpressで起動しているので、システムにグローバルインストールしたNode-REDとは若干異なります。
UE5からHTTP通信で送られてきたPOSTリクエストを受信するのは、以下のフローだけでできます。
▼http inノードで受信して、レスポンスをhttp outノードで返しています。受信したデータはデバッグノードにつながっています。
ノードをダブルクリックすると、ノードの設定を変更できます。
▼http inノードだけ、メソッドをPOST、URLを/csvに変更しています。
この後の手順でUE5から受信したデータがNode-REDのデバッグウィンドウに表示されます。
▼以下のように表示されます。
JSON形式で受信したデータをCSV形式に変換するので、csvノードを利用します。デバッグノードの間に入れるだけです。
▼コンマ区切りのデータになっています。
さらにファイルに保存するために、write fileノードを利用します。
▼csvノードの後ろに接続します。
▼csvノードの中身を見ると、Outputには改行が入っているようです。
write fileノードでは保存するファイルの名前を指定します。一つのcsvファイルに追記していくようにします。
▼改行はcsvノードで入るので、追加しないようにしています。
今回はファイルのパスを直接指定していますが、もちろん変数にして動的に変更することもできます。日付にしたり、試行回数にしたりできます。
▼うまくいくと以下のように保存されます。
Node-REDの設定はこれだけです。ノードだけで見ると、6つしか使っていません。
UE5からデータを送信する
プレイヤーの座標を取得する
UE5からプレイヤーの座標を送信します。
▼サードパーソンのプロジェクトを新規に作成しました。
▼プロジェクトを作成後、エディタ画面が起動しました。
▼レベルをプレイするとThirdPerson Characterを操作できるようになります。
UE5でHTTP通信とJSON形式のデータを扱うために、HTTP Blueprintプラグインを追加します。
▼編集→プラグインを選択します。
▼HTTP Blueprintを選択して、再起動します。
レベルブループリントでプレイヤーの座標を取得します。
▼レベルブループリントを開きました。
Get Player PawnノードとGet Actor Transformを接続しました。
▼構造体ピンを分割していくと、それぞれの座標を取得することができます。
今回はRotationとScaleは不要なので、Get Actor TransformノードではなくGet Actor Locationノードでもいいです。
この後HTTP Blueprintで通信するには、JSON形式の文字列に変換する必要があります。Set Fieldノードでx、y、zという名前のフィールドにそれぞれの座標を渡します。
▼Tickごとに取得するようにします。
それぞれのJson Objectは同じものを利用するので、変数にしておきます。
▼Json Objectを右クリックして、「変数へ昇格」を選択して変数を作成します。
各座標を代入したJson Objectを、Get Json Stringノードで文字列に変換します。
▼デバッグするためにPrint Stringノードにつなぎました。
▼フロー全体は以下のようになります。
この状態でレベルをプレイしてみます。
▼うまくいけば、プレイヤーの座標が出力されます。
HTTP Blueprintを利用する
ここからHTTP Requestノードを利用するのですが、注意点があります。
使い方を間違えると、すぐにエディタがクラッシュします。コンパイルした時点でクラッシュします。実験段階らしいバグが残っているのだと思います。
間違えたまま保存すると復旧が難しいので、HTTP Requestノードに接続する前に保存したり、GitHubなどでバージョンを管理して戻せるようにしておいてください。
▼GitHubでの管理については以前試したことがあります。
さてHTTP Requestノードにつないでいくのですが、Result BodyがStringかStructでないとエラーが出るようです。今回はPrint Stringに接続しておきます。表示が不要であれば、詳細を開いてPrint to Screenをオフにしてください。
▼Result Bodyにつないでいない状態だと以下のエラーが起きました。
▼先程プレイヤーの座標を取得していた文字列を、HTTP RequestのBodyとして接続しています。
注意点として、HTTP RequestのBodyにString以外を接続するとクラッシュします。例えばJson Objectを接続するとクラッシュしました。
通信先のURLは、グローバルインストールしたNode-REDであればhttp://localhost:1880/csvになります。お使いの環境に合わせて変更してください。VerbはPostリクエストを送信するのでPostです。
ここでさらに注意点なのですが、アクセス先のサーバーを立てていない状態でレベルをプレイするとクラッシュする場合があります。先にNode-REDの設定を行ったのはこの問題を回避するためです。
別の要因でクラッシュしていたのかもしれませんが、Node-REDを起動してからプレイするようにしてください。
▼フロー全体は以下のようになります。
CSVファイルを確認する
これまでの設定がうまくいけば、プレイ後にCSVファイルが保存されています。
▼Excelで開くと、ロックされているようでした。読み取り専用で開きました。
▼実際に少しだけ動いたときに保存されたCSVファイルは以下のようになりました。
▼座標の変化が見られました。
CSVファイルから3次元空間上に位置をプロットする
CSVファイルの座標を読み込み、Pythonで3次元空間上の位置をプロットしてみます。
コードはChatGPTに以下のように書いてもらいました。
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# CSVファイルのパスを指定
csv_file = "test.csv"
# CSVファイルを読み込み(列名がないためheader=Noneを指定)
data = pd.read_csv(csv_file, header=None)
# 各列をx, y, zとして取得
x = data[0]
y = data[1]
z = data[2]
# 3Dプロットの作成
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# 点をプロット
ax.scatter(x, y, z, c='b', marker='o')
# 軸ラベルを設定
ax.set_xlabel('X Axis')
ax.set_ylabel('Y Axis')
ax.set_zlabel('Z Axis')
# グラフを表示
plt.show()
実行するにはpandasとmatplotlibをpipでインストールする必要があります。
Pythonの実行は、私が開発したpython-venvノードを利用してNode-REDで実行しました。Pythonの仮想環境を作成し、その環境にパッケージをインストールしたり、コードを実行したりできます。
▼年末に開発の変遷を書きました。
https://qiita.com/background/items/d2e05e8d85427761a609
事前にPythonはインストールしておいてください。
▼フローはこちら
[{"id":"d144b536869d75b2","type":"venv","z":"9eee18ddb14cddf9","venvconfig":"199902437e3e4153","name":"","code":"import pandas as pd\nimport matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D\n\n# CSVファイルのパスを指定\ncsv_file = \"test.csv\"\n\n# CSVファイルを読み込み(列名がないためheader=Noneを指定)\ndata = pd.read_csv(csv_file, header=None)\n\n# 各列をx, y, zとして取得\nx = data[0]\ny = data[1]\nz = data[2]\n\n# 3Dプロットの作成\nfig = plt.figure()\nax = fig.add_subplot(111, projection='3d')\n\n# 点をプロット\nax.scatter(x, y, z, c='b', marker='o')\n\n# 軸ラベルを設定\nax.set_xlabel('X Axis')\nax.set_ylabel('Y Axis')\nax.set_zlabel('Z Axis')\n\n# グラフを表示\nplt.show()\n","continuous":false,"x":1170,"y":2080,"wires":[["f85d8c596a9ee823"]]},{"id":"497ca2167e3921af","type":"pip","z":"9eee18ddb14cddf9","venvconfig":"199902437e3e4153","name":"","arg":"pandas matplotlib","action":"install","tail":false,"x":1170,"y":2020,"wires":[["831dd2611e4f05a3"]]},{"id":"18de830787c6aa1a","type":"inject","z":"9eee18ddb14cddf9","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1030,"y":2020,"wires":[["497ca2167e3921af"]]},{"id":"831dd2611e4f05a3","type":"debug","z":"9eee18ddb14cddf9","name":"debug 384","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1330,"y":2020,"wires":[]},{"id":"b8a5da16d21f76e9","type":"inject","z":"9eee18ddb14cddf9","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1030,"y":2080,"wires":[["d144b536869d75b2"]]},{"id":"f85d8c596a9ee823","type":"debug","z":"9eee18ddb14cddf9","name":"debug 385","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1330,"y":2080,"wires":[]},{"id":"199902437e3e4153","type":"venv-config","venvname":"UE","version":"3.10"}]
▼pipノードでpandasとmatplotlibをインストールしています。
▼venvノードでpipノードと同じ仮想環境名を指定し、コードを入れています。
pipノードでパッケージをインストール後、コードを実行してみました。
▼それっぽいデータが出てきました。実際ぴょんぴょん跳んでいました。
今回はPythonのノードだけで実行しましたが、例えばUE5でプレイを終了するときにNode-REDに終了したという信号を送って、自動的にプロットしたデータを表示するといった処理につなげることができます。
最後に
UE5でファイルへの保存をC++で実装するのは大変なことになりそうですが、Node-REDと通信すると簡単にできました。さらに他の処理につなげることもできます。
今回はLocationだけ取得しましたが、他の情報も同様に取得できそうです。時系列データやRotationのデータも含めてプロットすると、動きが見やすくなるかもしれません。
Node-REDなら簡単にHTTP通信のエンドポイントを作成できるのですが、UE5 だと今回利用したプラグインではリクエストしか送信できないようです。UE5でもエンドポイントを作成できるとNode-REDとの通信がさらに楽になるので、方法を探したいなと思っています。