Unreal Engine 5.4.2 で C++ と Python を WebSocket で通信させる方法

プログラミング

この記事では、初心者でも理解できるように、Unreal Engine 5.4.2 で C++ と Python を WebSocket で通信させる方法を丁寧に解説します。サンプルコードをたくさん使って、わかりやすく説明していきますので、ぜひ最後までお付き合いください。

動作動画

WebSocket について

WebSocket は、クライアントとサーバー間で双方向のリアルタイム通信を可能にするプロトコルです。従来の HTTP 通信とは異なり、一度接続を確立すれば、クライアントとサーバーは自由にデータをやり取りできます。ゲーム開発においても、WebSocket を使うことで、リアルタイムのマルチプレイヤー機能などを実現できます。

プロジェクトの準備

まずは、Unreal Engine 5.4.2 で新しいプロジェクトを作成しましょう。このチュートリアルでは、Third Person テンプレートを使用します。プロジェクト名は WebSocketTestV2 とします。

プロジェクトを作成したら、以下のようにプロジェクトの Build.cs ファイルを編集して、WebSocket モジュールを追加します。

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class WebSocketTestV2 : ModuleRules
{
    public WebSocketTestV2(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "WebSockets", });
    }
}

コードの説明:

  • PublicDependencyModuleNames.AddRange は、プロジェクトで使用するモジュールを指定する部分です。
  • WebSockets を追加することで、WebSocket 機能が使えるようになります。

C++ 側の実装

GameInstance の設定

WebSocket の接続管理は、GameInstance クラスで行います。以下のように、WebSocketTestGameInstance.hWebSocketTestGameInstance.cpp を作成します。

WebSocketTestGameInstance.h:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "IWebSocket.h"
#include "WebSocketTestGameInstance.generated.h"

/**
 * 
 */
UCLASS()
class WEBSOCKETTESTV2_API UWebSocketTestGameInstance : public UGameInstance
{
    GENERATED_BODY()

public:
    virtual void Init() override;
    virtual void Shutdown() override;
    TSharedPtr<IWebSocket> WebSocket;

private:

};

コードの説明:

  • TSharedPtr<IWebSocket> WebSocket; は、WebSocket オブジェクトを格納する変数です。

WebSocketTestGameInstance.cpp:

// Fill out your copyright notice in the Description page of Project Settings.

#include "WebSocketTestGameInstance.h"
#include "WebSocketsModule.h"

void UWebSocketTestGameInstance::Init()
{
    Super::Init();

    if (!FModuleManager::Get().IsModuleLoaded("WebSockets"))
    {
        FModuleManager::Get().LoadModule("WebSockets");
    }

    WebSocket = FWebSocketsModule::Get().CreateWebSocket("ws://localhost:8080");

    WebSocket->OnConnected().AddLambda([]()
        {
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Green, "Successfully connected");
        });

    WebSocket->OnConnectionError().AddLambda([](const FString& Error)
        {
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, Error);
        });

    WebSocket->OnClosed().AddLambda([](int32 StatusCode, const FString& Reason, bool bWasClean)
        {
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, bWasClean ? FColor::Green : FColor::Red, "Connection closed " + Reason);
        });

    WebSocket->OnMessage().AddLambda([](const FString& MessageString)
        {
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Cyan, "Received message: " + MessageString);
        });

    WebSocket->OnMessageSent().AddLambda([](const FString& MessageString)
        {
            GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, "Sent message: " + MessageString);
        });

    WebSocket->Connect();
}

void UWebSocketTestGameInstance::Shutdown()
{
    if (WebSocket->IsConnected())
    {
        WebSocket->Close();
    }
    Super::Shutdown();
}

コードの説明:

  • FModuleManager::Get().LoadModule("WebSockets"); は、WebSocket モジュールをロードします。
  • WebSocket = FWebSocketsModule::Get().CreateWebSocket("ws://localhost:8080"); は、WebSocket オブジェクトを作成し、localhost8080 ポートに接続します。
  • WebSocket->OnConnected() などのラムダ関数は、WebSocket の各イベントに対応する処理を記述します。
  • WebSocket->Connect(); で、WebSocket サーバーに接続します。
  • Shutdown 関数で、WebSocket の接続を切断します。

