メインコンテンツまでスキップ

サーバーシャットダウンハンドラー - 専用サーバーでのクイックマッチ - (Unreal Engine モジュール)

Last updated on February 4, 2026

注釈:本資料はAI技術を用いて翻訳されています。

注記

このチュートリアルはオプションです。次のチュートリアル プレイテスト に直接スキップしてもプロジェクトは動作します。ただし、このチュートリアルはマルチプレイヤーゲーム開発のベストプラクティスをカバーしているため、実施することをお勧めします。

セッション、特に専用サーバー (DS) を使用する場合、セッションやサーバーが要求されているにもかかわらず、プレイヤーが接続していない状態になり、放置されてリソースを無駄にすることがあります。AccelByte Gaming Services (AGS) セッションには 非アクティブタイムアウト 機能 がありますが、リソースをできるだけ効率的に使用するために、より厳格な動作を設定したい場合があります。このチュートリアルでは、厳格なサーバーシャットダウンの実装方法と、それを呼び出すタイミングについて説明します。

サーバーをシャットダウンする

サーバーをシャットダウンする際、そのサーバーに紐付けられたセッションにプレイヤーが参加できないようにする必要があります。これには2つの方法があります。セッションを閉じるリクエストを送信するか、セッションの参加可能性を CLOSED に設定します。Byte Wars では後者を使用し、参加可能性を CLOSED に変更します。

\Source\AccelByteWars\Core\GameModes にある AccelByteWarsGameMode.h クラスを見てください。これは、すべての Byte Wars レベルのベースゲームモードクラスです。このクラスには、注目すべき2つの宣言があります。OnPreGameShutdown デリゲートと CloseGame() 関数です。

public:
// ...
void CloseGame(const FString& Reason);
public:
// ...
static inline TMulticastDelegate<void(TDelegate<void()>)> OnPreGameShutdown;

次に、AccelByteWarsGameMode.cpp ファイルの CloseGame 実装を見てください。

void AAccelByteWarsGameMode::CloseGame(const FString& Reason)
{
// Abort if the instance is not a server or if it already closing.
if (!IsRunningDedicatedServer() || bIsServerClosing)
{
return;
}

GAMEMODE_LOG(Log, TEXT("Closing the game with reason: %s."), *Reason);

KickAllPlayers();

if (OnPreGameShutdown.IsBound())
{
OnPreGameShutdown.Broadcast(TDelegate<void()>::CreateUObject(this, &ThisClass::SetupServerCloseCountdownValue));
}
else
{
SetupServerCloseCountdownValue();
}
}

この関数は、可能であれば OnPreGameShutdown デリゲートを実行して CloseGameInternal を渡すか、デリゲートがバインドされていない場合は CloseGameInternal を呼び出します。この関数の考え方は、OnPreGameShutdown にバインドされた関数が最初に何かを実行し、完了したら CloseGameInternal を呼び出すというものです。このデリゲートは、セッション入門 で実装した UpdateSessionJoinability をトリガーして、セッションの参加可能性を CLOSED に変更するために使用されます。これについては後で詳しく説明します。今は、CloseGameInternal() 関数に注目してください。

void AAccelByteWarsGameMode::CloseGameInternal() const
{
AAccelByteWarsGameSession* Session = Cast<AAccelByteWarsGameSession>(GameSession);
if (!Session)
{
GAMEMODE_LOG(Warning, TEXT("The game session is null. Shutting down immediately."));
FPlatformMisc::RequestExit(false);
return;
}

// Unregister the server.
Session->UnregisterServer();
}

ここでは、AccelByte Multiplayer Services を使用した専用サーバー モジュールで実装した UnregisterServer() 関数が使用されています。その UnregisterServer 実装は、レスポンスを受信すると FPlatformMisc::RequestExit も呼び出し、サーバーをシャットダウンします。

次に、OnPreGameShutdown デリゲートに戻ります。\Source\AccelByteWars\TutorialModules\Play\ にある AccelByteWarsServerSubsystemBase.cpp ファイルを見て、Initialize() 関数の実装に移動してください。次のコードが表示されます。

void UAccelByteWarsServerSubsystemBase::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
AAccelByteWarsMainMenuGameMode::OnPreGameShutdown.AddWeakLambda(this, [this](TDelegate<void()> OnComplete)
{
CloseGameSession(FOnUpdateSessionCompleteDelegate::CreateWeakLambda(this, [OnComplete](FName SessionName, bool bSucceeded)
{
OnComplete.ExecuteIfBound();
}));
});
// ...
}

