Skip to main content

Server shutdown handler - Quick match with dedicated server - (Unreal Engine module)

Last updated on October 24, 2024
note

This tutorial is optional, and your project will still work if you skip directly to the next tutorial: Play test. However, this tutorial covers a some best practices for multiplayer game development, and it is recommended that you follow it.

When using sessions, especially with a dedicated server (DS), there may be times when sessions and/or the servers are claimed, but no players are connected to them, leaving them in an abandoned and wasting resources. While AccelByte Gaming Services (AGS) Session has an Inactive Timeout feature you may want to configure stricter behavior to ensure you're using your resources as efficiently as possible. This tutorial will show you how to implement strict server shutdowns and when to call them.

Shut down the server

When shutting down a server, you also want players to prevent players from joining the session that is tied to that server. There are two ways to do this: send a request to close the session, or set the session joinability to CLOSED. Byte Wars uses the latter, changing the joinability to CLOSED.

Take a look at the AccelByteWarsGameMode.h class located in \Source\AccelByteWars\Core\GameModes. That is the base game mode class for all Byte Wars levels. There are two declaration that we want to point out in this class: the OnPreGameShutdown delegate and the CloseGame function.

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

Now, look at the CloseGame implementation in the AccelByteWarsGameMode.cpp file.

void AAccelByteWarsGameMode::CloseGame(const FString& Reason) const
{
GAMEMODE_LOG(Warning, TEXT("Unregistering or shutting down server with reason: %s."), *Reason);

if (!IsRunningDedicatedServer())
{
GAMEMODE_LOG(Warning, TEXT("Not a Dedicated Server, shutdown canceled"));
return;
}

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

Notice that this function will try to execute the OnPreGameShutdown delegate if possible and pass CloseGameInternal, or call CloseGameInternal if the delegate is not bound. The idea of this function is for the function that is bound to the OnPreGameShutdown to do something first, and then call CloseGameInternal when it is done. This delegate is used to trigger the UpdateSessionJoinability, which you have implemented in Introduction to Session, to change the session's joinability to CLOSED. This will be discussed in more detail later. For now, shift your attention to the CloseGameInternal function.

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();
}

Here, the UnregisterServer function that you implemented in the Dedicated Server with AccelByte Multiplayer Services module is used. That UnregisterServer implementation also calls FPlatformMisc::RequestExit upon receiving a response, shutting down the server.

Now, go back to the OnPreGameShutdown delegate. Look at the AccelByteWarsServerSubsystemBase.cpp file located in \Source\AccelByteWars\TutorialModules\Play\ and navigate to the Initialize function implementation. You will see this code:

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

Here, the CloseGameSession function is called, setting the session's joinability to CLOSED, preventing players from joining the session. Once the response is received, the delegate passed in the OnPreGameShutdown delegate is called, which is the CloseGameInternal function.

When to shut the server down

Byte Wars is set up to handle scenarios that could make the session or server end up in an abandoned state. You can see the details of each scenario and how Byte Wars handles them below.

Scenario: the dedicated server has no players

Look at the AccelByteWarsGameMode.cpp class file located in \Source\AccelByteWars\Core\GameModes\. You will see this code:

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"));
}
}

When the last player logs out, ABGameState->PlayerArray.Num() <= 1, the CloseGame function is called right away, which shuts down the server.

Scenario: the game end countdown has reached 0.

Look at the AccelByteWarsInGameGameMode.cpp class file located in \Source\AccelByteWars\Core\GameModes\. In the Tick function, you will see this code:

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;
// ...
}

When the game ends, indicated by the leaderboard screen on the client side, the server starts a countdown that will force itself to shut down when it reaches 0, regardless if there are players connected.

Scenario: server claimed countdown has reached 0

Once the server is claimed by a session, indicated by the OnServerSessionReceived function being called, a countdown begins. When it reaches 0, the server will shut down. This is to prevent an abandoned server in the event of a player starting a DS session only to be disconnected before entering the server. This logic is located in the Source\AccelByteWars\TutorialModules\Play\GameSessionEssentials\AccelByteWarsServerSubsystemBase.cpp file in the OnServerSessionReceived function.

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);
// ...
}

Scenario: countdown for level travel or minimum team size has reached 0

Byte Wars doesn't use Unreal's seamless travel feature, meaning when the server travels to another level, all the connected clients disconnect from the server for a brief moment and attempts to reconnect when the target level has been loaded onto the server. When this happens, there's a chance that not all clients that were connected on the previous level were able to reconnect to the server. In Byte Wars, you want the server to shut itself down when this happens.

That same logic is also used to shut down the server if the current connected team doesn't reach a set minimum team count. By default, that value is set to 2, meaning a countdown to shut the server down will begin if there aren't at least two teams connected.

These implementations can be seen in the AccelByteWarsInGameGameMode.cpp class file located in \Source\AccelByteWars\Core\GameModes\ in the Tick function:

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;
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("Time is over");
}
break;
// ...
}

Byte Wars has two helper functions for the countdown to shut down the server in the code snippet: the ShouldStartNotEnoughPlayerCountdown to check if the conditions are met, and NotEnoughPlayerCountdownCounting for the actual count down function.

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;
}

return GetLivingTeamCount() < ABInGameGameState->GameSetup.MinimumTeamCountToPreventAutoShutdown;
}
void AAccelByteWarsInGameGameMode::NotEnoughPlayerCountdownCounting(const float& DeltaSeconds) const
{
// start NotEnoughPlayerCountdown to trigger server shutdown
ABInGameGameState->NotEnoughPlayerCountdown -= DeltaSeconds;
if (ABInGameGameState->NotEnoughPlayerCountdown <= 0)
{
ABInGameGameState->GameStatus = EGameStatus::INVALID;
CloseGame("Not enough player");
}
}