【UE5】外部接続UDP編~UE内の車を動かしてみる~

2024年7月に開催しました「UE活用ウェビナー 外部通信編」で紹介しました、UDP通信について、実際にUEで作成しながら概要を説明します。

今回はこのように、UDP信号を送信できる同一PC内のソフトウェアから、UEに配置したアクターを制御していきます。

UDPについて

まず、UDP通信について軽く復習していきます。

2024年7月 UE活用ウェビナー 外部通信編 より

別アプリケーションと、大量の信号を送受信する必要がある際に、UDP(User Datagram Protocol)が利用することが多い通信方式です。

2024年7月 UE活用ウェビナー 外部通信編 より

産業機器などの制御用アプリからの制御信号をUEで受け取り、リアルタイムにアニメーションに反映する際に利用し、高速なデータ転送が求められるシミュレーションアプリでは特に有用です。

2024年7月 UE活用ウェビナー 外部通信編 より

弊社で制作している、DMU(デジタルモックアップ)でもUDP通信を利用しギミックやLEDによるランプ演出を制御しています。
※お客様のエミュレーター・シミュレーター・ハードなどの環境に合わせ通信方式は変更しています

このように、産業機器では多く利用されているUDP通信ですが、当記事筆記時点では残念ながらUEの標準機能ではUDP受信の機能は実装されておらず、プラグインを利用したりc++での自前でのUDP受信開発が必要になります。

UEプロジェクトでUDPを受信できるようにするには

今回は、プラグインは利用せず C++でシンプルなUDP受信クラスを作成する方法で紹介します。

要点を明確にするため、例外処理がなかったり、最低限なプログラムをしています
そのまま業務利用するには耐えられない状態となっていますので、ご利用の際はご注意ください

弊社 X(旧Twitter)で何度か登場している、ランクル200のプロジェクトを利用します。
こちらのUEプロジェクトですが、ブループリント用に作成しています。

「ツール」⇒「新規C++クラス」をクリックします

「全てのクラス」⇒「Object」⇒「次へ」をクリックします

ファイル名を入力し「クラスを作成」をクリックします
今回はファイル名を「UdpReceiver」としました

追加したクラスの UdpReceiver.cpp と UdpReceiver.h が開かれた状態でVisualStudioが起動します

UdpReceiver.cpp に ソースコードを作成していきます

#include "UdpReceiver.h"
#include "Async/Async.h"

// シングルトンインスタンスの初期化
UUdpReceiver* UUdpReceiver::Instance = nullptr;

UUdpReceiver::UUdpReceiver()
    : ReceiverSocket(nullptr), Port(0), IPAddress(TEXT("127.0.0.1"))
{
}

UUdpReceiver* UUdpReceiver::GetInstance()
{
    if (!Instance)
    {
        Instance = NewObject<UUdpReceiver>();
        Instance->AddToRoot(); // GC防止
    }
    return Instance;
}

void UUdpReceiver::InitializeReceiver(const FString& InIPAddress, int32 InPort)
{
    IPAddress = InIPAddress;
    Port = InPort;

    // ソケットの作成
    FIPv4Address IP;
    if (!FIPv4Address::Parse(IPAddress, IP))
    {
        UE_LOG(LogTemp, Error, TEXT("Invalid IP Address: %s"), *IPAddress);
        return;
    }

    FIPv4Endpoint Endpoint(IP, Port);

    ReceiverSocket = FUdpSocketBuilder(TEXT("UdpReceiver"))
                        .AsNonBlocking()
                        .WithReceiveBufferSize(1024)
                        .BoundToEndpoint(Endpoint);

    if (!ReceiverSocket)
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to create UDP socket!"));
    }
    else
    {
        UE_LOG(LogTemp, Log, TEXT("Socket initialized at %s:%d"), *IPAddress, Port);
    }
}

void UUdpReceiver::StartReceiving()
{
    if (ReceiverSocket)
    {
        StopReceivingThread.Reset();

        // 非同期でデータ受信を開始
        AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [this]()
        {
            ReceiveData();
        });
    }
}

