- Before Getting Started
- Connecting players to IMS Session Manager (Client-Side)
- Configuring your game server to work with IMS Session Manager (Server-Side)
- Conclusion
Please familiarize yourself with IMS Session Manager concepts and integration patterns by reading through the documentation. Ensure that you meet the specified pre-requisites.
Associated commit: Player authentication with PlayFab
To authorize IMS Session Manager calls, you must provide a form of authentication, as described in the Session Manager Authentication docs. In this example we use PlayFab to login users with Custom Id. This signs the user in using a custom unique identifier generated by the game title, and returns a session identifier that can subsequently be used for Session Manager API calls.
To integrate PlayFab authentication in your game you can use PlayFab's Unreal Plugin and refer to the Quickstart Guide.
void UShooterGameInstance::PlayerPlayFabLogin()
{
GetMutableDefault<UPlayFabRuntimeSettings>()->TitleId = PlayFabTitleId;
ClientAPI = IPlayFabModuleInterface::Get().GetClientAPI();
if (ClientAPI)
{
PlayFab::ClientModels::FLoginWithCustomIDRequest Request;
Request.CustomId = PlayFabCustomId;
Request.CreateAccount = true;
ClientAPI->LoginWithCustomID(Request,
PlayFab::UPlayFabClientAPI::FLoginWithCustomIDDelegate::CreateUObject(this, &UShooterGameInstance::PlayerPlayFabLoginOnSuccess),
PlayFab::FPlayFabErrorDelegate::CreateUObject(this, &UShooterGameInstance::PlayerPlayFabLoginOnError)
);
}
}
void UShooterGameInstance::PlayerPlayFabLoginOnSuccess(const PlayFab::ClientModels::FLoginResult& Result)
{
UE_LOG(LogOnlineIdentity, Log, TEXT("Successfully authenticated player with PlayFab."));
SessionTicket = Result.SessionTicket;
}
void UShooterGameInstance::PlayerPlayFabLoginOnError(const PlayFab::FPlayFabCppError& ErrorResult)
{
UE_LOG(LogOnlineIdentity, Error, TEXT("Failed to authenticate player with PlayFab."));
...
}
Associated commits: Session Manager API Client Generation, Temporary fix to OpenAPI bug involving body being set for GET requests
Similar to the IMS Zeuz integration, we recommend using the OpenAPI generator for UE4 to generate an IMSSessionManagerAPI
module with an interface to access Session Manager API calls.
We have provided an executable you can use to generate an API Module from the latest API specification here.
Note: Currently, making GET
requests using OpenAPI does not work because a content body is being set. In the meantime, there is a temporary fix for this.
Associated commit: Allow clients to create a session
Using the IMSSessionManagerAPI
module, it is now easy to make an API call to the CreateSession
endpoint. This creates a session (a running payload on IMS zeuz) which the players can join.
This endpoint requires us to set several parameters:
project_id
session_type
: allows us to specify in the request which allocation to create the session in
Additionally, we add an authorization header containing the Session Ticket
obtained after logging in the player with PlayFab.
Furthermore, you can optionally add a body to your request containing the session config to apply to the session. In our example, we allow players to specify the maximum number of players that can join the game as well as the number of bots to spawn. Later, in the game server, we will extract these values from the session config and apply them.
void AShooterGameSession::HostSession(const int32 MaxNumPlayers, const int32 BotsCount, const FString SessionTicket)
{
SessionManagerAPI->AddHeaderParam("Authorization", "Bearer playfab/" + SessionTicket);
IMSSessionManagerAPI::OpenAPISessionManagerV0Api::CreateSessionV0Request Request;
Request.SetShouldRetry(RetryPolicy);
Request.ProjectId = IMSProjectId;
Request.SessionType = IMSSessionType;
IMSSessionManagerAPI::OpenAPIV0CreateSessionRequestBody RequestBody;
RequestBody.SessionConfig = CreateSessionConfigJson(MaxNumPlayers, BotsCount);
Request.Body = RequestBody;
UE_LOG(LogOnlineGame, Display, TEXT("Attempting to create a session..."));
SessionManagerAPI->CreateSessionV0(Request, OnCreateSessionCompleteDelegate);
FHttpModule::Get().GetHttpManager().Flush(false);
}
If the request is successful, the response will contain the IP address and ports for the session, which you can use to form the session address players should connect to.
void AShooterGameSession::OnCreateSessionComplete(const IMSSessionManagerAPI::OpenAPISessionManagerV0Api::CreateSessionV0Response& Response)
{
if (Response.IsSuccessful() && Response.Content.Address.IsSet() && Response.Content.Ports.IsSet())
{
FString IP = Response.Content.Address.GetValue();
// Filtering the ports in the response for the "GamePort"
// This value should match what you have indicated in your allocation
const IMSSessionManagerAPI::OpenAPIV0Port* GamePortResponse = Response.Content.Ports.GetValue().FindByPredicate([](IMSSessionManagerAPI::OpenAPIV0Port PortResponse) { return PortResponse.Name == "GamePort"; });
if (GamePortResponse != nullptr)
{
FString SessionAddress = IP + ":" + FString::FromInt(GamePortResponse->Port);
UE_LOG(LogOnlineGame, Display, TEXT("Successfully created a session. Connect to session address: '%s'"), *SessionAddress);
// Call your function that joins the server at the provided address
}
else
{
UE_LOG(LogOnlineGame, Error, TEXT("Successfully created a session but could not find the Game Port."));
}
}
else
{
UE_LOG(LogOnlineGame, Display, TEXT("Failed to create a session."));
}
}
Note: For this to succeed, you need to make sure the specified session_type
value in your Session Manager API request matches the session_type
annotation configured in the allocation for the specified project_id
.
Associated commit: Allow clients to browse sessions
Using the IMSSessionManagerAPI
module, we can make an API call to the ListSessions
endpoint to retrieve all reserved sessions from an allocation.
void AShooterGameSession::FindSessions(FString SessionTicket)
{
SessionManagerAPI->AddHeaderParam("Authorization", "Bearer playfab/" + SessionTicket);
IMSSessionManagerAPI::OpenAPISessionManagerV0Api::ListSessionsV0Request Request;
Request.SetShouldRetry(RetryPolicy);
Request.ProjectId = IMSProjectId;
Request.SessionType = IMSSessionType;
UE_LOG(LogOnlineGame, Display, TEXT("Attempting to list sessions..."));
CurrentSessionSearch->SearchState = SearchState::InProgress;
SessionManagerAPI->ListSessionsV0(Request, OnFindSessionsCompleteDelegate);
FHttpModule::Get().GetHttpManager().Flush(false);
}
If the request is successful, the response will contain a list of sessions with an IP address, ports, and session_status
data to display to the player.
In our example, we take the list of sessions and display them to the user, along with details from the session status. In order to make retrieving session status information easy, we have extended the OpenAPIV0Session
with methods like GetPlayerCount
.
Currently, the game server is not setting any session status data, but later in this guide we will configure it to set the game phase, current number of players, and map name.
void AShooterGameSession::OnFindSessionsComplete(const IMSSessionManagerAPI::OpenAPISessionManagerV0Api::ListSessionsV0Response& Response)
{
if (Response.IsSuccessful())
{
UE_LOG(LogOnlineGame, Display, TEXT("Successfully listed sessions."));
TArray<Session> SearchResults;
for (IMSSessionManagerAPI::OpenAPIV0Session SessionResult : Response.Content.Sessions)
{
if (SearchResults.Num() < CurrentSessionSearch->MaxSearchResults)
{
SearchResults.Add(Session(SessionResult));
}
}
CurrentSessionSearch->SearchResults = SearchResults;
CurrentSessionSearch->SearchState = SearchState::Done;
}
else
{
UE_LOG(LogOnlineGame, Display, TEXT("Failed to list sessions."));
CurrentSessionSearch->SearchState = SearchState::Failed;
}
}
Associated commit: Allow clients to join sessions
Now that players can create and browse sessions, they need to be able to join them. Using the session address constructed from the game server IP address and specified port, we can easily travel to the session:
bool AShooterGameSession::TravelToSession(FString SessionAddress)
{
APlayerController* const PlayerController = GetWorld()->GetFirstPlayerController();
if (PlayerController)
{
PlayerController->ClientTravel(SessionAddress, TRAVEL_Absolute);
return true;
}
return false;
}
Associated commit: Set ProjectId/SessionType from CLI
During development you may want to specify directly from the command line the Project Id
and Session Type
to use. We use arguments to support this workflow.
While players can create, browse, and join sessions, there are a few tasks to address on the game server:
- The game server does not know whether the payload it is running on has been reserved.
- If no players join a reserved session after a given amount of time, the game server should shut itself down to avoid filling the allocation with unused reserved payloads.
- The session config passed in when the session is created is not being applied to the game server.
- No session status is being set by the server so players don't know the difference between available sessions.
Associated commit: Check for payload status updates
The game server does not know the state of the payload it is running in. However, this information is important for the game server to know so that it can trigger events such as retrieving the session config. It also allows the game server to know when the payload is in an unexpected state.
The Payload Local API has a GetPayloadDetails
endpoint which contains the current status of the payload. In order to react to these status changes we poll the endpoint every second.
void AShooterGameMode::DefaultTimer()
{
...
UpdatePayloadStatus();
...
}
void AShooterGameMode::UpdatePayloadStatus()
{
IMSZeuzAPI::OpenAPIPayloadLocalApi::GetPayloadV0Request Request;
Request.SetShouldRetry(RetryPolicy);
PayloadLocalAPI->GetPayloadV0(Request, OnUpdatePayloadStatusDelegate);
FHttpModule::Get().GetHttpManager().Flush(false);
}
If the request is successful, we update the internal payload state.
void AShooterGameMode::OnUpdatePayloadStatusComplete(const IMSZeuzAPI::OpenAPIPayloadLocalApi::GetPayloadV0Response& Response)
{
if (Response.IsSuccessful())
{
IMSZeuzAPI::OpenAPIPayloadStatusStateV0::Values PendingState = Response.Content.Result.Status.State.Value;
if (CurrentPayloadState != PendingState)
{
CurrentPayloadState = PendingState;
if (CurrentPayloadState == IMSZeuzAPI::OpenAPIPayloadStatusStateV0::Values::Reserved && WasCreatedBySessionManager())
{
// Retrieve session config
}
else if (CurrentPayloadState == IMSZeuzAPI::OpenAPIPayloadStatusStateV0::Values::Error || CurrentPayloadState == IMSZeuzAPI::OpenAPIPayloadStatusStateV0::Values::Unhealthy)
{
// Handle appropriately
}
}
}
else
{
UE_LOG(LogGameMode, Display, TEXT("Failed to retrieve payload details."));
}
}
Note: A session-manager
flag was added to know whether the game server was created by the Session Manager, as there are some operations (e.g. retrieving session config) which are only applicable to the Session Manager.
Associated commit: Shutdown server if no players join a reserved session
Because the server waits for players to connect before it starts counting down, if no players join the reserved payload, the payload will stay in that state indefinitely. This will fill up the allocation with unused payloads. If no players join a reserved session after a given amount of time, the game server should shut itself down.
When the payload state is updated in OnUpdatePayloadStatusComplete
, we track the timestamp of that change:
TimeOfLastPayloadStateChange = UGameplayStatics::GetRealTimeSeconds(GetWorld());
Then in the server waiting logic in the DefaultTimer
method, we check if the time since the payload was updated to the Reserved
state is greater than the configured timeout time:
if (GetMatchState() == MatchState::WaitingToStart && GetNumPlayers() == 0)
{
if (CurrentPayloadState == IMSZeuzAPI::OpenAPIPayloadStatusStateV0::Values::Reserved)
{
if (UGameplayStatics::GetRealTimeSeconds(GetWorld()) - TimeOfLastPayloadStateChange > TimeBeforeReservedPayloadTimeout)
{
// Shutdown server to avoid filling allocation buffer with reserved payloads that are not being used
FGenericPlatformMisc::RequestExit(false);
}
}
return;
}
Associated commit: Retrieve session config and apply to the game server
Upon detecting that the payload state is now Reserved
, we want to retrieve the session config (if any) and apply it to the game server. We can use the SessionManagerLocalAPI
that is part of the IMSZeuzAPI
module.
void AShooterGameMode::RetrieveSessionConfig()
{
IMSZeuzAPI::OpenAPISessionManagerLocalApi::GetSessionConfigV0Request Request;
Request.SetShouldRetry(RetryPolicy);
UE_LOG(LogGameMode, Display, TEXT("Attempting to retrieve session config..."));
SessionManagerLocalAPI->GetSessionConfigV0(Request, OnRetrieveSessionConfigDelegate);
FHttpModule::Get().GetHttpManager().Flush(false);
}
If the request is successful, we can then try to parse the Json session config response and extract the maximum number of players and number of bots.
Note: It is very important that you sanitize the values retrieved in the session config. Because the session config is set when a player creates a session, it is possible that a malicious player might make a request with unexpected data or values.
void AShooterGameMode::OnRetrieveSessionConfigComplete(const IMSZeuzAPI::OpenAPISessionManagerLocalApi::GetSessionConfigV0Response& Response)
{
if (Response.IsSuccessful())
{
UE_LOG(LogGameMode, Display, TEXT("Successfully retrieved session config."));
if (Response.Content.Config.IsSet())
{
ProcessSessionConfig(Response.Content.Config.GetValue());
}
else
{
UE_LOG(LogGameMode, Display, TEXT("No session config found to apply to game server."));
}
}
else
{
UE_LOG(LogGameMode, Display, TEXT("Failed to retrieve session config."));
}
}
void AShooterGameMode::ProcessSessionConfig(FString SessionConfig)
{
...
if (JsonObject->TryGetNumberField("MaxNumPlayers", MaxNumPlayersFromJson))
{
MaxNumPlayers = FMath::Clamp(MaxNumPlayersFromJson, MIN_NUMBER_PLAYERS, MAX_NUMBER_PLAYERS);
}
if (JsonObject->TryGetNumberField("BotsCount", BotsCountFromJson))
{
SetAllowBots(BotsCountFromJson > 0 ? true : false, BotsCountFromJson);
CreateBotControllers();
bNeedsBotCreation = false;
}
...
}
Associated commit: Set session status
Finally, we want to update the session status when a players joins or leaves the game, or when the game phase changes because this information is relevant to players when they browse sessions.
void AShooterGameMode::SetSessionStatus()
{
IMSZeuzAPI::OpenAPISessionManagerLocalApi::ApiV0SessionManagerStatusPostRequest Request;
Request.SetShouldRetry(RetryPolicy);
Request.RequestBody = CreateSessionStatusBody();
UE_LOG(LogGameMode, Display, TEXT("Attempting to set session status..."));
SessionManagerLocalAPI->ApiV0SessionManagerStatusPost(Request, OnSetSessionStatusDelegate);
FHttpModule::Get().GetHttpManager().Flush(false);
}
SetSessionStatus
is called after player login and logout, after retrieving session config, and when the game phase is set.
Congratulations, your game can now support the lifecycle of custom game sessions!
You can upload your game server as a new image to IMS Image Manager, and edit your allocation to specify the new image, -session-manager
argument and session_type
annotation.
To launch the client from the CLI and specify the Project Id and Session Type, run:
ShooterClient.exe -windowed -ProjectId your-project-id -SessionType your-session-type