シリアル通信をUnrealEngineで実現する【UE Advent Calendar 2023】

# 目次
[1. はじめに](#1-はじめに)
[2. シリアル通信とは](#2-シリアル通信とは)
[3. プラグインの紹介](#3-プラグインの紹介)
[4. 実装方法](#4-実装方法)
[5. Tips集](#5-tips集)
[まとめ](#まとめ)
# 1. はじめに
### 自己紹介
初めて記事を書くため、お手柔らかにお願いします。:pray_tone2:
普段は自作コントローラーを使ったゲーム開発をしているのですが、今回は開発中の自作コントローラーとUnrealEngineの連携方法について紹介できればと思います。

### 概要
この記事は[Unreal Engine (UE) Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ue)、20日目の記事です。
現在開発中の自作コントローラーとUEの連携はPCに**USB**または**Bluetooth**で接続し、**シリアル通信**で行っています。今回は、そのシリアル通信を実現しているプラグインについて紹介します。紹介するのは以下の内容です。
- シリアル通信とは
- プラグインの紹介
- デバイスの実装方法
- UnrealEngineの実装方法
- Tips

### 環境＆前提条件
- OS：Windwos10
- UE5.3.2（実装はブループリントのみ）
- 使用プラグイン：[SerialCOM](https://github.com/videofeedback/Unreal_Engine_SerialCOM_Plugin)
- シリアル通信をするデバイス：M5Stack Gray

# 2. シリアル通信とは
1対1を想定した通信手段の一つです。通信には、**USB／LANケーブル／Wi-Fi／Bluetoothなど**を通してデータの送受信ができ、他の手段よりもシンプルで簡単に通信することが出来ます。実例としては**SwitchのJoy-Con**も本体と合体している時は、シリアル通信を使用しているそうです。

### 通信方法
シリアル通信は【__COMポート__】で通信をするため、このCOMポートを制御する処理が必要です。Windowsでは、デバイスマネージャーで確認できる【ポート(COMとLPT)】が現在接続されているポートになっています。僕の場合はCOM1,3,4,5,6,8を使用していますね。

今回紹介するプラグインは<font color="red">**COMポートの制御がブループリントで出来る**</font>ようになっており、接続先のデバイスさえ用意すれば、誰でも簡単にシリアル通信が出来ます！便利！

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/33369478-3852-48f9-45a3-a8edc7af5a00.png">

# 3. プラグインの紹介
UE4とUE5でそれぞれ別のプラグインを使用します。UE4Duinoが元祖で、SerialCOMはUE4DuinoをUE5向けに改修・アップデートしたものになります。
### UE4プラグイン
https://github.com/RVillani/UE4Duino
### UE5プラグイン
https://github.com/videofeedback/Unreal_Engine_SerialCOM_Plugin

### ノード紹介
基本的に、接続系ノードで【**シリアル通信の設定とデバイスの接続**】を行い、データ処理系ノードで【**データの読み書き**】を行います。

**接続系ノード**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/40be5c44-f4fe-1dcf-fe93-cad3fec0b7ea.png">
>**OpenSerialPort**
基本はこのノードを使用して、指定したCOMポートの番号とボーレートでCOMポートに接続します。他のwithFlowControlなどは、シリアル通信にあるフロー制御を使う場合のみ使用します。（※フロー制御はデフォルトでfalseです。）

>**FlushSerialPort**
接続後にCOMポートに溜まっているデータをリセットしてくれます。

>**CloseSerialPort**
COMポートを閉じます。必ず接続解除する際やEndPlay時にこの処理を通さないと<font color="red">COMポートに接続できなくなる</font>ため、注意しましょう。

>**純粋関数**
接続中のCOMポートに関する情報を取得できます。CloseSerialPortをする前にisSerialPortOpen?でCOMポートの状態を確認したりします。

**データ処理系ノード**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/de172388-e595-205f-339f-d84ace048dd3.png">
>**SerialWrite〇〇**
シリアル通信で接続しているデバイスに情報を送信します。送信するデータ型ごとにノードが用意されています。

>**SerialRead〇〇**
シリアル通信で接続しているデバイスから情報を受信します。受信成功の有無はSuccessから確認でき、ReturnValueから実際に読み取ったデータを取得します。

>**SerialPrint**
String型でデバイスに送信します。SerialWriteとやっていることは同じですが、マイコンで送信プログラムを書く際もString型とそれ以外で使用する関数がWriteとPrintに分かれているので、表記を合わせているんだと思います。

>**純粋関数**
送受信するデータの型変換に使用します。


# 4. 実装方法
## デバイスの準備
まずはシリアル通信を行うデバイスを用意します。今回の記事では、手元にある**M5StackGray**を使用します。M5Stackなどへのプログラムの書き込み方の説明は省きます。以下のようなサイトを参考にしてみてください。

https://raspberrypi.mongonta.com/howto-start-m5stack-arduinoide/

とりあえず、簡単に0.1秒ごとに加算した数を送信するプログラムを用意しました。
これをArudinoIDEで書き込み、ArudinoIDEのシリアルモニタで確認してみます。
```c
#include <M5Stack.h>
int a = 0;

void setup() {
  M5.begin();
  M5.Power.begin();

  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(GREEN , BLACK);
  M5.Lcd.setBrightness(100);
  M5.Lcd.setTextSize(2);

  M5.Lcd.setCursor(0, 0);
  M5.Lcd.println("M5Stack");
}

void loop() {
  a++;
  
  Serial.println(a);

  M5.Lcd.setCursor(0, 20);
  M5.Lcd.println(a);
  
  M5.update();
  delay(100);
}
```
今回のM5StackはUSBでPCと接続しているので、デバイスマネージャーを確認すると<b>ポート(COMとLPT)</b>の所にUSBと表記があるCOM6に接続されていました。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/d7ad274b-72ca-798a-d225-8a183bfa9c8c.png">

ArudinoIDEのシリアルモニタを使うことでシリアル通信を確認できるので、COM6を見たところ、0.1秒ごとに加算されたデータが受信出来ているのを確認できました。これでデバイスは問題なさそうです。

![シリアル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/16ec28a1-fbf7-509e-ac59-1151f30c14bb.gif)

## UnrealEngineの実装
デバイスの用意はできたので、UnrealEngineの実装を進めます。今回はUE5.3.2を使用します。

まずは、[SerialCOM](https://github.com/videofeedback/Unreal_Engine_SerialCOM_Plugin)のGitHubからUnrealEngineのバージョンに合ったプラグインをダウンロードします。GitHub内の[Downloads (Releases)](https://github.com/videofeedback/Unreal_Engine_SerialCOM_Plugin#downloads-releases)の一覧に各バージョンが用意されています。

使用するプロジェクトにプラグインを導入後、テスト用にActorクラスのブループリントを作成し、レベルに配置します。作成したクラスに以下の内容を実装します。

#### BeginPlay
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/2e543d69-57ba-fe4c-c3f9-a5e89562834a.png)
今回は**BeginPlay**でゲーム開始時にCOMポートへの接続処理を行います。僕の環境では、M5StackはCOM6に接続されていたので、<font color="red">OpenSerialPortでポート6番、ボーレート115200</font>に設定し、COMポートの接続処理を実行します。その後、Branchで接続に成功した場合は、Serialの参照を保存し、FlushSerialPortで溜まったデータを一度綺麗にします。

:::note warn
<b><INS>※ポート番号とボーレートは使用環境で異なります。</INS></b>ポート番号はデバイスマネージャーで、ボーレートは使用するデバイスの設定を確認してください。M5StackGrayの場合はボーレートの初期値が115200でした。
:::

#### Tick
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/1c8dfe24-8bb9-ca55-e9ab-c032a808e0f0.png)
Tickでは、IsValidで変数があるかをチェックしつつ、SerialReadStringでシリアル通信のデータを受信します。受信に成功した場合は、PrintStringで画面に流します。

#### EndPlay
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/87288e4a-2ae0-5bfa-e3da-3fa016a144b9.png)
最後に、ゲーム終了時に呼ばれるEndPlayでCOMポートを閉じる処理を行います。isSerialPortOpen?でCOMポートが開いているかを確認しつつ、開いたままならFlushSerialPortでデータをクリアし、CloseSerialPortでCOMポートを閉じます。この時のFlushは必要ないかもしれないですが何となく付けています。最後に、変数を空にして完了です。

:::note warn
<b>※CloseSerialPortは忘れずに追加してください。</b>
この処理をせずにゲームを実行するとCOMポートが開きっぱなしになるのですが、一度開いたCOMポートは閉じるまで使えなくなるため、COMポートに接続できなくなります。
もし、CloseSerialPortを忘れた場合は、一度デバイスの接続を切って（USBを抜く、Bluetoothを切る etc.）繋ぎ直すことで元通りになります。
:::

## 実践
![接続成功例 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3594163/d59cd080-6de3-1529-0d47-4feef58f07ae.gif)
ゲームを実行すると、「接続」の文字の後に、データの受信に成功しました！
デバイスとの接続が出来たので、後はデバイスとゲームが連携できるように処理を自由に書いてあげればOKです。また、UE4でもUE4Duinoプラグインで同じ名前のノードを使うことで接続ができます。




# 5. Tips集
## Tips①：プラグインはWindows限定
UE4Duino／SerialCOMどちらも**Windows限定のプラグイン**になっています。
SerialCOMの方はWindwos限定の記載はありませんが、ソースコードを確認するとUE4Duino同様のWindwos用ライブラリを使用していたので、Windwosしか使えません。

MacやLinux用のプラグインは今のところ無さそうなので、プラグインの改造が必要です。

<b><font color="red">XでMac／Linux用のプラグインを紹介いただきました！ありがとうございます！</font></b>

https://x.com/Nekomimimofmof/status/1737335702545969170?s=20

https://github.com/waka2ya/ArduinoKit

## Tips②：間違ったCOMポートを開こうとするとフリーズする
具体的には【<font color="red">過去に接続して、デバイスマネージャーに表示されているが、現在通信していないCOMポート</font>】の番号を開こうとすると一瞬ゲームがフリーズします。記事序盤のデバイスマネージャーにあったCOM3,4,5,8がそうです。

SwitchのJoy-Conみたいに自作コントローラーを探して接続する処理が組めないかと思い、色々やっている時に気が付いたんですが、僕の知識ではまだ解決不可能でした。

### 現状判明している事
SerialCOMのソースコードを色々と解析したところ、SerialCom.cppの190行目にあるこのif文が悪さをしていることは分かりました。特に、Windwos側のライブラリの[SetCommState()](https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-setcommstate)と[SetupComm()](https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-setupcomm)が悪さをしているようで、この二つが結果を返すまで2秒くらいフリーズします。

```c++
if (!SetCommState(m_hIDComDev, &dcb) || !SetupComm(m_hIDComDev, 10000, 10000) || m_OverlappedRead->hEvent == NULL || m_OverlappedWrite->hEvent == NULL)
    {
		unsigned long dwError = GetLastError();
		if (m_OverlappedRead->hEvent != NULL) CloseHandle(m_OverlappedRead->hEvent);
		if (m_OverlappedWrite->hEvent != NULL) CloseHandle(m_OverlappedWrite->hEvent);
		CloseHandle(m_hIDComDev);
		m_hIDComDev = NULL;
		UE_LOG(LogTemp, Error, TEXT("Failed to setup port COM%d. Error: %08X"), nPort, dwError);
		return false;
	}
```

### 今後試そうと思っている解決策
※勉強中なので間違いがあったらすみません！
フリーズが発生するのは、**メインスレッド上でWindwosの結果を待っている**ためだと思われます。恐らく、画面描画処理と同じ[スレッド](https://wa3.i-3-i.info/word12453.html)で接続系の処理が実行されるため、Windwosからの結果を待つ間に描画の更新がストップし、一瞬フリーズが発生するというのが今回の問題だと予想しています。

そのため、ヒストリア様で紹介されているような、メインとは別のスレッドで接続処理が必要なんだと思っていますが、プランナー本職の自分には少し難しいので、ノンビリ勉強中です。

https://historia.co.jp/archives/6014/


# まとめ
いかがでしたでしょうか。今回はUEの基本操作を理解した初心者以上の方が実装できるようにまとめてみました。今回初めて記事を書くので、間違いがありましたら、コメントいただけますと幸いです。

シリアル通信は他の通信手段よりもシンプル＆簡単で、誰でも挑戦しやすいので、この機会に【自作コントローラー ✕ ゲーム】で新しいエンタメに挑戦しましょう！