UnrealEngineとPython間でUDP通信する【UE Advent Calendar 2024】

# 目次
- [1. はじめに](#はじめに)
- [2. 制作背景](#制作背景)
- [3. 実装してみた](#実装してみた)
    - [3-1. UEのプラグイン紹介](#ueのプラグイン紹介)
    - [3-2. デバイスの用意](#デバイスの用意)
    - [3-3. Python実装](#python実装)
    - [3-4. UEの実装](#ueの実装)
- [5. まとめ](#まとめ)

# はじめに
この記事は[Unreal Engine (UE) Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ue)の16日目の記事になります！

今回は<b><font color="#A40606">UnrealEngineとPython間でUDP通信する</font></b>方法を紹介します！
特に、物理デバイス（自作コントローラー）との連携として使ったので、実装例と共に紹介させていただきます！

実装内容は、同じPC内でPythonとUnrealEngineを連携する方法としてUDPを使っている物になります。
実際に作ると⇩このような形で動きます！

https://x.com/DHDNR/status/1842869295086456859

自分で作って思いましたが、めちゃくちゃ反応速度速くてビックリしました。
自作コントローラー作ってる方はぜひやってみてください！


## 制作背景
実装話の前に、ちょっとだけ制作した背景をまとめます。

今回の実装は **【物理デバイス⇔Python⇔UE】** と言った形で間にPythonを噛ますものになりますが、本来はPythonを使わずとも「プラグインを用いてシリアル通信を使う」などの手法で連携を取ることが出来ます。
こちらの詳細は、昨年記事にしているので、そちらをご確認ください！

https://qiita.com/RyogoNagao/items/8aa6523f89f540c2a4df#ue5%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3

ではなぜ、Pythonを使ったのかというと、UE上でシリアル通信のために利用しているプラグインが全ての処理をゲームスレッドで行うために、処理負荷の影響をもろに受け、<b><font color="A40606">ゲーム中に処理落ちが多発したため</font></b>です。

そのため、コントローラー処理を全て外部化すればゲームスレッドに処理負荷の影響を与えない＆疎結合化しやすい！という発想でPythonに手を出した形になります！

# 実装してみた
では、実装の話に移ろうと思います！
以下の流れで実装していきます。

1. デバイスの用意
1. Python実装
1. UEにプラグインを導入
1. UE側に処理の記載

## UEのプラグイン紹介
今回は「ObjectDeliverer」というプラグインを使ってUDP通信を実装しますので、プロジェクトに追加してください！
記事での実装は【UE4.27】で行っていますが、プラグインはUE5まで対応しているので、同じ実装で問題ないと思います！
※Fabのリンクは[こちら](https://www.fab.com/listings/b6ffd7d7-80da-483f-a7fa-09cb46b72651)

https://github.com/ayumax/ObjectDeliverer


## デバイスの用意
今回もM5Stackを事例に実装例を紹介します。
ただし、このデバイスで行っている処理は「**シリアル通信でセンサデータをPCに渡す**」のみなので、同じ様にシリアル通信で送信できればデバイスはなんでも大丈夫です！

``` IMUSerial.c
#define M5STACK_MPU6886
#include <M5Stack.h>

float pitch = 0.0F;
float roll  = 0.0F;
float yaw   = 0.0F;

// the setup routine runs once when M5Stack starts up
void setup() {
  M5.begin();
  M5.Power.begin();
  M5.IMU.Init();
}

void loop() {
  M5.IMU.getAhrsData(&pitch, &roll, &yaw);
  
  Serial.print(pitch);
  Serial.print(",");
  Serial.print(roll);
  Serial.print(",");
  Serial.println(yaw);

  M5.update();

  delay(10);
}
```

## Python実装
次に、Pythonの実装になります！
Pythonでは、以下の機能を実装していきます
- シリアル通信でデバイスからデータの受信
- UDP通信でUEにデータを送信

事前準備として、シリアル通信用pySerialのライブラリが必要です！
`pip install pyserial`を実行して、pySerialライブラリをインストールしてください
（Socketは標準で搭載されているので不要です。）

**実装手順** 
準備が出来たらこのようなフローで実装を進めます。
1. pythonファイルを作成し、コードを添付
1. デバイスをPCに接続
1. デバイスマネージャーで接続したデバイスのCOM番号を確認
    <img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/62e74d51-6e4d-aa27-97d1-ddaa62bfddc4.png">
1. `serial.Serial('COM7', 115200, timeout=1)`という部分のCOM7のところを、確認したCOM番号に変更
1. デバイスが繋がった状態でPythonを実行

実行するとこのような形で、ターミナル上でセンサデータを受け取っているのが確認できます！
実行しているプログラムはCtrl+cキーで停止できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/2c3242aa-74ec-0724-2770-c75af5850826.png)

```SendData2UE.py
import serial
import socket

ser = serial.Serial('COM7', 115200, timeout=1)

udp_ip = "127.0.0.1"  # Localhost IP (same machine)
udp_port = 1235        # 受信側でも同じポート番号を指定
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDPsocketを設定

def UdpSender():
    message = "Hello, World!"  # Message to be sent

    try:
        print(f"Message sent: {message}")
    except:
        sock.close()
        print("通信を終了します")

UdpSender()

while True:
    try:
        # シリアル通信からデータを取得
        val_M5 = ser.readline()
        
        # データ解析
        data_str = val_M5.decode('utf-8').strip()
        # print(data_str)
        data_xyz = data_str.split(',')
        print(data_xyz)
        
        x, y, z = map(float, data_xyz)
        print(f"X={x}, Y={y}, Z={z}")
        ValueOnlyMag = f"{x},{y},{z}".encode()
        sock.sendto(ValueOnlyMag, (udp_ip, udp_port))

    # Ctrl+cで停止
    except KeyboardInterrupt:
        print("キーボードによる停止")
        ser.close()
        sock.close()
```
:::note info
**【コードの中身についてザックリ解説】**
:::
**シリアル通信部**
`serial.Serial('COM7', 115200, timeout=1)`では、指定したCOM番号のデバイスとシリアル通信をセットアップしており、その後、`ser.readline()`で送られてきたデータを行ごとに読み出しています。
デバイスから`pitch, roll, yaw`の形でデータが送られてくるので、「,」区切りでデータを分解し、データの送信形式を整えてUDPで送信しています。

**UDP通信部**
また、今回は同じPC内でUDP通信を行うので、UDP通信で指定するIPアドレスは自分自身を示す「127.0.0.1」のループバックアドレスを指定。
`sock.sendto(送信データ, (IPアドレス, ポート番号))`にて、指定したIP＆ポートにUDPでデータを送信しています。


## UEの実装
最後にUEの実装です！
今回は、ワールド上に設置したCubeをセンサデータで回転させるように実装を行います。

プラグインの使い方は、こちらでも解説されているのでご確認ください！

https://unreal-mmo-dev.com/2024/06/20/devlog-59-udp-with-free-object-deliverer-plugin-in-ue/



まずは、レベル上にCubeを追加します。
少し変形さえると回転方向が分かりやすいです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/075d6ffd-c125-e23d-0711-01e04dbe2a99.png)

次に、レベルブループリントを開いて下図のように実装を行っていきます。
上から順番に紹介します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/09be8494-355b-eee9-d932-48141f09976a.png)

#### ①BeginePlayで実装
今回はテスト実装なので全てBeginePlayにまとめます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/b2978552-c811-8cf2-4528-b8144368f9c9.png)

#### ②接続時の挙動を設定
DelivererManagerクラスを作成し、参照を保持します。このクラスがUDP接続を管理しています。
今回は接続した際に、PrintStringで「接続開始」。接続が切断された場合は「切断」と表示されるようにイベントをバインドしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/55603183-3453-1243-3f85-5bc68ff4621e.png)

#### ③受信した際の挙動を設定
UTF-8のString型で受信するDeliveryBoxクラスを作成し、参照を保存します。
このクラスはマネージャークラスから指定したIPアドレスのデータ送受信を受け取る窓口になっています。

Receivedイベントにバインドすることでデータ受信時の挙動を設定できます。
Pythonからは`x, y, z`の形でデータが送られてくるので、Splitでデータを「,」区切りで分解し、それぞれの回転軸のデータをSetActorRotationでCubeに反映します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/a7bb7740-627c-aacd-a039-bc6914d2c478.png)

#### ④UDP通信を開始
必要なセットアップが出来たので、最後にUDP通信を開始します。

CreateProtocolUdpSocketReciverでUDP通信の受信用ソケットを作成します。引数のBoundPortには、Pythonで指定したポート番号（1235）を入れます。
※その他のプロトコル設定は[こちら](https://github.com/ayumax/ObjectDeliverer/wiki/Change-of-communication-protocol)

CreatePacketRuleNodivisionで受信するパケットのルールを設定します。
NoDivisionなので分割無しで生データを受け取る設定になっています。
※その他のルールは[こちら](https://github.com/ayumax/ObjectDeliverer/wiki/Change-of-data-division-rule)

今回は使いませんが、GetIPAddressinStringなどで接続先のIPアドレスの確認が出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/ba893a43-96c3-49fc-b6af-6aeaa09893bb.png)


これで実装は完了です！
あとは、Pythonを実行してから、UEを実行します。
実際の挙動は以下のような形になります！

https://x.com/DHDNR/status/1842869295086456859

# まとめ
以上になります！
今回の実装は接続と連携のみにフォーカスしてしたため、受信のみの実装ですが、PythonとUEどちらも実装を進めれば、外部デバイスとの連携を外部化してUE側のパフォーマンスに影響を与えないシステムが実装できると思います！

それと今回のPythonコードはほぼChatGPTで生成した物を使っています。
ChatGPTが出力するPythonコードの精度が高く、ほとんど修正無しにPythonを実装出来てめちゃくちゃ便利でした！Python ✖ ChatGPTが便利で良いですねぇ…引き続き使っていきたいと思います！