マッチ閲覧メニュー - 専用サーバーで参加可能なセッション - (Unreal Engine モジュール)
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
- 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 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;
}
}
Browse Empty state
This state consists of static text stating that there are no sessions 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
. 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; }
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;
}
}
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
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)
.
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
- Header file:
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
- 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 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:
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.
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);Open the
BrowseMatchDSWidget_Starter
CPP file and add the implementations below. ForFindSessions
, change the Browse Match menu widget state to its Browse Loading state. If theOnCreateSessionComplete
succeeds and theSessionEssentialsInfo
array is not empty, 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 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);
}
}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;Open the
BrowseMatchDSWidget_Starter
CPP file and add the function implementations below. For theCancelJoiningSession
, change the menu state to Join Loading with the cancel button disabled. If theOnCancelJoiningSessionComplete
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);
}
}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;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);
}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;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);
}
}With all the functions declared and implemented, bind the widget components to those functions. In the
BrowseMatchDSWidget_Starter
CPP file, navigate to theNativeOnActivated
function and add the code below. Notice thatFindSessions
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);
}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);
}Now, build the project and open it with the Unreal Editor.
In the Unreal Editor, from the Content Browser, navigate to
Content\TutorialModules\Play\MatchSessionDS\UI\
and openW_BrowseMatchDS_Starter
. Make sure that all widgets are bound properly in the Bind Widgets tab and the Parent class is set toBrowseMatchDSWidget_Starter
.Open
Content\TutorialModules\Play\MatchSessionDS\DA_MatchSessionDSEssentials.uasset
and enable theIs Starter Mode Active
. Save the Data Asset.Click Play in the editor. Navigate to Online Play > Play Online > Browse Match. The UI will appear if implemented correctly.
Resources
- The files used in this tutorial section are available in the Byte Wars GitHub repository.
- AccelByteWars/Content/TutorialModules/Play/MatchSessionDS/UI/W_BrowseMatchDS_Starter.uasset
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/UI/BrowseMatchDSWidget_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchSessionDS/UI/BrowseMatchDSWidget_Starter.cpp
- AccelByteWars/Content/TutorialModules/Play/MatchSessionDS/UI/DA_MatchSessionDSEssentials.uasset