---
title: "Play your first match"
description: "Sign in two players, create a lobby, and join the same playable map."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Use the **EOS only** preset for the first pass, configure [EOS](/integrations/eos/setup), and prepare two test accounts. Add Steam mirrors after this flow works. Include your gameplay map in the packaged build.

Use your **Game Instance** Blueprint for these flows. Replace `/Game/Maps/Arena` with your packaged gameplay map. Disable automatic login for this button-driven example.

## One host, one joining player

**Blueprint**

<BlueprintViewer title="Host the first match" src="/blueprints/first-match-host.txt" />
<BlueprintViewer title="Join the first match" src="/blueprints/first-match-join.txt" />

**C++**

Game module dependencies: `UCIKCore`, `BetideCore`, `Engine`. Use this Game Instance class, or merge its members into your own. Call **HostFirstMatch** on the host and **JoinFirstMatch** on the other player.

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// FirstMatchGameInstance.h
#pragma once

#include "Engine/GameInstance.h"
#include "UCIKIdentitySubsystem.h"
#include "Types/UCIKCrossplayTypes.h"
#include "Types/BetideResult.h"
#include "FirstMatchGameInstance.generated.h"

UCLASS()
class UFirstMatchGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable) void HostFirstMatch();
    UFUNCTION(BlueprintCallable) void JoinFirstMatch();
    UFUNCTION(BlueprintCallable) void StopWaitingForMatch();
    void Shutdown() override;

  private:
    FUCIK_Lobby PendingLobby;
    FDelegateHandle ReadyBinding;
    void SignIn(bool bHost);
    void OnLogin(const FUCIK_LoginUpdate &Update, bool bHost);
    void OnCreated(const TBetideResult<FUCIK_Lobby> &Result);
    void OnHosted(const TBetideResult<FUCIK_Lobby> &Result);
    void OnFound(const TBetideResult<TArray<FUCIK_Lobby>> &Result);
    void OnJoined(const TBetideResult<FUCIK_Lobby> &Result);
    void OnReady(const FUCIK_Lobby &Lobby);
    void OnTravel(const TBetideResult<bool> &Result);
};
```

```cpp
// Copyright (c) 2026 Betide Studio. All Rights Reserved.
// FirstMatchGameInstance.cpp
#include "FirstMatchGameInstance.h"
#include "Lobbies/UCIKLobbySubsystem.h"
#include "Travel/UCIKTravelSubsystem.h"
#include "UCIKLobbyEventsSubsystem.h"
#include "UCIKCore.h"

void UFirstMatchGameInstance::HostFirstMatch()
{
    SignIn(true);
}
void UFirstMatchGameInstance::JoinFirstMatch()
{
    SignIn(false);
}

void UFirstMatchGameInstance::SignIn(bool bHost)
{
    auto *Identity = GetSubsystem<UUCIKIdentitySubsystem>();
    if (!Identity || !GetSubsystem<UUCIKLobbySubsystem>() ||
        !GetSubsystem<UUCIK_LobbyEventsSubsystem>() || !GetSubsystem<UUCIKTravelSubsystem>())
    {
        UE_LOG(LogUCIK, Warning, TEXT("Required Crossplay subsystem is unavailable"));
        return;
    }
    if (Identity->IsLoginInProgress() || ReadyBinding.IsValid())
    {
        UE_LOG(LogUCIK, Warning, TEXT("A login or match wait is already active"));
        return;
    }
    FUCIK_LoginRequest Request;
    Request.LocalUserIndex = 0;
    Request.MethodId = TEXT("Betide.EOS.Auth.AccountPortal");
    Request.AccountPolicy = EUCIK_AccountPolicy::SignInOrCreate;
    Identity->Login(Request,
                    FUCIKLoginUpdateCallback::CreateUObject(this, &ThisClass::OnLogin, bHost));
}

void UFirstMatchGameInstance::OnLogin(const FUCIK_LoginUpdate &Update, bool bHost)
{
    if (!Update.Operation.IsTerminal())
        return;
    if (Update.Operation.State != EUCIK_LoginOperationState::Succeeded)
    {
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Update.Error.GetLogString());
        return;
    }
    auto *Lobbies = GetSubsystem<UUCIKLobbySubsystem>();
    if (!Lobbies)
        return;
    if (bHost)
    {
        FUCIK_LobbySettings Settings;
        Settings.MaxMembers = 4;
        Settings.JoinPolicy = EUCIK_JoinPolicy::Public;
        FUCIK_Attribute Mode;
        Mode.StringValue = TEXT("Coop");
        Settings.Attributes.Add(TEXT("Mode"), Mode);
        Lobbies->CreateLobby(
            TEXT("GameLobby"), Settings,
            FBetideCallback<FUCIK_Lobby>::CreateUObject(this, &ThisClass::OnCreated));
        return;
    }
    FUCIK_LobbySearch Search;
    Search.MaxResults = 20;
    Search.MinOpenSlots = 1;
    FUCIK_SearchFilter Filter;
    Filter.Key = TEXT("Mode");
    Filter.Value.StringValue = TEXT("Coop");
    Search.Filters.Add(Filter);
    Lobbies->FindLobbies(
        Search, FBetideCallback<TArray<FUCIK_Lobby>>::CreateUObject(this, &ThisClass::OnFound));
}

