Add quick play menu - Quick match with dedicated server - (Unreal Engine module)
About the Quick Play UI
In Byte Wars, Quick Play is where you can access the matchmaking feature, where a player chooses a game mode and server type to matchmake to. In the Byte Wars project, Quick Play is a widget to play the game by using matchmaking. In the widget, there are two types of game modes to select: Elimination and Team Deathmatch. Elimination is a four-player free-for-all (FFA), while Team Deathmatch consists of two competing teams of two players.
The Quick Play widget is available in the Resources section and consists of following files:
- Header file:
/Source/AccelByteWars/TutorialModules/Play/MatchmakingEssentials/UI/QuickPlayWidget.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Play/MatchmakingEssentials/UI/QuickPlayWidget.cpp
- Blueprint widget:
/Content/TutorialModules/Play/MatchmakingEssentials/UI/W_QuickPlay.uasset
This widget has different states to show the relevant user interface (UI) based on the matchmaking state. The states are:
- Select Game Mode: showing buttons to select a game mode.
- Select Server Type: showing buttons to select what type of server to use for matchmaking. This is where the matchmaking dedicated server button will be spawned.
The important thing to note from this widget is the way to access the selected game mode.
EGameModeType UQuickPlayWidget::GetSelectedGameModeType() const
{
return SelectedGameModeType;
}
About the Matchmaking DS UI
The Matchmaking DS (Dedicated Server) UI menu is where the actual matchmaking flow will be shown. The related files are available in the Resources section and consists of the following files:
- Header file:
/Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/UI/MatchmakingDSWidget_Starter.h
- CPP file:
/Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/UI/MatchmakingDSWidget_Starter.cpp
- Blueprint widget:
/Content/TutorialModules/Play/MatchmakingDS/UI/W_MatchmakingDS_Starter.uasset
This widget already has some functions provided, so you can focus on the integration.
OnlineSession
variable, which is your gateway to the session functions.
private:
UPROPERTY()
UAccelByteWarsOnlineSessionBase* OnlineSession;
void UMatchmakingDSWidget_Starter::NativeOnActivated()
{
// ...
UOnlineSession* BaseOnlineSession = GetWorld()->GetGameInstance()->GetOnlineSession();
if (!ensure(BaseOnlineSession))
{
return;
}
OnlineSession = Cast<UAccelByteWarsOnlineSessionBase>(BaseOnlineSession);
ensure(OnlineSession);
// ...
}
SessionInvite
variable to store the current session invite of the match
private:
// ...
TSharedPtr<FOnlineSessionInviteAccelByte> SessionInvite;
- The
SelectedGameModeType
variable to store the selected game mode type. Retrieved from the Quick Play UI.
private:
// ...
EGameModeType SelectedGameModeType;
void UMatchmakingDSWidget_Starter::NativeOnActivated()
{
// ...
UAccelByteWarsBaseUI* BaseUIWidget = Cast<UAccelByteWarsGameInstance>(GetGameInstance())->GetBaseUIWidget();
for (const UCommonActivatableWidget* Widget : BaseUIWidget->Stacks[EBaseUIStackType::Menu]->GetWidgetList())
{
if (const UQuickPlayWidget* QuickPlayWidget = Cast<UQuickPlayWidget>(Widget))
{
SelectedGameModeType = QuickPlayWidget->GetSelectedGameModeType();
}
}
// ...
}
High-level states to represents each matchmaking state:
- Request Sent: Loading screen indicating that a request has just been sent and is currently waiting for a response.
- Finding Match: Loading screen with a cancel button indicating the search for a match.
- Match Found: Loading screen indicating that the match has just been found and it's about to move to the next step.
- Canceling Match: Loading screen indicating that the cancel match request has just been sent and is currently waiting for a response.
- Waiting for Player: A screen where the player can Join or Reject the found match.
- Rejecting Match: Loading screen indicating that the reject match request has just been sent and is currently waiting for a response.
- Joining Match: Loading screen indicating that the join session request has just been sent and is currently waiting for a response.
- Session Joined: Loading screen indicating that the join session success response has just been received and it's about to move to the next step.
- Requesting Server: Loading screen indicating that the server has been requested and is currently waiting for the server to be ready.
- Error: General error screen with a retry button.
A function to switch between those high-level states:
// ...
protected:
// ...
void ChangeWidgetState(const EWidgetState State);
Low-level states that are the actual distinct widget components to represent the high-level states:
Loading: A screen indicating a task is in progress with text, subtext, and a cancel button.
Error: A screen indicating a task has failed with error text and a retry button.
Waiting for Player: A screen with countdown text, a join button, and a reject button.
The state changes are possible using the Unreal Motion Graphics's Widget Switcher with three widget components. Here are the declarations of those components:
// ...
private:
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UWidgetSwitcher* Ws_Root;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UWidget* W_Loading;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UWidget* W_Error;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UWidget* W_WaitingForPlayer;
Loading state
Here, player will see text, subtext (depending on the high-level state), and a cancel button that might be disabled depending on the high-level state.
Those components are declared in the Header file:
// ...
private:
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UCommonButtonBase* Btn_Cancel;
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UTextBlock* Tb_LoadingText;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UTextBlock* Tb_LoadingSubText;
Error state
In this state, player will see error text and a retry button.
Those components are declared in the Header file:
// ...
private:
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UCommonButtonBase* Btn_Retry;
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UTextBlock* Tb_ErrorText;
Waiting for Player state
This state is where the player can choose whether to join a match or reject it. It consists of a countdown, a join button, and a reject button.
// ...
private:
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UCommonButtonBase* Btn_Reject;
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UCommonButtonBase* Btn_Join;
// ...
UPROPERTY(BlueprintReadOnly, meta = (BindWidget, BlueprintProtected = true, AllowPrivateAccess = true))
UTextBlock* Tb_WaitingForPlayersCountdown;
The countdown will be used as an auto join function. If the player has not clicked anything when the countdown hits zero, it will activate the action that you will define later. Here are the countdown variables declaration in the Header file and its implementation in the CPP file:
// ...
private:
// ...
const float AutoJoinDelay = 10;
// ...
float AutoJoinCurrentCountdown = 0;
void UMatchmakingDSWidget_Starter::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
MoveCameraToTargetLocation(InDeltaTime, FVector(60.0f, 800.0f, 160.0f));
// Manual "Auto" Join
if (WidgetState == EWidgetState::WAITING_FOR_PLAYER && AutoJoinCurrentCountdown > 0)
{
AutoJoinCurrentCountdown -= InDeltaTime;
Tb_WaitingForPlayersCountdown->SetText(FText::FromString(FString::FromInt(AutoJoinCurrentCountdown)));
if (AutoJoinCurrentCountdown <= 0)
{
// ...
}
}
// ...
}
Ready the UI
In this section, you are going to prepare the MatchmakingDSWidget_Starter
widget class.
Declare some functions to be called to trigger matchmaking and related events. Open the
MatchmakingDSWidget_Starter
Header file and add the following code:protected:
UFUNCTION()
void StartMatchmaking();
UFUNCTION()
void JoinSession();
UFUNCTION()
void CancelMatchmaking();
UFUNCTION()
void RejectSessionInvite();Open the
MatchmakingDSWidget_Starter
CPP file and define theStartMatchmaking()
function. Later, you will trigger it to start matchmaking, but for now, add the dummy function below. TheW_Parent
variable is a reference to the parent widget, which is the Quick Play widget. This way, you can switch the Quick Play's state.void UMatchmakingDSWidget_Starter::StartMatchmaking()
{
if (OnlineSession->ValidateToStartMatchmaking.IsBound() &&
!OnlineSession->ValidateToStartMatchmaking.Execute(SelectedGameModeType))
{
return;
}
// Reset stored invite
SessionInvite = nullptr;
// Reset auto join session countdown.
AutoJoinCurrentCountdown = AutoJoinDelay;
MatchFoundCurrentCountdown = MatchFoundDelay;
SessionJoinedCurrentCountdown = SessionJoinedDelay;
Tb_WaitingForPlayersCountdown->SetText(FText::FromString(FString::FromInt(AutoJoinCurrentCountdown)));
ChangeWidgetState(EWidgetState::REQUEST_SENT);
// ...
}Define the
JoinSession()
function. You will call the actual join session function here later. For now, add the following dummy function:void UMatchmakingDSWidget_Starter::JoinSession()
{
if (!SessionInvite)
{
ChangeWidgetState(EWidgetState::ERROR);
Tb_ErrorText->SetText(TEXT_FAILED_SESSION_INVITE_INVALID);
}
ChangeWidgetState(EWidgetState::JOINING_MATCH);
// ...
}Define the
CancelMatchmaking()
function. Later, you will trigger it to cancel the matchmaking, but for now, add the following dummy function:void UMatchmakingDSWidget_Starter::CancelMatchmaking()
{
ChangeWidgetState(EWidgetState::CANCELING_MATCH);
// ...
}Define the
RejectSessionInvite()
function. You will call the actual reject session invite functionality here later. For now, add the following dummy function:void UMatchmakingDSWidget_Starter::RejectSessionInvite()
{
if (!SessionInvite)
{
ChangeWidgetState(EWidgetState::ERROR);
Tb_ErrorText->SetText(TEXT_FAILED_SESSION_INVITE_INVALID);
}
ChangeWidgetState(EWidgetState::REJECTING_MATCH);
// ...
}Back to the
MatchmakingDSWidget_Starter
Header file. Declare some additional functions. These functions will handle the callback when the matchmaking events complete.protected:
// ...
void OnStartMatchmakingComplete(FName SessionName, bool bSucceeded);
void OnMatchmakingComplete(FName SessionName, bool bSucceeded);
void OnSessionInviteReceived(
const FUniqueNetId& UserId,
const FUniqueNetId& FromId,
const FOnlineSessionInviteAccelByte& Invite);
void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result);
void OnSessionServerUpdateReceived(
const FName SessionName,
const FOnlineError& Error,
const bool bHasClientTravelTriggered);
void OnCancelMatchmakingComplete(FName SessionName, bool bSucceeded);
void OnRejectSessionInviteComplete(bool bSucceeded);Open the
MatchmakingDSWidget_Starter
CPP file and define theOnStartMatchmakingComplete()
function. This function handles the callback when the start matchmaking process completes to switch the Quick Play widget to a relevant state. TheOnlineSession
variable is a reference to the current active online session. More information about online sessions will be covered later.void UMatchmakingDSWidget_Starter::OnStartMatchmakingComplete(FName SessionName, bool bSucceeded)
{
// Abort if not a game session.
if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
{
return;
}
if (bSucceeded)
{
ChangeWidgetState(EWidgetState::FINDING_MATCH);
}
else
{
Tb_ErrorText->SetText(TEXT_FAILED_FIND_MATCH);
ChangeWidgetState(EWidgetState::ERROR);
}
}Define the
OnMatchmakingComplete()
function. This function handles the callback when the matchmaking process completes to switch the widget to a relevant state.void UMatchmakingDSWidget_Starter::OnMatchmakingComplete(FName SessionName, bool bSucceeded)
{
// Abort if not a game session.
if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
{
return;
}
if (bSucceeded)
{
ChangeWidgetState(EWidgetState::MATCH_FOUND);
}
else
{
Tb_ErrorText->SetText(TEXT_FAILED_FIND_MATCH);
ChangeWidgetState(EWidgetState::ERROR);
}
}Define the
OnSessionInviteReceived()
function. This function handles the callback when the game has received an invite to the session matchmaking found to switch the widget to a relevant state. There are two possible outcomes in this function. You will check the auto join flag first and, if it is enabled, transition the widget toJOINING_MATCH
. If not, transition toWAITING_FOR_PLAYER
. This function is also responsible to store the session invite to a variable so we can use it later for other functions. We will discuss how to enable or disable the auto join feature later.void UMatchmakingDSWidget_Starter::OnSessionInviteReceived(
const FUniqueNetId& UserId,
const FUniqueNetId& FromId,
const FOnlineSessionInviteAccelByte& Invite)
{
// Abort if not a game session.
if (Invite.SessionType != EAccelByteV2SessionType::GameSession)
{
return;
}
// Store session invite for later use
SessionInvite = MakeShared<FOnlineSessionInviteAccelByte>(Invite);
// Check if auto join is enabled or not
const TSharedPtr<FOnlineSessionInfoAccelByteV2> SessionInfo =
StaticCastSharedPtr<FOnlineSessionInfoAccelByteV2>(Invite.Session.Session.SessionInfo);
check(SessionInfo.IsValid());
const bool bAutoJoin = SessionInfo->GetBackendSessionData()->Configuration.AutoJoin;
/**
* If auto join, show joining match screen, else show waiting for players screen.
* Only if the match found screen has been up for longer than MatchFoundDelay
*/
if (MatchFoundCurrentCountdown <= 0)
{
ChangeWidgetState(bAutoJoin ? EWidgetState::JOINING_MATCH : EWidgetState::WAITING_FOR_PLAYER);
}
}Define the
OnJoinSessionComplete()
function. This function handles the callback when the join session process completes to switch the widget to a relevant state.void UMatchmakingDSWidget_Starter::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result)
{
// Abort if not a game session.
if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
{
return;
}
if(Result == EOnJoinSessionCompleteResult::Success)
{
ChangeWidgetState(EWidgetState::SESSION_JOINED);
}
else
{
Tb_ErrorText->SetText(TEXT_FAILED_JOIN_MATCH);
ChangeWidgetState(EWidgetState::ERROR);
}
}Define the
OnSessionServerUpdateReceived()
function. This function handles the callback when the game receives any update about the server from the backend to switch the widget to a relevant state.void UMatchmakingDSWidget_Starter::OnSessionServerUpdateReceived(
const FName SessionName,
const FOnlineError& Error,
const bool bHasClientTravelTriggered)
{
if (Error.bSucceeded && SessionJoinedCurrentCountdown <= 0)
{
ChangeWidgetState(EWidgetState::REQUESTING_SERVER);
}
else if (!Error.bSucceeded)
{
Tb_ErrorText->SetText(TEXT_FAILED_FIND_SERVER);
ChangeWidgetState(EWidgetState::ERROR);
}
}Define the
OnCancelMatchmakingComplete()
function. This function handles the callback for when the cancel matchmaking process completes to switch the Quick Play widget to a relevant state.void UMatchmakingDSWidget_Starter::OnCancelMatchmakingComplete(FName SessionName, bool bSucceeded)
{
// Abort if not a game session.
if (!SessionName.IsEqual(OnlineSession->GetPredefinedSessionNameFromType(EAccelByteV2SessionType::GameSession)))
{
return;
}
if (bSucceeded)
{
DeactivateWidget();
}
else
{
Tb_ErrorText->SetText(TEXT_FAILED_CANCEL_MATCH);
ChangeWidgetState(EWidgetState::ERROR);
}
}Define the
OnRejectSessionInviteComplete()
function. This function handles the callback when the reject session invite process completes to switch the Quick Play widget to a relevant state.void UMatchmakingDSWidget_Starter::OnRejectSessionInviteComplete(bool bSucceeded)
{
if (bSucceeded)
{
DeactivateWidget();
}
else
{
Tb_ErrorText->SetText(TEXT_FAILED_REJECT_MATCH);
ChangeWidgetState(EWidgetState::ERROR);
}
}Bind the buttons to start the matchmaking. In the
NativeOnActivated()
function, add the code below.void UMatchmakingDSWidget_Starter::NativeOnActivated()
{
// ...
Btn_Join->OnClicked().AddUObject(this, &ThisClass::JoinSession);
Btn_Cancel->OnClicked().AddUObject(this, &ThisClass::CancelMatchmaking);
Btn_Reject->OnClicked().AddUObject(this, &ThisClass::RejectSessionInvite);
Btn_Retry->OnClicked().AddUObject(this, &ThisClass::StartMatchmaking);
// ...
}Unbind those buttons when the widget is not active. In the
NativeOnDeactivated()
add the following code:void UMatchmakingDSWidget_Starter::NativeOnDeactivated()
{
Btn_Join->OnClicked().RemoveAll(this);
Btn_Cancel->OnClicked().RemoveAll(this);
Btn_Reject->OnClicked().RemoveAll(this);
Btn_Retry->OnClicked().RemoveAll(this);
// ...
Super::NativeOnDeactivated();
}Build your project and open it in the Unreal Engine Editor. Navigate to
/Content/TutorialModules/Play/MatchmakingDS/
. There, you will find a data asset calledDA_MatchmakingDSEssentials
. Open it and enable theIs Starter Mode Active
. Then, save the data asset again. This will activate the widgets so you can navigate through them when you play the game.Play the game in the Editor. From the Main Menu, navigate to Play Online > Quick Play > Elimination > Dedicated Server. You will be able to see the loading state if the implementation was successful.
Resources
The files used in this tutorial section are available in the Unreal Byte Wars GitHub repository.
- AccelByteWars/Content/TutorialModules/Play/MatchmakingEssentials/UI/W_QuickPlay.uasset
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchmakingEssentials/UI/QuickPlayWidget.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchmakingEssentials/UI/QuickPlayWidget.cpp
- AccelByteWars/Content/TutorialModules/Play/MatchmakingDS/UI/W_MatchmakingDS_Starter.uasset
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/UI/MatchmakingDSWidget_Starter.h
- AccelByteWars/Source/AccelByteWars/TutorialModules/Play/MatchmakingDS/UI/MatchmakingDSWidget_Starter.cpp