Skip to main content

Add quick play menu - Quick match with dedicated server - (Unreal Engine module)

Last updated on October 24, 2024

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.

Preview of the Loading 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.

Preview of the Error state

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.

Preview of the Waiting for Player state

// ...
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.

  1. 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();
  2. Open the MatchmakingDSWidget_Starter CPP file and define the StartMatchmaking() function. Later, you will trigger it to start matchmaking, but for now, add the dummy function below. The W_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);
    // ...
    }
  3. 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);
    // ...
    }
  4. 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);
    // ...
    }
  5. 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);
    // ...
    }
  6. 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);
  7. Open the MatchmakingDSWidget_Starter CPP file and define the OnStartMatchmakingComplete() function. This function handles the callback when the start matchmaking process completes to switch the Quick Play widget to a relevant state. The OnlineSession 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);
    }
    }
  8. 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);
    }
    }
  9. 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 to JOINING_MATCH. If not, transition to WAITING_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);
    }
    }
  10. 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);
    }
    }
  11. 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);
    }
    }
  12. 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);
    }
    }
  13. 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);
    }
    }
  14. 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);
    // ...
    }
  15. 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();
    }
  16. Build your project and open it in the Unreal Engine Editor. Navigate to /Content/TutorialModules/Play/MatchmakingDS/. There, you will find a data asset called DA_MatchmakingDSEssentials. Open it and enable the Is 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.

    Activate tutorial module data asset starter mode Unreal Byte Wars quick match dedicated server

  17. 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