ここでは、CloseGameSession() 関数が呼び出され、セッションの参加可能性を CLOSED に設定し、プレイヤーがセッションに参加できないようにします。レスポンスを受信すると、OnPreGameShutdown デリゲートで渡されたデリゲートが呼び出されます。これが CloseGameInternal() 関数です。

サーバーをシャットダウンするタイミング

Byte Wars は、セッションやサーバーが放置状態になる可能性のあるシナリオを処理するように設定されています。各シナリオの詳細と Byte Wars がそれらをどのように処理するかについては、以下を参照してください。

シナリオ: 専用サーバーにプレイヤーがいない

\Source\AccelByteWars\Core\GameModes\ にある AccelByteWarsGameMode.cpp クラスファイルを見てください。次のコードが表示されます。

void AAccelByteWarsGameMode::Logout(AController* Exiting)
{
Super::Logout(Exiting);

if (IsServer())
{
if (bShouldRemovePlayerOnLogoutImmediately && !ABGameState->bIsServerTravelling)
{
const bool bSucceeded = RemovePlayer(Cast<APlayerController>(Exiting));
GAMEMODE_LOG(Warning, TEXT("Removing player from GameState data. Succeeded: %s"), *FString(bSucceeded ? "TRUE" : "FALSE"));
}
}

if (IsRunningDedicatedServer() &&
ABGameState->PlayerArray.Num() <= 1 &&
bImmediatelyShutdownWhenEmpty &&
!ABGameState->bIsServerTravelling)
{
CloseGame(TEXT("Last player logs out and bImmediatelyShutdownWhenEmpty was set to true"));
}
}

最後のプレイヤーがログアウトすると、ABGameState->PlayerArray.Num() <= 1 の場合、CloseGame() 関数がすぐに呼び出され、サーバーがシャットダウンされます。

シナリオ: ゲーム終了カウントダウンが0に達した

\Source\AccelByteWars\Core\GameModes\ にある AccelByteWarsInGameGameMode.cpp クラスファイルを見てください。Tick() 関数に、次のコードが表示されます。

void AAccelByteWarsInGameGameMode::Tick(float DeltaSeconds)
{
// ...
case EGameStatus::GAME_ENDS:
if (IsRunningDedicatedServer() && ABInGameGameState->GameSetup.GameEndsShutdownCountdown != INDEX_NONE)
{
ABInGameGameState->PostGameCountdown -= DeltaSeconds;
if (ABInGameGameState->PostGameCountdown <= 0)
{
ABInGameGameState->GameStatus = EGameStatus::INVALID;
CloseGame("Game finished");
}
}
break;
// ...
}

ゲームが終了すると、クライアント側のリーダーボード画面で示されるように、サーバーはカウントダウンを開始し、0に達すると、接続されているプレイヤーがいるかどうかに関係なく、強制的にシャットダウンします。

シナリオ: サーバー要求カウントダウンが0に達した

サーバーがセッションによって要求されると、OnServerSessionReceived() 関数が呼び出されることで示され、カウントダウンが開始されます。0に達すると、サーバーはシャットダウンします。これは、プレイヤーが DS セッションを開始したものの、サーバーに入る前に切断された場合に、放置されたサーバーを防ぐためです。このロジックは、Source\AccelByteWars\TutorialModules\Play\GameSessionEssentials\AccelByteWarsServerSubsystemBase.cpp ファイルの OnServerSessionReceived() 関数にあります。

void UAccelByteWarsServerSubsystemBase::OnServerSessionReceived(FName SessionName)
{
// ...
// On DS, allow the lobby to shutdown if there's no player present in a certain amount of time.
const AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();
if (!GameMode)
{
return;
}
const AAccelByteWarsMainMenuGameMode* MainMenuGameMode = Cast<AAccelByteWarsMainMenuGameMode>(GameMode);
if (!MainMenuGameMode)
{
return;
}

MainMenuGameMode->SetAllowAutoShutdown(true);

// Should shutdown be enabled on last logout or not.
const FString CmdArgs = FCommandLine::Get();
const bool bShutdownOnLastLogout = !CmdArgs.Contains(TEXT("-NoImmediateShutdown"));
MainMenuGameMode->SetImmediatelyShutdownWhenEmpty(bShutdownOnLastLogout);
// ...
}

シナリオ: レベル移動または最小チームサイズのカウントダウンが0に達した

