Skip to content

Latest commit

 

History

History
421 lines (322 loc) · 20.3 KB

session-manager-integration.md

File metadata and controls

421 lines (322 loc) · 20.3 KB

Integrate IMS Session Manager functionality into your game

Table of Contents

Before Getting Started

Please familiarize yourself with IMS Session Manager concepts and integration patterns by reading through the documentation. Ensure that you meet the specified pre-requisites.

Connecting players with IMS Session Manager

1. Authenticate players with PlayFab

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

	...
}

2. Generate Session Manager API Client with OpenAPI

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.

3. Allow players to create sessions

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.

4. Allow players to browse sessions

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

5. Allow players to join sessions

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

6. Set Project Id and Session Type from the CLI

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.

7. Next steps

While players can create, browse, and join sessions, there are a few tasks to address on the game server:

  1. The game server does not know whether the payload it is running on has been reserved.
  2. 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.
  3. The session config passed in when the session is created is not being applied to the game server.
  4. No session status is being set by the server so players don't know the difference between available sessions.

Configuring your game server to work with IMS Session Manager

1. Check for payload status updates

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.

2. Shutdown server if no players join a reserved session

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

3. Retrieve session config and apply to the game server

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

4. Set session status

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.

Conclusion

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