
こんにちは!金融ソリューション事業部の山下です。
本記事では、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で執筆されました)