void UFirstMatchGameInstance::OnCreated(const TBetideResult<FUCIK_Lobby> &Result)
{
    if (!Result.bOk)
    {
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
        return;
    }
    if (auto *Lobbies = GetSubsystem<UUCIKLobbySubsystem>())
        Lobbies->HostLobbyListenServer(
            TEXT("GameLobby"), TEXT("/Game/Maps/Arena"), {},
            FBetideCallback<FUCIK_Lobby>::CreateUObject(this, &ThisClass::OnHosted));
}

void UFirstMatchGameInstance::OnHosted(const TBetideResult<FUCIK_Lobby> &Result)
{
    if (!Result.bOk)
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
    else
        UE_LOG(LogUCIK, Log, TEXT("Match ready for players"));
}

void UFirstMatchGameInstance::OnFound(const TBetideResult<TArray<FUCIK_Lobby>> &Result)
{
    if (!Result.bOk)
    {
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
        return;
    }
    if (Result.GetValue().IsEmpty())
    {
        UE_LOG(LogUCIK, Log, TEXT("No Coop lobby found; start the host first"));
        return;
    }
    auto *Lobbies = GetSubsystem<UUCIKLobbySubsystem>();
    auto *Events = GetSubsystem<UUCIK_LobbyEventsSubsystem>();
    if (!Lobbies || !Events)
        return;
    StopWaitingForMatch();
    PendingLobby = Result.GetValue()[0];
    ReadyBinding =
        Events->OnLobbyGameConnectionAvailableNative.AddUObject(this, &ThisClass::OnReady);
    Lobbies->JoinLobby(PendingLobby, false,
                       FBetideCallback<FUCIK_Lobby>::CreateUObject(this, &ThisClass::OnJoined));
}

void UFirstMatchGameInstance::OnJoined(const TBetideResult<FUCIK_Lobby> &Result)
{
    if (!Result.bOk)
    {
        StopWaitingForMatch();
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
        return;
    }
    UE_LOG(LogUCIK, Log, TEXT("Joined lobby; waiting for host"));
}

void UFirstMatchGameInstance::OnReady(const FUCIK_Lobby &Lobby)
{
    if (!(Lobby.Primary == PendingLobby.Primary) && !Lobby.Mirrors.Contains(PendingLobby.Primary))
        return;
    StopWaitingForMatch();
    if (auto *Travel = GetSubsystem<UUCIKTravelSubsystem>())
        Travel->TravelToLobby(Lobby,
                              FBetideCallback<bool>::CreateUObject(this, &ThisClass::OnTravel));
}

void UFirstMatchGameInstance::OnTravel(const TBetideResult<bool> &Result)
{
    if (!Result.bOk)
        UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
    else
        UE_LOG(LogUCIK, Log, TEXT("Travel requested"));
}

void UFirstMatchGameInstance::StopWaitingForMatch()
{
    if (auto *Events = GetSubsystem<UUCIK_LobbyEventsSubsystem>())
        Events->OnLobbyGameConnectionAvailableNative.Remove(ReadyBinding);
    ReadyBinding.Reset();
}

void UFirstMatchGameInstance::Shutdown()
{
    StopWaitingForMatch();
    Super::Shutdown();
}
```

The joining Blueprint uses `PendingLobby` (Crossplay Lobby) to match readiness events. Keep one host/join attempt active at a time; disable both menu actions until that attempt ends. A hosting failure can leave the created lobby active—leave it before retrying.

Start the host first. The joining player takes the first open lobby with **Mode = Coop**; a game browser should let the player choose instead. Turn off **Party Follows Host** for this manual travel example. Call **StopWaitingForMatch** when leaving the waiting room.

The match is working when the second player reaches the host's gameplay world and both see each other's replicated pawn—not merely when Create or Join succeeds.

## Leave and repeat

Use **Destroy Crossplay Lobby** and **Clear Last Crossplay Match** on an intentional exit. In C++, call `UUCIKLobbySubsystem::LeaveLobby` and `UUCIKTravelSubsystem::ClearLastMatch`. Return to your menu, then repeat the test with the other player hosting.

Next: [invites](/crossplay/parties-invites), [voice](/crossplay/voice), or [Steam setup](/integrations/steam/setup).
