こんにちは!金融ソリューション事業部の山下です。
本記事では、Unreal EngineのPluginであるOnlineSubsystemを利用して、インターネット経由で同時接続するオンラインマルチプレイ機能をC++で実装する手順を紹介します。
前提知識
ネットワークモデル
オンラインゲームにおけるネットワークモデルには複数の選択肢があります。
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採用ページ
参考
執筆:@yamashita.yuki、レビュー:@wakamoto.ryosuke
(Shodoで執筆されました)