void UUdpReceiver::StopReceiving()
{
    StopReceivingThread.Increment();

    if (ReceiverSocket)
    {
        // ソケットを閉じる
        ReceiverSocket->Close();
        ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(ReceiverSocket);
        ReceiverSocket = nullptr;

        UE_LOG(LogTemp, Log, TEXT("Socket closed."));
    }

    // インスタンスを破棄
    if (Instance)
    {
        Instance->RemoveFromRoot(); // GC対象に戻す
        Instance = nullptr;
        UE_LOG(LogTemp, Log, TEXT("Instance destroyed."));
    }
}

void UUdpReceiver::ReceiveData()
{
    const int32 BufferSize = 1024;
    while (StopReceivingThread.GetValue() == 0)
    {
        if (ReceiverSocket)
        {
            TArray<uint8> ReceivedData;
            ReceivedData.SetNumUninitialized(BufferSize);

            int32 BytesRead = 0;
            if (ReceiverSocket->Recv(ReceivedData.GetData(), ReceivedData.Num(), BytesRead, ESocketReceiveFlags::None))
            {
                if (BytesRead > 0)
                {
                    ReceivedData.SetNum(BytesRead);
                    OnDataReceived(ReceivedData);
                }
            }
        }

        FPlatformProcess::Sleep(0.01f); // CPU負荷軽減
    }
}

void UUdpReceiver::OnDataReceived(const TArray<uint8>& Data)
{
    FString DataString;
    for (const uint8& Value : Data)
    {
        DataString.AppendChar(static_cast<TCHAR>(Value)); // ASCII文字に変換して追加
    }

    UE_LOG(LogTemp, Log, TEXT("Received Data (as characters): %s"), *DataString);

    // スレッドセーフにデータを格納
    FScopeLock Lock(&DataCriticalSection);
    RetVal = DataString;
}

FString UUdpReceiver::GetUDPData()
{
    FString Data;
    FScopeLock Lock(&DataCriticalSection);
    Data = RetVal;
    return Data;
}

UdpReceiver.h に ヘッダーコードを作成していきます

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Networking.h"
#include "Sockets.h"
#include "SocketSubsystem.h"
#include "IPAddress.h"
#include "UdpReceiver.generated.h"

/**
 * UDP Receiver クラス
 */
UCLASS(Blueprintable, BlueprintType)
class BLOGLANDCRUISER200_API UUdpReceiver : public UObject
{
    GENERATED_BODY()

public:
    // シングルトンインスタンス取得
    UFUNCTION(BlueprintCallable, Category = "UDP")
    static UUdpReceiver* GetInstance();

    // ソケットの初期化
    UFUNCTION(BlueprintCallable, Category = "UDP")
    void InitializeReceiver(const FString& IPAddress, int32 Port);

    // データ受信開始
    UFUNCTION(BlueprintCallable, Category = "UDP")
    void StartReceiving();

    // 受信停止
    UFUNCTION(BlueprintCallable, Category = "UDP")
    void StopReceiving();

    // UDPデータ取得
    UFUNCTION(BlueprintCallable, Category = "UDP")
    FString GetUDPData();

private:
    // コンストラクタ(外部からの直接生成を禁止)
    UUdpReceiver();

    // ソケットのハンドル
    FSocket* ReceiverSocket;

    // 受信するためのポート
    int32 Port;

    // IPアドレス
    FString IPAddress;

    // スレッド制御
    FThreadSafeCounter StopReceivingThread;

    // スレッドセーフなデータアクセス
    FCriticalSection DataCriticalSection;

    // 受信データ
    FString RetVal;

    // 受信処理
    void ReceiveData();

    // データを処理するためのコールバック
    void OnDataReceived(const TArray<uint8>& Data);

    // シングルトンインスタンス
    static UUdpReceiver* Instance;
};

※今回はソースコードの詳しい説明は省略します
 繰り返しとなりますが、要点を明確にするため最低限のソースとなります
 ご利用は、自己責任でよろしくお願いいたします

コードを作成したら「ビルド」⇒「ソリューションのリビルド」をクリックします

ビルドエラーが発生しました
エラー内容を確認し、対応します

