Skip to main content

Browse match menu - Joinable sessions with dedicated servers - (Unreal Engine module)

Last updated on October 24, 2024

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 connects to the browse session functionality, which will be shown inside the menu widget. Those widgets are the Browse Match widget and Browse Match DS widget.

The widget that you're going to prepare is 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. It is available in the Resources section and consists of two parts:

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

The Browse Match menu has six states:

  • Browse Loading: showing a loading status for the browse session.
  • Browse Empty: showing text stating that there are currently no sessions 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 clicks 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 Graphics'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;
}

DesiredFocusTargetButton = FocusTarget;
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);
}
RequestRefreshFocus();

// Set FTUE
if (Type == EContentType::BROWSE_EMPTY || Type == EContentType::BROWSE_NOT_EMPTY)
{
InitializeFTUEDialogues(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 module.

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

Browse Loading state

This state consists of 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 Unreal Byte Wars joinable sessions dedicated server

Browse Empty state

This state consists of static text stating that there are no sessions currently available.

Preview of the Browse Empty state Unreal Byte Wars joinable sessions dedicated server

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 AccelByteWarsOnlineSessionModels.h to pass data to the entry widget of the List View.

public:
// ...
UListView* GetListViewWidgetComponent() const { return Lv_Sessions; }

Preview of the Browse Not Empty state Unreal Byte Wars joinable sessions dedicated server

Browse Error state

This sate is comprised of a text 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 Unreal Byte Wars joinable sessions dedicated server

Join Loading state

This state consists of a cancel button and 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 Unreal Byte Wars joinable sessions dedicated server

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 Unreal Byte Wars joinable sessions dedicated server

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:

  • BrowseMatchDSWidget_Starter: The C++ class where most of the implementation will be.
    • Header file: \Source\AccelByteWars\TutorialModules\Play\MatchSessionDS\UI\BrowseMatchDSWidget_Starter.h
    • CPP file: \Source\AccelByteWars\TutorialModules\MatchSessionDS\UI\BrowseMatchDSWidget_Starter.cpp
  • W_BrowseMatchDS_Starter: A widget Blueprint class 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.

protected:
// ...
UPROPERTY()
UAccelByteWarsOnlineSessionBase* OnlineSession;
private:
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UCommonButtonBase* Btn_Refresh;

UPROPERTY()
UBrowseMatchWidget* W_Parent;

The code that assigns the Online Session class and the parent widget is seen in the NativeOnActivated function.

void UBrowseMatchDSWidget_Starter::NativeOnActivated()
{
// ...
// Get online session
UOnlineSession* BaseOnlineSession = GetWorld()->GetGameInstance()->GetOnlineSession();
if (!ensure(BaseOnlineSession))
{
return;
}
OnlineSession = Cast<UAccelByteWarsOnlineSessionBase>(BaseOnlineSession);
ensure(OnlineSession);

// Get parent menu widget
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 Unreal Byte Wars joinable sessions dedicated server

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 the implementations below. 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 the 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);
    // ...
    }
    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 functionality to cancel joining, 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. Open the BrowseMatchDSWidget_Starter CPP file and add the function implementations below. 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 the Join Error state.

    void UBrowseMatchDSWidget_Starter::CancelJoining() const
    {
    W_Parent->SetLoadingMessage(TEXT_LEAVING_SESSION, false, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::JOIN_LOADING);
    // ...
    }
    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. Implement the UI logic for join session. Go back to the 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 implementations below. JoinSession will transition the menu widget to its Join Loading state. OnJoinSessionComplete, will do nothing unless it fails, where it will transition to Join Error and display 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);
    // ...
    }
    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 dedicated server 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 the function implementation below. 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)
    {
    // Keep showing the loading state until the client travels to the server.
    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 code below. Notice that FindSessions is called in this function. This will trigger that function every time the player opens the menu widget.

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

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

    void UBrowseMatchDSWidget_Starter::NativeOnDeactivated()
    {
    // ...
    Btn_Refresh->OnClicked().RemoveAll(this);
    W_Parent->GetJoiningWidgetComponent()->OnCancelClicked.RemoveAll(this);
    }
  11. Now, build the project and open it with the Unreal Editor.

  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 and the Parent class is set to BrowseMatchDSWidget_Starter.

    Bind widgets tab Unreal Byte Wars joinable sessions dedicated server

  13. Open Content\TutorialModules\Play\MatchSessionDS\DA_MatchSessionDSEssentials.uasset and enable the Is Starter Mode Active. Save the Data Asset.

    Data Asset changes preview Unreal Byte Wars joinable sessions dedicated server

  14. Click Play in the editor. Navigate to Online Play > Play Online > Browse Match. The UI will appear if implemented correctly.

Resources