Byte Wars は Unreal のシームレストラベル機能を使用していません。つまり、サーバーが別のレベルに移動すると、接続されているすべてのクライアントは一時的にサーバーから切断され、ターゲットレベルがサーバーにロードされたときに再接続を試みます。この場合、前のレベルで接続されていたすべてのクライアントがサーバーに再接続できない可能性があります。Byte Wars では、これが発生した場合にサーバーを自動的にシャットダウンします。

同じロジックは、現在接続されているチームが設定された最小チーム数に達していない場合にサーバーをシャットダウンするためにも使用されます。デフォルトでは、この値は2に設定されています。つまり、少なくとも2つのチームが接続されていない場合、サーバーをシャットダウンするカウントダウンが開始されます。

これらの実装は、\Source\AccelByteWars\Core\GameModes\ にある AccelByteWarsInGameGameMode.cpp クラスファイルの Tick() 関数で確認できます。

void AAccelByteWarsInGameGameMode::Tick(float DeltaSeconds)
{
// ...
case EGameStatus::AWAITING_PLAYERS:
// Check if all registered players have re-entered the server
if (ABInGameGameState->PlayerArray.Num() == ABInGameGameState->GetRegisteredPlayersNum())
{
ABInGameGameState->GameStatus = EGameStatus::PRE_GAME_COUNTDOWN_STARTED;
FillEmptySlotWithBot();
if (IsRunningDedicatedServer())
{
// reset NotEnoughPlayerCountdown
SetupShutdownCountdownsValue();
}
}
else
{
// Use NotEnoughPlayerCountdown as a countdown to wait for all registered players to reconnect to the DS.
if (IsRunningDedicatedServer())
{
NotEnoughPlayerCountdownCounting(DeltaSeconds);
}
}
break;
// ...
}
void AAccelByteWarsInGameGameMode::Tick(float DeltaSeconds)
{
// ...
case EGameStatus::AWAITING_PLAYERS_MID_GAME:
if (IsRunningDedicatedServer())
{
SimulateServerCrashCountdownCounting(DeltaSeconds);

if (ShouldStartNotEnoughPlayerCountdown())
{
NotEnoughPlayerCountdownCounting(DeltaSeconds);
}
else
{
ABInGameGameState->GameStatus = EGameStatus::GAME_STARTED;
SetupShutdownCountdownsValue();
}
}
break;
// ...
}
void AAccelByteWarsInGameGameMode::Tick(float DeltaSeconds)
{
// ...
case EGameStatus::GAME_STARTED:
if (IsRunningDedicatedServer())
{
SimulateServerCrashCountdownCounting(DeltaSeconds);

if (ShouldStartNotEnoughPlayerCountdown())
{
ABInGameGameState->GameStatus = EGameStatus::AWAITING_PLAYERS_MID_GAME;
}
}

// Gameplay timer
ABInGameGameState->TimeLeft -= DeltaSeconds;
if (ABInGameGameState->TimeLeft <= 0)
{
ABInGameGameState->TimeLeft = 0;
EndGame(GAME_END_REASON_TIMES_UP);
}
break;
// ...
}

Byte Wars には、コードスニペットでサーバーをシャットダウンするカウントダウンのための2つのヘルパー関数があります。条件が満たされているかどうかをチェックする ShouldStartNotEnoughPlayerCountdown と、実際のカウントダウン関数である NotEnoughPlayerCountdownCounting です。

bool AAccelByteWarsInGameGameMode::ShouldStartNotEnoughPlayerCountdown() const
{
// check if the config is enabled in game setup
if (ABInGameGameState->GameSetup.NotEnoughPlayerShutdownCountdown == INDEX_NONE || ABInGameGameState->GameSetup.MinimumTeamCountToPreventAutoShutdown == INDEX_NONE)
{
return false;
}

const int32 LivingTeamCount = GetLivingTeamCount();
const int32 HumanPlayerCount = GetConnectedHumanPlayerCount();
const int32 MinRequiredCount = ABInGameGameState->GameSetup.MinimumTeamCountToPreventAutoShutdown;

// Start countdown if either not enough teams OR not enough actual human players
return (LivingTeamCount < MinRequiredCount) || (HumanPlayerCount < MinRequiredCount);
}
void AAccelByteWarsInGameGameMode::NotEnoughPlayerCountdownCounting(const float& DeltaSeconds)
{
// start NotEnoughPlayerCountdown to trigger server shutdown
ABInGameGameState->NotEnoughPlayerCountdown -= DeltaSeconds;
if (ABInGameGameState->NotEnoughPlayerCountdown <= 0)
{
ABInGameGameState->GameStatus = EGameStatus::INVALID;
CloseGame("Not enough player");
}
}