Implement Subsystem - Login Queue - (Unreal Engine module)
Unwrap the subsystem
Byte Wars uses the Game Instance Subsystem named ULoginQueueSubsystem_Starter
to act as the wrapper for the AccelByte Gaming Services (AGS) OSS. It will only use the FOnlineIdentityAccelByte
provided by the AGS OSS, derived from the Identity Interface.
The AGS OSS's login queue AsyncTask does not enable timeout. The reason is that the login queue wait time might exceed the AsyncTask's timeout duration. Thus, the operation will wait until the player tries to log in or if the player cancels the login queue.
The diagram below describes how the Login Queue feature works with the OSS.
What's in the Starter Pack
The starter class ULoginQueueSubsystem_Starter
for you to modify is provided for you in the Resources section and consists of:
- A Header file:
/Source/AccelByteWars/TutorialModules/Access/LoginQueue/LoginQueueSubsystem_Starter.h
- A CPP file:
/Source/AccelByteWars/TutorialModules/Access/LoginQueue/LoginQueueSubsystem_Starter.cpp
- A Model file:
/Source/AccelByteWars/TutorialModules/Access/LoginQueue/LoginQueueModel.h
The starter class includes the following functionalities:
Include the AccelByte's Online Identity Interface and a pointer to it in the Header file:
// ...
#include "OnlineIdentityInterfaceAccelByte.h"protected:
FOnlineIdentityAccelBytePtr IdentityInterface;A code to get that Online Identity pointer in the CPP file:
void ULoginQueueSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// Get Online Subsystem and make sure it's valid.
FOnlineSubsystemAccelByte* Subsystem = static_cast<FOnlineSubsystemAccelByte*>(Online::GetSubsystem(GetWorld()));
if (!ensure(Subsystem))
{
UE_LOG_LOGIN_QUEUE(Warning, TEXT("The online subsystem is invalid. Please make sure OnlineSubsystemAccelByte is enabled and the DefaultPlatformService under [OnlineSubsystem] in the Engine.ini file is set to AccelByte."));
return;
}
// Grab the reference of AccelByte Identity Interface and make sure it's valid.
IdentityInterface = StaticCastSharedPtr<FOnlineIdentityAccelByte>(Subsystem->GetIdentityInterface());
if (!ensure(IdentityInterface.IsValid()))
{
UE_LOG_LOGIN_QUEUE(Warning, TEXT("Identity interface is not valid."));
return;
}
// ...
}Multicast delegates declaration to connect the game's logic to the Identity Interface's delegates in the Model file:
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnLoginQueueCancelCompleted, const APlayerController*, const FOnlineError&)
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnLoginQueued, const APlayerController*, const FAccelByteModelsLoginQueueTicketInfo& TicketInfo)
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnLoginTicketStatusUpdated, const APlayerController*, const FAccelByteModelsLoginQueueTicketInfo&, const FOnlineError&)Helper functions to get local player number from Player Controller as vice versa. These functions were made to simplify the interaction between UI and Identity Interface. The game UI has a
GetOwningPlayer
function which will return the owning Player Controller while the Identity Interface accepts local player number.int32 ULoginQueueSubsystem_Starter::GetLocalUserNumFromPlayerController(const APlayerController* PlayerController) const
{
if (!PlayerController)
{
return INDEX_NONE;
}
const ULocalPlayer* LocalPlayer = PlayerController->GetLocalPlayer();
if (!LocalPlayer)
{
return INDEX_NONE;
}
return LocalPlayer->GetControllerId();
}APlayerController* ULoginQueueSubsystem_Starter::GetPlayerControllerByLocalUserNum(const int32 LocalUserNum) const
{
APlayerController* MatchedPC = nullptr;
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
if (!It->IsValid())
{
continue;
}
if (APlayerController* PC = It->Get(); PC)
{
if (LocalUserNum == GetLocalUserNumFromPlayerController(PC))
{
MatchedPC = PC;
break;
}
}
}
return MatchedPC;
}
Implement Login Queue callbacks
Open the
ULoginQueueSubsystem_Starter
Header file and add these two delegates. These delegates will be the connection point between the OSS's delegates and Byte Wars' UIs.public:
// ...
FOnLoginQueued OnLoginQueuedDelegates;
FOnLoginTicketStatusUpdated OnLoginTicketStatusUpdatedDelegates;Still in the Header file, add the following functions to be bound to the OSS's delegates.
protected:
void OnLoginQueued(const int32 PlayerNum, const FAccelByteModelsLoginQueueTicketInfo& TicketInfo) const;
void OnLoginTicketStatusUpdated(
const int32 PlayerNum,
bool bWasSuccessful,
const FAccelByteModelsLoginQueueTicketInfo& TicketInfo,
const FOnlineErrorAccelByte& Error) const;Next, open the
ULoginQueueSubsystem_Starter
CPP file and implement theOnLoginQueued
function. As the name suggest, this will be the callback when a player is queued after login attempt. Here, change thePlayerNum
received from the OSS's delegate toPlayerController
to simplify the code on the UI side and trigger theOnLoginQueuedDelegates
.void ULoginQueueSubsystem_Starter::OnLoginQueued(const int32 PlayerNum, const FAccelByteModelsLoginQueueTicketInfo& TicketInfo) const
{
OnLoginQueuedDelegates.Broadcast(GetPlayerControllerByLocalUserNum(PlayerNum), TicketInfo);
}Still in the CPP file, implement the
OnLoginTicketStatusUpdated
function. This function will be the callback when the polling response is received. Just like the previous function, this function will simply change thePlayerNum
toPlayerController
and trigger theOnLoginTicketStatusUpdatedDelegates
.void ULoginQueueSubsystem_Starter::OnLoginTicketStatusUpdated(
const int32 PlayerNum,
bool bWasSuccessful,
const FAccelByteModelsLoginQueueTicketInfo& TicketInfo,
const FOnlineErrorAccelByte& Error) const
{
// bWasSuccessful false means the player is still in the queue -- not that the request has failed
OnLoginTicketStatusUpdatedDelegates.Broadcast(GetPlayerControllerByLocalUserNum(PlayerNum), TicketInfo, Error);
}infoThe polling request itself is done by the OSS with the delay based on the Polling Time configuration in the Admin Portal (Login Queue Configuration). If somehow this is set to 0, OSS will use the EstimatedWaitingTimeInSeconds that comes from the backend + random number between -10 to 10 seconds, which is capped to minimum 3 seconds and maximum 40 seconds.
Still in the CPP file, bind the functions you just implemented to the OSS's delegate. Navigate to the
Initialize
function and add the code below.void ULoginQueueSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
IdentityInterface->AccelByteOnLoginQueuedDelegates->AddUObject(this, &ThisClass::OnLoginQueued);
IdentityInterface->AccelByteOnLoginTicketStatusUpdatedDelegates->AddUObject(this, &ThisClass::OnLoginTicketStatusUpdated);
}Unbind the delegates in the
Deinitialize
function.void ULoginQueueSubsystem_Starter::Deinitialize()
{
// ...
IdentityInterface->AccelByteOnLoginQueuedDelegates->RemoveAll(this);
IdentityInterface->AccelByteOnLoginTicketStatusUpdatedDelegates->RemoveAll(this);
}
Implement Cancel Login
Open the
ULoginQueueSubsystem_Starter
Header file, add a function to trigger the cancel function and a delegate as a callback for the response.public:
void CancelLoginQueue(const APlayerController* PlayerController) const;
FOnLoginQueueCancelCompleted OnLoginQueueCancelCompletedDelegates;Still in the Header file, declare the following function as the callback that will be bound to the OSS delegate.
protected:
// ...
void OnLoginQueueCancelCompleted(const int32 PlayerNum, bool bWasSuccessful, const FOnlineErrorAccelByte& Error) const;Open the
ULoginQueueSubsystem_Starter
CPP file and implement theCancelLoginQueue
function. Convert thePlayerController
parameter to the associate local player index and call the cancel login functionality.void ULoginQueueSubsystem_Starter::CancelLoginQueue(const APlayerController* PlayerController) const
{
IdentityInterface->CancelLoginQueue(GetLocalUserNumFromPlayerController(PlayerController));
}Still in the CPP file, implement the callback function,
OnLoginQueueCancelCompleted
. This function will trigger theOnLoginQueueCancelCompletedDelegates
that you created during the Implement Cancel Login step. Note that, in addition to this,OnLoginComplete
will also be triggered right after this callback is triggered.void ULoginQueueSubsystem_Starter::OnLoginQueueCancelCompleted(
const int32 PlayerNum,
bool bWasSuccessful,
const FOnlineErrorAccelByte& Error) const
{
// OnLoginComplete will also be triggered after this is triggered
FOnlineError OutError = Error;
OutError.bSucceeded = bWasSuccessful;
OnLoginQueueCancelCompletedDelegates.Broadcast(GetPlayerControllerByLocalUserNum(PlayerNum), OutError);
}Now that you've implemented the function, you need to bind the callback to the OSS's delegate. Still in the CPP file, navigate to the
Initialize
function and add the code below.void ULoginQueueSubsystem_Starter::Initialize(FSubsystemCollectionBase& Collection)
{
// ...
IdentityInterface->AccelByteOnLoginQueueCancelCompleteDelegates->AddUObject(this, &ThisClass::OnLoginQueueCancelCompleted);
// ...
}Lastly, unbind the previous delegate in the
Deinitialize
function.void ULoginQueueSubsystem_Starter::Deinitialize()
{
// ...
IdentityInterface->AccelByteOnLoginQueueCancelCompleteDelegates->RemoveAll(this);
// ...
}Build the AccelByteWars project and make sure there is no compile errors.
Resources
- The files used in this tutorial section are available in the Byte Wars Unreal GitHub repository.