Skip to main content

Unreal Engine Module - Create a Joinable Session Using a Dedicated Server - add Browse Match menu

Last updated on January 13, 2024
AGS Starter

This tutorial isn't applicable to the AccelByte Gaming Service (AGS) Starter tier. It requires the AccelByte Multiplayer Server (AMS) or Armada, which isn't currently supported on AGS Starter.

Just like the Create Match Session UI, to simultaneously support both server modes, Peer to Peer (P2P) and Dedicated Server (DS), you must separate the menu widget and the widget that actually connects to the browse session functionality, which will be shown inside the menu widget. Those widgets, in order, are the Browse Match widget and Browse Match DS widget.

The widget that you're going to prepare is the Browse Match DS.

About the Browse Match menu

Browse Match menu is a widget in Byte Wars used to display all the sessions that are created from the Browse Match menu and a way to join them.

The Browse Match menu consists of two parts:

  • The C++ class BrowseMatchWidget, where most of our implementation will be in.
    • Header file: \Source\AccelByteWars\TutorialModules\MatchSessionEssentials\UI\BrowseMatchWidget.h
    • CPP file: \Source\AccelByteWars\TutorialModules\MatchSessionEssentials\UI\BrowseMatchWidget.cpp
  • The widget blueprint class W_BrowseMatch that was created and designed using Unreal Motion Graphics (UMG).
    • Widget Blueprint file: \Content\TutorialModules\MatchSessionEssentials\UI\W_BrowseMatch.uasset

Browse Match menu has six states:

  • Browse Loading : showing a loading status for the browse session.
  • Browse Empty : showing a text stating that there's currently no session available.
  • Browse Not Empty : showing a list of all the available sessions and a join button for each of them.
  • Browse Error : showing an error text for when the browse session returns an error.
  • Join Loading : showing the current joining status when the player click any join button.
  • Join Error : showing an error text for when the join session returns an error.

The state changes are possible using a combination of Unreal Motion Graphic's Widget Switcher and our AccelByteWars Widget Switcher. The AccelByteWars Widget Switcher is a custom Widget Switcher with predefined states: empty, loading, error, and success. Here are the declarations of the mentioned components in the header file.

private:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UWidgetSwitcher- Ws_Root;

UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UAccelByteWarsWidgetSwitcher- Ws_Browse_Content;

UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UAccelByteWarsWidgetSwitcher- Ws_Joining;

To switch between states, the Browse Match widget has the following function.

