目次
1. はじめに
2. シリアル通信とは
3. プラグインの紹介
4. 実装方法
5. Tips集
まとめ
1. はじめに
自己紹介
初めて記事を書くため、お手柔らかにお願いします。
普段は自作コントローラーを使ったゲーム開発をしているのですが、今回は開発中の自作コントローラーとUnrealEngineの連携方法について紹介できればと思います。
概要
この記事はUnreal Engine (UE) Advent Calendar 2023、20日目の記事です。
現在開発中の自作コントローラーとUEの連携はPCにUSBまたはBluetoothで接続し、シリアル通信で行っています。今回は、そのシリアル通信を実現しているプラグインについて紹介します。紹介するのは以下の内容です。
- シリアル通信とは
- プラグインの紹介
- デバイスの実装方法
- UnrealEngineの実装方法
- Tips
環境&前提条件
- OS:Windwos10
- UE5.3.2(実装はブループリントのみ)
- 使用プラグイン:SerialCOM
- シリアル通信をするデバイス:M5Stack Gray
2. シリアル通信とは
1対1を想定した通信手段の一つです。通信には、USB/LANケーブル/Wi-Fi/Bluetoothなどを通してデータの送受信ができ、他の手段よりもシンプルで簡単に通信することが出来ます。実例としてはSwitchのJoy-Conも本体と合体している時は、シリアル通信を使用しているそうです。
通信方法
シリアル通信は【COMポート】で通信をするため、このCOMポートを制御する処理が必要です。Windowsでは、デバイスマネージャーで確認できる【ポート(COMとLPT)】が現在接続されているポートになっています。僕の場合はCOM1,3,4,5,6,8を使用していますね。
今回紹介するプラグインはCOMポートの制御がブループリントで出来るようになっており、接続先のデバイスさえ用意すれば、誰でも簡単にシリアル通信が出来ます!便利!
3. プラグインの紹介
UE4とUE5でそれぞれ別のプラグインを使用します。UE4Duinoが元祖で、SerialCOMはUE4DuinoをUE5向けに改修・アップデートしたものになります。
UE4プラグイン
UE5プラグイン
ノード紹介
基本的に、接続系ノードで【シリアル通信の設定とデバイスの接続】を行い、データ処理系ノードで【データの読み書き】を行います。
OpenSerialPort
基本はこのノードを使用して、指定したCOMポートの番号とボーレートでCOMポートに接続します。他のwithFlowControlなどは、シリアル通信にあるフロー制御を使う場合のみ使用します。(※フロー制御はデフォルトでfalseです。)
FlushSerialPort
接続後にCOMポートに溜まっているデータをリセットしてくれます。
CloseSerialPort
COMポートを閉じます。必ず接続解除する際やEndPlay時にこの処理を通さないとCOMポートに接続できなくなるため、注意しましょう。
純粋関数
接続中のCOMポートに関する情報を取得できます。CloseSerialPortをする前にisSerialPortOpen?でCOMポートの状態を確認したりします。
SerialWrite〇〇
シリアル通信で接続しているデバイスに情報を送信します。送信するデータ型ごとにノードが用意されています。
SerialRead〇〇
シリアル通信で接続しているデバイスから情報を受信します。受信成功の有無はSuccessから確認でき、ReturnValueから実際に読み取ったデータを取得します。
SerialPrint
String型でデバイスに送信します。SerialWriteとやっていることは同じですが、マイコンで送信プログラムを書く際もString型とそれ以外で使用する関数がWriteとPrintに分かれているので、表記を合わせているんだと思います。
純粋関数
送受信するデータの型変換に使用します。
4. 実装方法
デバイスの準備
まずはシリアル通信を行うデバイスを用意します。今回の記事では、手元にあるM5StackGrayを使用します。M5Stackなどへのプログラムの書き込み方の説明は省きます。以下のようなサイトを参考にしてみてください。
とりあえず、簡単に0.1秒ごとに加算した数を送信するプログラムを用意しました。
これをArudinoIDEで書き込み、ArudinoIDEのシリアルモニタで確認してみます。
#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と接続しているので、デバイスマネージャーを確認するとポート(COMとLPT)の所にUSBと表記があるCOM6に接続されていました。
ArudinoIDEのシリアルモニタを使うことでシリアル通信を確認できるので、COM6を見たところ、0.1秒ごとに加算されたデータが受信出来ているのを確認できました。これでデバイスは問題なさそうです。
UnrealEngineの実装
デバイスの用意はできたので、UnrealEngineの実装を進めます。今回はUE5.3.2を使用します。
まずは、SerialCOMのGitHubからUnrealEngineのバージョンに合ったプラグインをダウンロードします。GitHub内のDownloads (Releases)の一覧に各バージョンが用意されています。
使用するプロジェクトにプラグインを導入後、テスト用にActorクラスのブループリントを作成し、レベルに配置します。作成したクラスに以下の内容を実装します。
BeginPlay
今回はBeginPlayでゲーム開始時にCOMポートへの接続処理を行います。僕の環境では、M5StackはCOM6に接続されていたので、OpenSerialPortでポート6番、ボーレート115200に設定し、COMポートの接続処理を実行します。その後、Branchで接続に成功した場合は、Serialの参照を保存し、FlushSerialPortで溜まったデータを一度綺麗にします。
※ポート番号とボーレートは使用環境で異なります。ポート番号はデバイスマネージャーで、ボーレートは使用するデバイスの設定を確認してください。M5StackGrayの場合はボーレートの初期値が115200でした。
Tick
Tickでは、IsValidで変数があるかをチェックしつつ、SerialReadStringでシリアル通信のデータを受信します。受信に成功した場合は、PrintStringで画面に流します。
EndPlay
最後に、ゲーム終了時に呼ばれるEndPlayでCOMポートを閉じる処理を行います。isSerialPortOpen?でCOMポートが開いているかを確認しつつ、開いたままならFlushSerialPortでデータをクリアし、CloseSerialPortでCOMポートを閉じます。この時のFlushは必要ないかもしれないですが何となく付けています。最後に、変数を空にして完了です。
※CloseSerialPortは忘れずに追加してください。
この処理をせずにゲームを実行するとCOMポートが開きっぱなしになるのですが、一度開いたCOMポートは閉じるまで使えなくなるため、COMポートに接続できなくなります。
もし、CloseSerialPortを忘れた場合は、一度デバイスの接続を切って(USBを抜く、Bluetoothを切る etc.)繋ぎ直すことで元通りになります。
実践
ゲームを実行すると、「接続」の文字の後に、データの受信に成功しました!
デバイスとの接続が出来たので、後はデバイスとゲームが連携できるように処理を自由に書いてあげればOKです。また、UE4でもUE4Duinoプラグインで同じ名前のノードを使うことで接続ができます。
5. Tips集
Tips①:プラグインはWindows限定
UE4Duino/SerialCOMどちらもWindows限定のプラグインになっています。
SerialCOMの方はWindwos限定の記載はありませんが、ソースコードを確認するとUE4Duino同様のWindwos用ライブラリを使用していたので、Windwosしか使えません。
MacやLinux用のプラグインは今のところ無さそうなので、プラグインの改造が必要です。
XでMac/Linux用のプラグインを紹介いただきました!ありがとうございます!
Tips②:間違ったCOMポートを開こうとするとフリーズする
具体的には【過去に接続して、デバイスマネージャーに表示されているが、現在通信していないCOMポート】の番号を開こうとすると一瞬ゲームがフリーズします。記事序盤のデバイスマネージャーにあったCOM3,4,5,8がそうです。
SwitchのJoy-Conみたいに自作コントローラーを探して接続する処理が組めないかと思い、色々やっている時に気が付いたんですが、僕の知識ではまだ解決不可能でした。
現状判明している事
SerialCOMのソースコードを色々と解析したところ、SerialCom.cppの190行目にあるこのif文が悪さをしていることは分かりました。特に、Windwos側のライブラリのSetCommState()とSetupComm()が悪さをしているようで、この二つが結果を返すまで2秒くらいフリーズします。
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の結果を待っているためだと思われます。恐らく、画面描画処理と同じスレッドで接続系の処理が実行されるため、Windwosからの結果を待つ間に描画の更新がストップし、一瞬フリーズが発生するというのが今回の問題だと予想しています。
そのため、ヒストリア様で紹介されているような、メインとは別のスレッドで接続処理が必要なんだと思っていますが、プランナー本職の自分には少し難しいので、ノンビリ勉強中です。
まとめ
いかがでしたでしょうか。今回はUEの基本操作を理解した初心者以上の方が実装できるようにまとめてみました。今回初めて記事を書くので、間違いがありましたら、コメントいただけますと幸いです。
シリアル通信は他の通信手段よりもシンプル&簡単で、誰でも挑戦しやすいので、この機会に【自作コントローラー ✕ ゲーム】で新しいエンタメに挑戦しましょう!