Character の設定

次に、WebSocketTestV2Character.hWebSocketTestV2Character.cpp を編集して、キー入力に応じて WebSocket でメッセージを送信できるようにします。

WebSocketTestV2Character.h:

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Logging/LogMacros.h"
#include "WebSocketTestV2Character.generated.h"

class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;

DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);

UCLASS(config=Game)
class AWebSocketTestV2Character : public ACharacter
{
    GENERATED_BODY()

    /** Camera boom positioning the camera behind the character */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    USpringArmComponent* CameraBoom;

    /** Follow camera */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
    UCameraComponent* FollowCamera;

    /** MappingContext */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    UInputMappingContext* DefaultMappingContext;

    /** Jump Input Action */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    UInputAction* JumpAction;

    /** Move Input Action */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    UInputAction* MoveAction;

    /** Look Input Action */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    UInputAction* LookAction;

    /** Notify Input Action */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
    UInputAction* NotifyAction;

public:
    AWebSocketTestV2Character();

protected:

    /** Called for movement input */
    void Move(const FInputActionValue& Value);

    /** Called for looking input */
    void Look(const FInputActionValue& Value);

    void NotifyServer();
    void StartNotifyServer();
    void EndNotifyServer();
private:
    bool bIsNotifying = false;

protected:
    // APawn interface
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

    // To add mapping context
    virtual void BeginPlay();

public:
    /** Returns CameraBoom subobject **/
    FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
    /** Returns FollowCamera subobject **/
    FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};

コードの説明:

  • NotifyAction は、WebSocket でメッセージを送信するためのアクションです。

WebSocketTestV2Character.cpp の一部:

void AWebSocketTestV2Character::NotifyServer()
{
    UWebSocketTestGameInstance* GameInstance = Cast<UWebSocketTestGameInstance>(GetGameInstance());

    if (GameInstance)
    {
        if (GameInstance->WebSocket->IsConnected())
        {
            GameInstance->WebSocket->Send("pressed E key");
        }
    }
}

void AWebSocketTestV2Character::StartNotifyServer()
{
    if (!bIsNotifying)
    {
        bIsNotifying = true;
        NotifyServer();
    }
}

void AWebSocketTestV2Character::EndNotifyServer()
{
    bIsNotifying = false;
}

コードの説明:

  • NotifyServer 関数で、GameInstance から WebSocket オブジェクトを取得し、接続中であればメッセージを送信します。
  • StartNotifyServerEndNotifyServer は、キーの押下状態を管理するための関数です。

入力設定

最後に、プロジェクトの入力設定を行います。Project SettingsInput セクションで、NotifyActionE キーにバインドします。

以上で、C++ 側の実装は完了です。

Python 側の実装

次に、Python 側で WebSocket サーバーを実装します。以下のようなコードを websocket_server.py として保存します。

Python 側で WebSocket サーバーを実装する方法を解説します。以下のコードは、websockets ライブラリを使用して、localhost8080 ポートで WebSocket サーバーを起動し、クライアントからの接続を処理します。

import asyncio
import websockets
import uuid
from loguru import logger
import time
from tqdm import tqdm
connected_clients = set()

async def handle_client(websocket, path):
    # このWebSocket接続用のユニークなIDを生成
    connection_id = uuid.uuid4()
    client_address = websocket.remote_address
    client_info = f"接続元 IP: {client_address[0]}, ポート: {client_address[1]}"

    if connection_id not in connected_clients:
        # 初回接続時のみ、WebSocket IDを含むクライアント情報をログに記録し、クライアントに送信
        logger.info(f"クライアント接続: {client_info} (WebSocket ID: {connection_id})")
        connection_info_message = f"サーバーに接続しました。接続情報: {client_info}, WebSocket ID: {connection_id}"
        await websocket.send(connection_info_message)
        logger.info(f"{client_address}に接続情報を送信: {connection_info_message}")
        connected_clients.add(connection_id)

    try:
        async for message in websocket:
            logger.info(f"{client_address}からメッセージを受信 (WebSocket ID: {connection_id}): {message}")
            response = f"サーバーが受信: {message}"

            for i in tqdm(range(10)):
                time.sleep(1)
            await websocket.send(response)
            logger.info(f"{client_address}に応答を送信 (WebSocket ID: {connection_id}): {response}")
    except websockets.ConnectionClosed as e:
        logger.info(f"クライアントによる接続切断: {client_info} (WebSocket ID: {connection_id}), コード: {e.code}, 理由: {e.reason}")
        connected_clients.remove(connection_id)

    logger.info(f"クライアント切断: {client_info} (WebSocket ID: {connection_id})")

