Add match browser menu - Joinable sessions with peer-to-peer - (Unreal Engine module)
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
- Header file:
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
- Widget Blueprint file:
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;
}
}
Browse Empty state
This consists of static text stating that there's no session currently available.
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; }
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;
}
}
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)
.
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)
.
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
- Header file:
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
- Widget Blueprint file:
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:
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.
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);Open the
BrowseMatchP2PWidget_Starter
CPP file and add the implementations below. ForFindSessions
, we're changing the Browse Match menu widget state to its Browse Loading state. ForOnCreateSessionComplete
, if the request succeeds and theSessionEssentialsInfo
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 theSessionEssentialsInfo
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);
}
}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;Open the
BrowseMatchP2PWidget_Starter
CPP file and add the function implementations below. For theCancelJoiningSession
, you're changing the menu state to Join Loading with the cancel button disabled. For theOnCancelJoiningSessionComplete
, 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);
}
}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;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);
}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;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);
}
}With all the functions declared and implemented, bind the widget components to those functions. In the
BrowseMatchP2PWidget_Starter
CPP file, navigate to theNativeOnActivated
function and add the code below. Notice that you also callFindSessions
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);
}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);
}Build the project and open it in the Unreal Editor.
In the Unreal Editor, from the Content Browser, navigate to
Content\TutorialModules\Play\MatchSessionP2P\UI\
and openW_BrowseMatchP2P_Starter
. Make sure that all widgets are bound properly in the Bind Widgets tab and the Parent class is set toBrowseMatchP2PWidget_Starter
.Open
Content\TutorialModules\Play\MatchSessionP2P\DA_MatchSessionP2PEssentials.uasset
and enable theIs Starter Mode Active
. Save the Data Asset.Play the game in the editor. Navigate to Online Play > Play Online > Browse Match. The starter UI will be shown if implemented correctly.
Resources
- The files used in this tutorial section are available in the Unreal Byte Wars GitHub repository.
- AccelByteWars/Content/TutorialModules/Play/MatchSessionP2P/UI/W_BrowseMatchP2P_Starter.uasset
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/UI/BrowseMatchP2PWidget_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionP2P/UI/BrowseMatchP2PWidget_Starter.cpp
- AccelByteWars/Content/TutorialModules/Play/MatchSessionP2P/UI/DA_MatchSessionP2PEssentials.uasset