Skip to main content

Add match browser menu - Joinable sessions with peer-to-peer - (Unreal Engine module)

Last updated on October 24, 2024

Just like the Create Match Session UI, to support both server modes (peer-to-peer (P2P) and dedicated server (DS)) at once, we separate the menu widget and the widget that actually connects to the browse session functionality that will be shown inside the menu widget. Those widgets are the Browse Match widget and Browse Match P2P widget.

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

About the Browse Match menu

The 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: The C++ class where most of our 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 available sessions and a join button for each one.
  • Browse Error: showing error text for when the browse session returns an error.
  • Join Loading: showing the current joining status when the player clicks a join button.
  • Join Error: showing 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 the 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. That is a Panel Widget that will house the Browse Match P2P 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 peer-to-peer

Browse Empty state

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

Preview of the Browse Empty state Unreal Byte Wars joinable sessions peer-to-peer

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. A UObject called UMatchSessionData has been provided, which 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; }

Preview of the Browse Not Empty state Unreal Byte Wars joinable sessions peer-to-peer

Browse 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 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 peer-to-peer

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 peer-to-peer

Join Error state

This is comprised of error text 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 peer-to-peer

About the Browse Match P2P

The Browse Match P2P widget consists of a button that will spawn inside the Browse Match menu. This widget will be the one responsible to control the Browse Match menu widget. It is available in the Resources section and consists of two parts:

  • BrowseMatchP2PWidget_Starter: The C++ where most of our implementation will be.
    • Header file: \Source\AccelByteWars\TutorialModules\Play\MatchSessionP2P\UI\BrowseMatchP2PWidget_Starter.h
    • CPP file: \Source\AccelByteWars\TutorialModules\MatchSessionP2P\UI\BrowseMatchP2PWidget_Starter.cpp
  • W_BrowseMatchP2P_Starter: A widget Blueprint class that was created and designed using Unreal Motion Graphics (UMG).
    • Widget Blueprint file: \Content\TutorialModules\MatchSessionP2P\UI\W_BrowseMatchP2P_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 UBrowseMatchP2PWidget_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 P2P widget:

Preview of the Browse Match P2P widget Unreal Byte Wars joinable sessions peer-to-peer

Ready the Browse Match P2P widget

The functionality that we need for the Browse Match P2P widget is the browse session, cancel joining session, and join session. You are going to prepare the widget for those functionalities.

  1. Open the BrowseMatchP2PWidget_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 BrowseMatchP2PWidget_Starter CPP file and add the implementations below. For FindSessions, we're changing the Browse Match menu widget state to its Browse Loading state. For OnCreateSessionComplete, if the request succeeds and the SessionEssentialsInfo array is not empty, you 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 the Browse Error state.

    void UBrowseMatchP2PWidget_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 UBrowseMatchP2PWidget_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. On to the cancel joining functionality. Go back to the BrowseMatchP2PWidget_Starter Header file and add the following function declarations:

    protected:
    // ...
    void CancelJoining() const;
    void OnCancelJoiningComplete(FName SessionName, bool bSucceeded) const;
  4. Open the BrowseMatchP2PWidget_Starter CPP file and add the function implementations below. For the CancelJoiningSession, you're changing the menu state to Join Loading with the cancel button disabled. For the OnCancelJoiningSessionComplete, if successful, you're transitioning back to the Browse Not Empty state. Otherwise, transition to the Join Error state.

    void UBrowseMatchP2PWidget_Starter::CancelJoining() const
    {
    W_Parent->SetLoadingMessage(TEXT_LEAVING_SESSION, false, false);
    W_Parent->SwitchContent(UBrowseMatchWidget::EContentType::JOIN_LOADING);
    // ...
    }
    void UBrowseMatchP2PWidget_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 BrowseMatchP2PWidget_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 the BrowseMatchP2PWidget_Starter CPP file and add the implementations below. JoinSession will transition the menu widget to its Join Loading state. OnJoinSessionComplete, if successful, will do nothing. Otherwise, it will transition to Join Error and display the error type in the error message.

    void UBrowseMatchP2PWidget_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 UBrowseMatchP2PWidget_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. Let the player know when the server is ready or when there's an error. Go back to the BrowseMatchP2PWidget_Starter Header file and add this function declaration:

    protected:
    // ...
    void OnSessionServerUpdateReceived(
    const FName SessionName,
    const FOnlineError& Error,
    const bool bHasClientTravelTriggered) const;
  8. Open the BrowseMatchP2PWidget_Starter CPP file and add the function implementation below. For this function, if successful, you simply change the loading message. Otherwise, show error.

    void UBrowseMatchP2PWidget_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 P2P host.
    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 BrowseMatchP2PWidget_Starter CPP file, navigate to the NativeOnActivated function and add the code below. Notice that you also call FindSessions in this function. This will trigger that function every time the player opens the menu widget.

    void UBrowseMatchP2PWidget_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, unbind it when the widget is no longer in use. Still in the CPP file, navigate to NativeOnDeactivated and add the following code:

    void UBrowseMatchP2PWidget_Starter::NativeOnDeactivated()
    {
    // ...
    Btn_Refresh->OnClicked().RemoveAll(this);
    W_Parent->GetJoiningWidgetComponent()->OnCancelClicked.RemoveAll(this);
    }
  11. Build the project and open it in the Unreal Editor.

  12. In the Unreal Editor, from the Content Browser, navigate to Content\TutorialModules\Play\MatchSessionP2P\UI\ and open W_BrowseMatchP2P_Starter. Make sure that all widgets are bound properly in the Bind Widgets tab and the Parent class is set to BrowseMatchP2PWidget_Starter.

    Bind widgets tab Unreal Byte Wars joinable sessions peer-to-peer

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

    Data Asset changes preview Unreal Byte Wars joinable sessions peer-to-peer

  14. Play the game in the editor. Navigate to Online Play > Play Online > Browse Match. The starter UI will be shown if implemented correctly.

Resources