async def start_server():
    server = await websockets.serve(handle_client, "localhost", 8080)
    logger.info("WebSocketサーバーを ws://localhost:8080 で起動")
    await server.wait_closed()

if __name__ == "__main__":
    logger.add("websocket_server.log", rotation="1 MB", retention="7 days")
    asyncio.run(start_server())

コードの説明

  1. 必要なライブラリをインポートします。

    • asyncio: 非同期プログラミングのためのライブラリ
    • websockets: WebSocket サーバーを実装するためのライブラリ
    • uuid: 一意の識別子を生成するためのライブラリ
    • loguru: ログ出力を行うためのライブラリ
    • time, tqdm: 処理の遅延とプログレスバーを表示するためのライブラリ
  2. connected_clients 変数を定義します。この変数は、接続中のクライアントの WebSocket ID を格納するための集合です。

  3. handle_client 関数を定義します。この関数は、クライアントからの接続を処理します。

    • 接続したクライアントごとにユニークな WebSocket ID を生成します。
    • クライアントの接続情報(IP アドレスとポート)をログに記録します。
    • 初回接続時のみ、WebSocket ID を含むクライアント情報をログに記録し、クライアントに送信します。
    • クライアントからのメッセージを受信し、ログに記録します。
    • 受信したメッセージに対する応答を生成し、10秒間のプログレスバーを表示した後、クライアントに送信します。
    • クライアントが接続を切断した場合、切断情報をログに記録し、connected_clients から WebSocket ID を削除します。
  4. start_server 関数を定義します。この関数は、WebSocket サーバーを起動します。

    • websockets.serve 関数を使用して、localhost8080 ポートで WebSocket サーバーを起動します。
    • サーバーが起動したことをログに記録します。
  5. if __name__ == "__main__": ブロックで、ログファイルの設定を行い、start_server 関数を非同期で実行します。

動作確認

ここまでで、C++ と Python 両方の実装が完了しました。以下の手順で動作確認を行います。

  1. websocket_server.py を実行して、WebSocket サーバーを起動します。

  2. Unreal Editor でプロジェクトを開き、プレイボタンを押してゲームを開始します。

  3. ゲーム画面で E キーを押すと、WebSocket でメッセージが送信され、Python 側で受信したメッセージと現在の接続数が表示されます。また、Unreal 側では、送信したメッセージと受信したメッセージがデバッグメッセージとして表示されます。

以上で、Unreal Engine 5.4.2 で C++ と Python を WebSocket で通信させる方法の解説は終了です。

まとめ

この記事では、Unreal Engine 5.4.2 で C++ と Python を WebSocket で通信させる方法を、初心者にもわかりやすく解説しました。サンプルコードを多用し、コメントアウトで丁寧に説明することで、理解しやすくなっていると思います。

WebSocket は、リアルタイムの双方向通信を実現するための強力なツールです。ゲーム開発だけでなく、様々な分野で活用されています。この記事を通じて、WebSocket の基本的な使い方を理解し、自分の開発に役立てていただければ幸いです。

リポジトリ

GitHub - Sunwood-ai-labs/UE5.4.2_WebSocketTestV2
Contribute to Sunwood-ai-labs/UE5.4.2_WebSocketTestV2 development by creating an account on GitHub.

参考サイト

How to Use WebSockets in Unreal Engine
Download Core to create games for FREE: the Game Creator Challenge: this video, we go over how t...
How to use Enhanced Input in Unreal Engine C++
Enhanced Input is the future of unreal engine. The old input system gave little control and is now deprecated. However, enhanced input is complicated in C++....

コメント

タイトルとURLをコピーしました