void UBrowseMatchWidget::SwitchContent(const EContentType Type)
{
bool bBrowseMenu = false;
EAccelByteWarsWidgetSwitcherState State = EAccelByteWarsWidgetSwitcherState::Loading;

bool bJoin_ShowBackButton = false;

UWidget- FocusTarget = Btn_Back;

switch (Type)
{
case EContentType::BROWSE_LOADING:
bBrowseMenu = true;
State = EAccelByteWarsWidgetSwitcherState::Loading;
CameraTargetY = 600.0f;
FocusTarget = Btn_Back;
break;
case EContentType::BROWSE_EMPTY:
bBrowseMenu = true;
State = EAccelByteWarsWidgetSwitcherState::Empty;
CameraTargetY = 600.0f;
FocusTarget = Btn_Back;
break;
case EContentType::BROWSE_NOT_EMPTY:
bBrowseMenu = true;
State = EAccelByteWarsWidgetSwitcherState::Not_Empty;
CameraTargetY = 600.0f;
FocusTarget = Lv_Sessions;
break;
case EContentType::BROWSE_ERROR:
bBrowseMenu = true;
State = EAccelByteWarsWidgetSwitcherState::Error;
CameraTargetY = 600.0f;
FocusTarget = W_ActionButtonsOuter->HasAnyChildren() ? W_ActionButtonsOuter->GetChildAt(0) : Btn_Back;
break;
case EContentType::JOIN_LOADING:
bBrowseMenu = false;
State = EAccelByteWarsWidgetSwitcherState::Loading;
bJoin_ShowBackButton = false;
CameraTargetY = 750.0f;
FocusTarget = Ws_Joining;
break;
case EContentType::JOIN_ERROR:
bBrowseMenu = false;
State = EAccelByteWarsWidgetSwitcherState::Error;
bJoin_ShowBackButton = true;
CameraTargetY = 750.0f;
FocusTarget = Btn_Joining_Back;
break;
}

FocusTarget->SetUserFocus(GetOwningPlayer());
Ws_Root->SetActiveWidget(bBrowseMenu ? W_Browse_Outer : W_Joining_Outer);
if (bBrowseMenu)
{
Ws_Browse_Content->SetWidgetState(State);
}
else
{
Btn_Joining_Back->SetVisibility(bJoin_ShowBackButton ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
Ws_Joining->SetWidgetState(State);
}

// Set FTUE
if (Type == EContentType::BROWSE_EMPTY || Type == EContentType::BROWSE_NOT_EMPTY)
{
InitializeFTEUDialogues(true);
}
else
{
DeinitializeFTUEDialogues();
}
}

Take a look at the BrowseMatchWidget header file. You will see a variable called W_ActionButtonsOuter. This is a Panel Widget that will house the Browse Match DS widget that you will prepare later in this submodule.

private:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UPanelWidget- W_ActionButtonsOuter;

Browse Loading state

This state consists of a text that can be set to any message. To do so, call SetLoadingMessage with the const bool bBrowse parameter as true just before calling the SwitchContent(EContentType::BROWSE_LOADING).

void UBrowseMatchWidget::SetLoadingMessage(const FText& Text, const bool bBrowse, const bool bEnableCancelButton) const
{
if (bBrowse)
{
Ws_Browse_Content->LoadingMessage = Text;
}
else
{
Ws_Joining->LoadingMessage = Text;
Ws_Joining->bEnableCancelButton = bEnableCancelButton;
}
}

Preview of the Browse Loading state

Browse Empty state

This state of a static text stating that there's no session currently available.

Preview of the Browse Empty state

Browse Not Empty state

Here, the player will see a list of all available sessions and can join one of them. This consists solely of a List View, the pointers of which can be retrieved using GetListViewWidgetComponent. The UObject called UMatchSessionData can be found in the AccelByteWarsOnlineSessionModels.h to pass data to the entry widget of the List View.

public:
UListView- GetListViewWidgetComponent() const { return Lv_Sessions; }

private:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UListView- Lv_Sessions;

Preview of the Browse Not Empty state

Browse Error state

This sate is comprised of an error message which can be set to any message. To do so, call SetErrorMessage with the const bool bBrowse parameter as true just before calling SwitchContent(EContentType::BROWSE_ERROR).

void UBrowseMatchWidget::SetErrorMessage(const FText& Text, const bool bBrowse) const
{
if (bBrowse)
{
Ws_Browse_Content->ErrorMessage = Text;
}
else
{
Ws_Joining->ErrorMessage = Text;
}
}

Preview of the Browse Error state

Join Loading state

This state consists of a cancel button and a text that can be set to any message. To do so, call SetLoadingMessage, the same way as the Browse Loading state, except set the const bool bBrowse parameter as false just before calling the SwitchContent(EContentType::BROWSE_LOADING).

Preview of the Join Loading state

Join Error state

Comprised of an error message which can be set to any message. To do so, call SetErrorMessage with the const bool bBrowse parameter as false just before calling SwitchContent(EContentType::JOIN_ERROR).

Preview of the Join Error state

About the Browse Match DS

The Browse Match DS widget consists of only one button that will be spawned inside the Browse Match menu. This widget will be the one responsible to control the Browse Match menu widget.

The Browse Match DS widget consists of two parts:

  • The C++ class BrowseMatchDSWidget_Starter where most of our implementation will be in.
    • Header file: \Source\AccelByteWars\TutorialModules\Play\MatchSessionDS\UI\BrowseMatchDSWidget_Starter.h
    • CPP file: \Source\AccelByteWars\TutorialModules\MatchSessionDS\UI\BrowseMatchDSWidget_Starter.cpp
  • The widget blueprint class W_BrowseMatchDS_Starter that was created and designed using Unreal Motion Graphics (UMG).
    • Widget Blueprint file: \Content\TutorialModules\MatchSessionDS\UI\W_BrowseMatchDS_Starter.uasset

In the header file, you will see OnlineSession which is your gateway to the session functionalities, the button itself, and a pointer to the parent widget.

private:
UPROPERTY()
UAccelByteWarsOnlineSessionBase- OnlineSession;

UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UCommonButtonBase- Btn_Refresh;

UPROPERTY()
UBrowseMatchWidget- W_Parent;

The codes that assigned the Online Session class and to the parent widget, is seen on the NativeOnActivated function.

void UBrowseMatchDSWidget_Starter::NativeOnActivated()
{
Super::NativeOnActivated();

UOnlineSession- BaseOnlineSession = GetWorld()->GetGameInstance()->GetOnlineSession();
if (!ensure(BaseOnlineSession))
{
return;
}
OnlineSession = Cast<UAccelByteWarsOnlineSessionBase>(BaseOnlineSession);
ensure(OnlineSession);

W_Parent = GetFirstOccurenceOuter<UBrowseMatchWidget>();
if (!ensure(W_Parent))
{
return;
}

...
}

Here is a preview of the Browse Match DS widget.

Preview of the Browse Match DS widget

Ready the Browse Match DS widget

The functionalities needed for the Browse Match DS widget are browse session, cancel joining session, and join session. You are going to prepare the widget for those functionalities.

  1. Open the BrowseMatchDSWidget_Starter header file and add the following function declarations.

    protected:
    void FindSessions(const bool bForce) const;
    void OnFindSessionComplete(const TArray<FMatchSessionEssentialInfo> SessionEssentialsInfo, bool bSucceeded);
  2. Open the BrowseMatchDSWidget_Starter CPP file and add these implementations. For FindSessions, change the Browse Match menu widget state to its Browse Loading state. If the OnCreateSessionComplete succeeds and the SessionEssentialsInfo array is not empty, transition the menu widget state to Browse Not Empty and populate the parent widget List View. If it succeeds but the SessionEssentialsInfo is empty, transition to Browse Empty state. Otherwise, transition to Browse Error state.

    void UBrowseMatchDSWidget_Starter::FindSessions(const bool bForce) const
    {
    W_Parent->SetLoadingMessage(TEXT_LOADING_DATA, true, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::BROWSE_LOADING);
    Btn_Refresh->SetIsEnabled(false);

    // TODO: Call the find session through Online Session
    }

    void UBrowseMatchDSWidget_Starter::OnFindSessionComplete(const TArray<FMatchSessionEssentialInfo> SessionEssentialsInfo, bool bSucceeded)
    {
    Btn_Refresh->SetIsEnabled(true);

    if (bSucceeded)
    {
    if (SessionEssentialsInfo.IsEmpty())
    {
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::BROWSE_EMPTY);
    }
    else
    {
    TArray<UMatchSessionData*> MatchSessionDatas;
    for (const FMatchSessionEssentialInfo& SessionEssentialInfo : SessionEssentialsInfo)
    {
    UMatchSessionData- MatchSessionData = NewObject<UMatchSessionData>();
    MatchSessionData->SessionEssentialInfo = SessionEssentialInfo;
    MatchSessionData->OnJoinButtonClicked.BindUObject(this, &ThisClass::JoinSession);
    MatchSessionDatas.Add(MatchSessionData);
    }

    W_Parent->GetListViewWidgetComponent()->ClearListItems();
    W_Parent->GetListViewWidgetComponent()->SetListItems<UMatchSessionData*>(MatchSessionDatas);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::BROWSE_NOT_EMPTY);
    }
    }
    else
    {
    W_Parent->SetErrorMessage(TEXT_FAILED_TO_RETRIEVE_DATA, true);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::BROWSE_ERROR);
    }
    }
  3. For the cancel joining functionality, go back to BrowseMatchDSWidget_Starter header file and add the following function declarations.

    protected:
    void CancelJoining() const;
    void OnCancelJoiningComplete(FName SessionName, bool bSucceeded) const;
  4. For the implementation, open the BrowseMatchDSWidget_Starter CPP file and add these function implementations. For the CancelJoiningSession, change the menu state to "Join Loading" with the cancel button disabled. If the OnCancelJoiningSessionComplete succeeds, transition back to the "Browse Not Empty" state. Otherwise, transition to "Join Error" state.

    void UBrowseMatchDSWidget_Starter::CancelJoining() const
    {
    W_Parent->SetLoadingMessage(TEXT_LEAVING_SESSION, false, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::JOIN_LOADING);

    // TODO: Call the cancel joining through Online Session
    }

    void UBrowseMatchDSWidget_Starter::OnCancelJoiningComplete(FName SessionName, bool bSucceeded) const
    {
    // Abort if not a game session.
    if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
    {
    return;
    }

    if (bSucceeded)
    {
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::BROWSE_NOT_EMPTY);
    }
    else
    {
    W_Parent->SetErrorMessage(TEXT_FAILED_TO_LEAVE_SESSION, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::JOIN_ERROR);
    }
    }
  5. Now, implement the UI logic for join session. Go back to BrowseMatchDSWidget_Starter header file and add the following function declarations.

    protected:
    void JoinSession(const FOnlineSessionSearchResult&SessionSearchResult) const;
    void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type CompletionType) const;
  6. Open BrowseMatchDSWidget_Starter CPP file and add the following implementations. JoinSession will transition the menu widget to its Join Loading state. OnJoinSessionComplete, if succeeded, will basically do nothing. Otherwise, transition to Join Error and displays the error type in the error message.

    void UBrowseMatchDSWidget_Starter::JoinSession(const FOnlineSessionSearchResult& SessionSearchResult) const
    {
    if (OnlineSession->ValidateToJoinSession.IsBound() &&
    !OnlineSession->ValidateToJoinSession.Execute(SessionSearchResult))
    {
    return;
    }

    W_Parent->SetLoadingMessage(TEXT_JOINING_SESSION, false, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::JOIN_LOADING);

    // TODO: Call the join session through Online Session
    }

    void UBrowseMatchDSWidget_Starter::OnJoinSessionComplete(
    FName SessionName,
    EOnJoinSessionCompleteResult::Type CompletionType) const
    {
    // Abort if not a game session.
    if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
    {
    return;
    }

    bool bSucceeded;
    FText ErrorMessage;

    switch (CompletionType)
    {
    case EOnJoinSessionCompleteResult::Success:
    bSucceeded = true;
    ErrorMessage = FText();
    break;
    case EOnJoinSessionCompleteResult::SessionIsFull:
    bSucceeded = false;
    ErrorMessage = TEXT_FAILED_SESSION_FULL;
    break;
    case EOnJoinSessionCompleteResult::SessionDoesNotExist:
    bSucceeded = false;
    ErrorMessage = TEXT_FAILED_SESSION_NULL;
    break;
    case EOnJoinSessionCompleteResult::CouldNotRetrieveAddress:
    bSucceeded = false;
    ErrorMessage = TEXT_FAILED_TO_JOIN_SESSION;
    break;
    case EOnJoinSessionCompleteResult::AlreadyInSession:
    bSucceeded = false;
    ErrorMessage = TEXT_FAILED_ALREADY_IN_SESSION;
    break;
    case EOnJoinSessionCompleteResult::UnknownError:
    bSucceeded = false;
    ErrorMessage = TEXT_FAILED_TO_JOIN_SESSION;
    break;
    default:
    bSucceeded = true;
    ErrorMessage = FText();
    }

    W_Parent->SetErrorMessage(ErrorMessage, false);
    W_Parent->SetLoadingMessage(TEXT_JOINING_SESSION, false, true);
    W_Parent->SwitchContent(bSucceeded ?
    UBrowseMatchWidget::EContentType::JOIN_LOADING :
    UBrowseMatchWidget::EContentType::JOIN_ERROR);
    }
  7. You can also let the players know when the DS is ready or when there's an error. Go back to the BrowseMatchDSWidget_Starter header file and add this function declaration.

    protected:
    void OnSessionServerUpdateReceived(
    const FName SessionName,
    const FOnlineError& Error,
    const bool bHasClientTravelTriggered) const;
  8. Open the BrowseMatchDSWidget_Starter CPP file and add this function implementation. If this function succeeds, change the loading message. Otherwise, show the error.

    void UBrowseMatchDSWidget_Starter::OnSessionServerUpdateReceived(
    const FName SessionName,
    const FOnlineError& Error,
    const bool bHasClientTravelTriggered) const
    {
    // Abort if not a game session.
    if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
    {
    return;
    }

    if (Error.bSucceeded && !bHasClientTravelTriggered)
    {
    // waiting for further update
    W_Parent->SetLoadingMessage(TEXT_JOINING_SESSION, false, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::JOIN_LOADING);
    }
    else if (!bHasClientTravelTriggered && !Error.bSucceeded)
    {
    W_Parent->SetErrorMessage(TEXT_FAILED_TO_JOIN_SESSION, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::JOIN_ERROR);
    }
    }
  9. With all the functions declared and implemented, bind the widget components to those functions. In the BrowseMatchDSWidget_Starter CPP file, navigate to the NativeOnActivated function and add the highlighted lines in the following codes. Notice that the FindSessions is called in this function. This will trigger that function every time the player open the menu widget.

    void UBrowseMatchDSWidget_Starter::NativeOnActivated()
    {
    ...

    // TODO: Add your Online Session delegates setup here

    Btn_Refresh->OnClicked().AddUObject(this, &ThisClass::FindSessions, true);
    W_Parent->GetJoiningWidgetComponent()->OnCancelClicked.AddUObject(this, &ThisClass::CancelJoining);

    FindSessions(false);
    }
  10. With the binding done, time to implement codes to unbind it when the widget is no longer in use. Still in the CPP file, navigate to NativeOnDeactivated and add the highlighted lines in the following codes.

    void UBrowseMatchDSWidget_Starter::NativeOnDeactivated()
    {
    ...

    // TODO: Add your Online Session delegates cleanup here

    Btn_Refresh->OnClicked().RemoveAll(this);
    W_Parent->GetJoiningWidgetComponent()->OnCancelClicked.RemoveAll(this);
    }
  11. Now, build the AccelByteWars project and open it with Unreal Editor once it's done building.

  12. In the Unreal Editor, from the Content Browser, navigate to Content\TutorialModules\Play\MatchSessionDS\UI\ and open W_BrowseMatchDS_Starter. Make sure that all widgets are bound properly in the Bind Widgets tab, stacked just beside the Hierarchy tab by default, and the Parent class is set to BrowseMatchDSWidget_Starter.

    Bind widgets tab

  13. To test out the Browse Match DS widget, open Content\TutorialModules\Play\MatchSessionDS\DA_MatchSessionDSEssentials.uasset and enable the Is Starter Mode Active. Save the Data Asset.

    Data Asset changes preview

  14. Click Play in the editor, navigate to Online Play > Play Online > Browse Match. The starter UI appears.

  15. Congratulations! You have finished setting up the Browse Match DS widget.

Resources