Server shutdown handler - Quick match with dedicated server - (Unreal Engine module)
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");
}
}