今回は、致命的になっていたエラーは
エラー C2065 FUdpSocketReceiverが定義されていない
ということで、diprossTools.Build.cs に
“Networking”と”Sockets” 依存モジュールを追加しました

全てのビルドエラーを取り除き、デバッグモードで起動するとUEが起動します。
デバッグモードで起動することで、記載したソースコードにブレイクポイントをたて細かくデバッグが可能な状態で起動します

一見、普通にUEが起動しているように見えます

今回は、車が入っているアクター「BP_Car」に先程作成したC++クラスのUDP受信機能を入れていきます

UDPで検索すると、作成したC++で作成したブループリントノードが表示されます

今回作成したクラスでは、UDP受信用のインスタンスを作成し、初期化 その後 通信開始という流れとなるよう作成しました

Event Tickで、受信し保管している値を取得しにいきます
Event End Play でインスタンスを破棄することでポートを開放します

取得した値をPrintStringするシンプルなブループリントが完成しました

こちらが、UDP信号を送信するアプリケーションです
今回はVisualBasicで最低限の送信プロジェクトを作成してみました

    Private Sub SendUdpMessage(sender As Object, e As EventArgs)
        Try
            ' ポート番号とメッセージを取得
            Dim port As Integer = Integer.Parse(tb_port.Text)
            Dim message As String = tb_1.Value.ToString() + "|" + tb_2.Value.ToString() + "|" + tb_number.Text

            ' UDPクライアントを作成
            Dim udpClient As New UdpClient()

            ' 送信先IP(例としてlocalhost)
            Dim ipAddress As IPAddress = IPAddress.Parse(tb_ip.Text)
            Dim endPoint As New IPEndPoint(ipAddress, port)

            ' メッセージをUTF-8エンコードして送信
            Dim sendBytes As Byte() = Encoding.UTF8.GetBytes(message)
            udpClient.Send(sendBytes, sendBytes.Length, endPoint)

            ' UDPクライアントを閉じる
            udpClient.Close()

            ' 現在の送信メッセージを表示
            tb_val.Text = message

        Catch ex As Exception
            ' エラーハンドリング
            MessageBox.Show("Error: " & ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
        End Try
    End Sub

送信部分のVBソースコードです
こちらのプロシージャーを送信ボタン押下で、100ミリ秒間隔で呼びだすプログラムです

パケットキャプチャソフト Wireshark を利用し、正しくUDP送信できているか確認します
※本来はヘッダーやチェックサムを入れてデータの整合性を
 確認する必要がありますが今回は最低限のデータのみ送信しています

UEエディタでPlayし、UDP送信ソフトで信号を送信し、PrintStringで受信データが表示されることを確認できました

100ミリ秒間隔で送信しているので、スライダーやテキストボックスの値を変更するとリアルタイムで値が更新されます

受信したデータで車を動かす

ここからは受信したUDP信号を利用してアクターを動かしていきたいと思います

車を動かすためのカスタムイベントを作成しました
パラメタはString一つのみです

カスタムイベントは、TickでUDP信号受信ノードの後に受信した文字列を引数にして呼び出します

データは、0|0|4649 というように、
前後のスピード|フロントタイヤの角度|ナンバープレート というフォーマット受信しますので
スピードと角度とナンバーに文字列を分けます

ここが、スピードに合わせタイヤを回転させBP_Carをレベル上で移動させる処理とタイヤの角度と車の角度を変える処理です
※こちらも最低限でかなり不自然な動きをしますが要点ではないので作り込みは省略しています

ナンバープレートを変更するブループリントです
力業で作った無駄だらけのロジックですので、引きの状態で失礼します。。。

レベルにカメラを配置します

レベルのブループリントを開き、配置したカメラを利用する設定をします

こちらが完成したアプリをスタンドアローンで実行し、UDP送信アプリケーションから信号を送りUE内の車を動かしている動画です

今回は、同一PC内のアプリから動かしましたが、ネットワーク接続できるESP32からUDP信号を送信したり制御装置からの信号を受信したりとUEの活用範囲がますます広がりそうですね!

▼弊社で開発しているデジタルモックアップのご紹介ページはこちら

エンジニアリング事業部からの最新情報をお届けします