こんにちは!金融ソリューション事業部の山下です。 本記事では、 Unreal Engine のPluginである OnlineSubsystem を利用して、インターネット経由で同時接続するオンライン マルチプレイ 機能を C++ で実装する手順を紹介します。 前提知識 ネットワークモデル ゲームサーバー/ゲームクライアント UEにおけるゲームサーバー方式 ゲームセッション オンラインサービス OnlineSubsystem 実施手順 実施環境/ツール 1. UEプロジェクト作成と各種設定 2. Session Interfaceの作成 3. CreateSession()の実装 4. JoinGameSession()の実装 5. 端末2台を用いた接続確認 所感 参考 前提知識 ネットワークモデル オンラインゲームにおけるネットワークモデルには複数の選択肢があります。 Unreal Engine では、基本的に「Client-Server」モデルが採用されております。 Peer-To-Peerモデル:プレイヤー同士でゲーム情報を相互通信する方式。プレイヤー数の増加に伴い通信が増大してしまう。また、ゲーム内に「唯一の正しいステート」が存在しない為、緻密な判定やプレイ精度が求められるゲームには不向きです。 Client-Serverモデル:「唯一の正しいステート」を持つゲームサーバーに対して、ゲームクライアントが接続する方式。ゲームクライアントから送られた情報は、ゲームサーバー経由で各ゲームクライアントにBroadCastされます。 ゲームサーバー/ゲームクライアント よく混同されますが、 Webサービス におけるWebサーバーとWebクライアントとは異なります。 ゲームサーバー:Client-Serverモデルにおける、「唯一の正しいステート」を持つサーバーです。 ゲームクライアント:Client-Serverモデルにおける、ゲームを実行するクライアントです。 UEにおけるゲームサーバー方式 Unrealn Engine では、以下2種類のゲームサーバー方式が利用可能です。 ListenServer:ゲームサーバー上でグラフィックの レンダリング をします。特定のプレイヤーがゲームサーバーを兼ねることにより、運営リソースを節約できます。一方で、ユーザーの端末スペックに依存してしまうこと、また多人数のゲームには不向きである点が欠点です。 DedicatedServer:ゲームサーバー上でグラフィックの レンダリング を行わいません。運営側でサーバーを用意する必要があり、ユーザー数の増加に伴いインフラコストもかかりますが、多人数のゲームにも対応可能です。 ゲームセッション よく混同されますが、ゲームセッションと Webサービス のセッションは異なります。 ゲームセッションは、具体的にはゲームサーバー上で動作するゲーム インスタンス を指します。 複数のプレイヤーが同一のゲームセッションに接続することで、「同じゲーム空間の共有 = マルチプレイ 」が可能になります。 オンラインサービス 一般的なオンライン マルチプレイ ゲームでは、UserやSession、AchievementやFriendなどの機能が必要になります。 そこでサービスプラットフォーム(Steam、 Xbox live 、 Facebook など)では、このような機能がオンラインサービスとして提供されております。 サービスプラットフォームを利用せずに自前で構築することももちろん可能です。例えば AWS では GameLift などのサービスも提供しており、DedicatedServerの ホスティング に加えてオンラインサービスの提供もされています。 AWS GameLift の利用方法については、 孫さんの記事 をぜひご覧ください。 OnlineSubsystem Unreal Engine が提供するPluginです。 各オンラインサービスプラットフォームにアクセスする為の共通モジュールおよびインターフェースが提供されています。 Steam、 Xbox live 、 Facebook 、EOSなど マルチプラットフォーム のゲームが、基本的にはコンフィギューレーションを調整するだけで1コードベースで マルチプラットフォーム の実装が可能です。 インターフェースには、Session、Friends, Achievementsなどが提供されています。 本記事では、基本的なCreateSession()とFindSessions()、JoinSession()を用います。 詳細は以下をご覧ください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/Interfaces/IOnlineSession/ 実施手順 今回は、検証を簡易にするためListenServer方式で検証を行います。 また、セッション管理を行うオンラインサービスについては、開発用IDが無償提供されているSteamネットワークを使用します。 UEプロジェクト作成と各種設定 Session Interfaceの作成 CreateSession()の実装 JoinGameSession()の実装 端末2台を用いた接続確認 実施環境/ツール OS: Windows 11 pro GPU : NVIDIA GeForce RTX 3070Ti Laptop DCC: Adobe Substance 3D Sampler 3.4.1 Game Engine: Unreal Engine 5.1.0 1. UEプロジェクト作成と各種設定 Unreal Engine のNew Project > Third Personテンプレートを選択します。Project Defaultsで、 C++ を選択します。 今回、プロジェクト名は「OnlineMultiplaySteam」としました。 Edit >Pluginを開きます。 「Online Subsystem Steam」を選択します。 Restartが求められるので再起動します。 次に、 Visual Studio エディタに移ります。 SolusionExplorerで以下のファイルを開きます。 Games > OnlineMultiplaySteam > Source > OnlineMultiplaySteam > OnlineMultiplaySteam.Build.cs PublicDependencyModuleに"OnlineSubsystemSteam", "OnlineSubsystem"を追加します。 11行目を以下に書き換え、Buildします。 PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "OnlineSubsystemSteam", "OnlineSubsystem" }); 次に、DefaultEngine.iniを修正します。 SolusionExplorerで以下のファイルを開きます。 Games > OnlineMultiplaySteam > Config > DefaultEngine.ini こちらのUEドキュメント を参考に、以下を追記します。 [/Script/Engine.GameEngine] +NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") [OnlineSubsystem] DefaultPlatformService=Steam [OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480 [/Script/OnlineSubsystemSteam.SteamNetDriver] NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection" 今回使用するSteamDevAppIdは480となっております。これはSteamから開発用に提供されているサンプルゲーム(SpaceWar)のIDです。本番開発で用いる場合は、自身でAppIdを取得する必要がありますのでご注意ください。 最後にプロジェクトファイルを生成します。 Editorを閉じて、File Explorer で「Saved」「Intermidiate」「Binaries」ファイルを削除します。その後、「Generate Visual Studio project files」でプロジェクトファイルを生成します。 これでプロジェクト設定は完了です。 2. Session Interfaceの作成 ThirdPersonTemplateのCharacterクラスを修正します。 Unreal Engine 独自の プレフィックス (クラス名にF,A、型名にF,Uなど)については、 公式のコーディング規約 をご参照ください。 OnlineMultiplaySteamCharacter.hを編集します。 includeに以下を追加します。 #include "Interfaces/OnlineSessionInterface.h" 記載する行について、"....generated.h"が一番最下部になる点にはご注意ください。 class内に、以下を追加します。 public: IOnlineSessionPtr OnlineSessionInterface; SessionInterfaceの変数が宣言できました。 IOnlineSession インターフェイス 仕様は、以下を参照してください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/Interfaces/IOnlineSession/ OnlineMultiplaySteamCharacter.cppを編集します。 includeに以下を追加します。 #include "OnlineSubsystem.h" コンスト ラク タの最下部に、以下を追記します。 IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get(); if (OnlineSubsystem) { OnlineSessionInterface = OnlineSubsystem->GetSessionInterface(); if (GEngine) { GEngine->AddOnScreenDebugMessage( 15.f, Color::Blue, String::Printf(TEXT("Found subsystem %s"), *OnlineSubsystem->GetSubsystemName().ToString()) ); } } OnlineSubsystemを使って、事前に指定したSteamと接続するためのInterfaceを取得しています。 本処理はCharacterクラスに追記しているため、キャ ラク ターがレベルにSpawnするタイミングでSessionInterfaceが作成されることになります。 IOnlineSubsystemの インターフェイス 仕様は、以下をご確認ください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/IOnlineSubsystem/ ビルドしてゲームを立ち上げます。 以下のように左側に青字のメッセージが表示されることを確認します。 SessionInterfaceが無事作成され、Steamネットワークに接続ができました。 3. CreateSession()の実装 作成したSessionInterfaceを使って、ゲームセッションの作成を行います。 事前に、セッション作成後の移動先レベルを作っておきます。レベルはDafaultのままで、名称はLobbyとします。 OnlineMultiplaySteamCharacter.hを編集します。 BluePrintでイベント(今回はKey1押下イベント)を受け取ってセッションを実行する為に、 protectedセクションを作成して以下BlueprintCallable関数を追加します。 protected: UFUNCTION(BlueprintCallable) void CreateGameSession(); OnlineSessionInterfaceでCreateGameSesion()を実行後、コールバック関数であるOnCreateSessionComplete()を実行します。 同じくprotectedセクションに、セッション作成後に呼びだすコールバック関数も追加します。 void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); privateセクションを追加して、コールバック関数をBindするためのDelegete変数も追加します。 private: FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; 続いて、.cppファイルにDelegete変数をコールバック関数とbindした上で、OnlineSessionInterfaceの Delegate ハンドラーに登録していきます。 OnlineMultiplaySteamCharacter.cppに以下を追加します。 コンスト ラク タの冒頭に、以下のように Delegate 変数にコールバック関数をBindします。 AOnlineMultiplaySteamCharacter::AOnlineMultiplaySteamCharacter(): CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)) CreateGameSession ()を追加します。 少し長いコードになってきたので、折りたたみ表示にします。詳細はコメントをご確認ください。 CreateGameSession() void AOnlineMultiplaySteamCharacter::CreateGameSession() { // called when pressed 1 key if (!OnlineSessionInterface.IsValid()) { return; } // check existing session auto ExistingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession); if (ExistingSession != nullptr) { OnlineSessionInterface->DestroySession(NAME_GameSession); } // Add Delegete variable to OnlineSessionInterface OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate); // Create Session Settings TSharedPtr <FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings()); SessionSettings->bIsLANMatch = false; SessionSettings->NumPublicConnections = 4; SessionSettings->bAllowJoinInProgress = true; SessionSettings->bAllowJoinViaPresence = true; SessionSettings->bShouldAdvertise = true; SessionSettings->bUsesPresence = true; SessionSettings->bUseLobbiesIfAvailable = true; SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); //Create Session const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings); } SessionSettingsを用意してSessionInterface->CreateSession関数を呼んでいます。 また、コンスト ラク タでも触れた Delegate 変数をハンドラーに登録しています。 SessionSettingのパラメーターは こちら を参照してください。 セッション作成後に呼びだすコールバック関数を実装します。 OnCreateSessionComplete() void AOnlineMultiplaySteamCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) { if (bWasSuccessful) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Blue, FString::Printf(TEXT("Successsfully Created session: %s"), *SessionName.ToString()) ); } UWorld* World = GetWorld(); if (World) { World->ServerTravel(FString("/Game/ThirdPerson/Maps/Lobby?Listen")); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Failed to create session!")) ); } } セッション作成完了後にシンプルなログメッセージを表示しています。 またLobbyレベルへの移動は、World->ServerTravel()関数を用いました。 オプションパラメーターでListenサーバーを指定しています。 イベントを受け取るために、EditorでThirdPersonCharacterのBPを開いて以下ノードを追加します。 これで、Key1を押下すると、BluePrint Callableで実装したCreate Game Session関数が実行されます。 コンパイル 後、ゲームを立ち上げます。 Key1を押下します。 成功ログが画面に表示されれば、セッション作成は完了です。 これで、ListenServer方式のゲームサーバー側の処理は完了しました。 注意:オンラインシステムへの接続を試す際、ゲームをEditor Viewportで実行しても正しく接続されません。Standsloneモード or パッケージ化した上で実行してください。 4. JoinGameSession()の実装 BluePrintでイベント(こちらはKey2押下イベント)を受け取ってセッションに参加する処理を実装します。 OnlineMultiplaySteamCharacter.hを編集します。 protectedセクションを作成して以下BlueprintCallable関数を追加します。 protected: UFUNCTION(BlueprintCallable) void JoinGameSession(); このJoinGameSesion()の中では、「セッションの検索」と「セッションへの参加」の2つの処理を行います。 その為、今回はそれぞれのコールバック関数を2つ用意します。 void OnFindSessionsComplete(bool bWasSuccessful); void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); privateセクションに、Delegete変数も2つ追加します。 また、検索条件を格納する為のSharedPointerも追加します。 FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; // Search Setting TSharedPtr<FOnlineSessionSearch> SessionSearch; 続いてOnlineMultiplaySteamCharacter.cppを編集します。 includeに以下を追加します。 #include "OnlineSessionSettings.h" 3.と同様にコンスト ラク タにて、 Delegate 変数にコールバック関数をbindします。 FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)), JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)) 以下関数を実装します。 void JoinGameSession(); void OnFindSessionsComplete(); void OnJoinSessionComplete(); こちらもそれぞれ折りたたみ表示にします。詳細はコメントをご確認ください。 まずJoinGameSession()では、検索条件であるSessionSearchを設定してFindSessionsを実行します。 SessionSearchのパラメーターは こちら をご覧ください。 JoinGameSession() void AOnlineMultiplaySteamCharacter::JoinGameSession() { // called when pressing 2 key if (!OnlineSessionInterface.IsValid()) { return; } if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Blue, FString::Printf(TEXT("pressed 2 and Executed function: JoinGameSession()")) ); } OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate); SessionSearch = MakeShareable(new FOnlineSessionSearch()); SessionSearch->MaxSearchResults = 10000; SessionSearch->bIsLanQuery = false; SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef()); } OnFindSessionsComplete()は、FindSessions()実行後のコールバック関数です。 検索結果のSession情報を用いて、セッションに参加します。 今回利用しているSteamAppIDは世界中の人が使っている為、SessionSettingで定義したMatchTypeにフィルタリングをしています。取得したセッション情報を引数に、JoinSession()を実行します。 OnFindSessionsComplete() void AOnlineMultiplaySteamCharacter::OnFindSessionsComplete(bool bWasSuccessful) { if (!OnlineSessionInterface.IsValid()) { return; } if (bWasSuccessful) { GEngine->AddOnScreenDebugMessage(-1,15.f,FColor::Cyan, FString::Printf(TEXT("FindSession Complete! SearchResults.Num() = %d"), SessionSearch->SearchResults.Num())); for (auto Result : SessionSearch->SearchResults) { FString Id = Result.GetSessionIdStr(); FString User = Result.Session.OwningUserName; FString MatchType; Result.Session.SessionSettings.Get(FName("MatchType"), MatchType); if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Successsfully Find Session! Id: %s , OwningUser: %s"), *Id, *User) ); } if (MatchType == FString("FreeForAll")) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Joining Match Type: %s"), *MatchType) ); } OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate); const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Result); } } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Failed to join session!")) ); } } } OnJoinSessionComplete()では、セッションへの参加後、セッション情報から IPアドレス を取得してレベルに移動します。 レベルの移動には、PlayerController->ClientTravel()を用います。 OnJoinSessionComplete() void AOnlineMultiplaySteamCharacter::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (!OnlineSessionInterface.IsValid()) { return; } FString Address; if (OnlineSessionInterface->GetResolvedConnectString(NAME_GameSession, Address)) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Yellow, FString::Printf(TEXT("Connect string: %s"), *Address) ); } APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); if (PlayerController) { PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute); } } } 最後に、EditorでThirdPersonCharacterのBPを開いて以下ノードを追加します。 5. 端末2台を用いた接続確認 マルチプレイ の検証のために、 Windows マシンを2つ用意します。 また、Steam IDも2つ用意します(Steam IDが同一だとセッションに入れない為)。 各Steamアカウントの設定で、Download Regionを同一にする必要がある点にもご注意ください。 1台目で、Key1を押下します。 2台目で、Key2を押下します。 操作をしてみて、動きが正しく同期されていることを確認します。 無事、オンライン経由で マルチプレイ を実現しました。 所感 今回は、OnlineSubSystemを用いて、インターネット経由のオンライン マルチプレイ を実装しました。 基本的には Unreal Engine で提供されているOnlineSubsystem Pluginを用いることで実現可能なため、単純な ユースケース であれば比較的実装はしやすい印象でした。 以前紹介したPixelStreaming と組み合わせることで、今回用意したような高スペックなマシン不要で、オンライン マルチプレイ を実現できます。 これらについては今後も検証していきたいと思います。 以前の記事 でも紹介したように、現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ご連絡ください。 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ 参考 https://docs.unrealengine.com/5.1/en-US/online-subsystem-steam-interface-in-unreal-engine/ 執筆: @yamashita.yuki 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )