Skip to main content

Implement Subsystem - Login Queue - (Unreal Engine module)

Last updated on October 24, 2024

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

  1. 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;
  2. 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;
  3. Next, open the ULoginQueueSubsystem_Starter CPP file and implement the OnLoginQueued function. As the name suggest, this will be the callback when a player is queued after login attempt. Here, change the PlayerNum received from the OSS's delegate to PlayerController to simplify the code on the UI side and trigger the OnLoginQueuedDelegates.

    void ULoginQueueSubsystem_Starter::OnLoginQueued(const int32 PlayerNum, const FAccelByteModelsLoginQueueTicketInfo& TicketInfo) const
    {
    OnLoginQueuedDelegates.Broadcast(GetPlayerControllerByLocalUserNum(PlayerNum), TicketInfo);
    }
  4. 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 the PlayerNum to PlayerController and trigger the OnLoginTicketStatusUpdatedDelegates.

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

    The 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.

  5. 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);
    }
  6. Unbind the delegates in the Deinitialize function.

    void ULoginQueueSubsystem_Starter::Deinitialize()
    {
    // ...
    IdentityInterface->AccelByteOnLoginQueuedDelegates->RemoveAll(this);
    IdentityInterface->AccelByteOnLoginTicketStatusUpdatedDelegates->RemoveAll(this);
    }

Implement Cancel Login

  1. 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;
  2. 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;
  3. Open the ULoginQueueSubsystem_Starter CPP file and implement the CancelLoginQueue function. Convert the PlayerController 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));
    }
  4. Still in the CPP file, implement the callback function, OnLoginQueueCancelCompleted. This function will trigger the OnLoginQueueCancelCompletedDelegates 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);
    }
  5. 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);
    // ...
    }
  6. Lastly, unbind the previous delegate in the Deinitialize function.

    void ULoginQueueSubsystem_Starter::Deinitialize()
    {
    // ...
    IdentityInterface->AccelByteOnLoginQueueCancelCompleteDelegates->RemoveAll(this);
    // ...
    }
  7. Build the AccelByteWars project and make sure there is no compile errors.

Resources