Skip to content
UCIK Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Play your first match

Sign in two players, create a lobby, and join the same playable map.

Use the EOS only preset for the first pass, configure EOS, 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

Host the first match
Preparing your graph…

Loading the interactive viewer.

Join the first match
Preparing your graph…

Loading the interactive viewer.

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.

// 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);
};
// 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, voice, or Steam setup.

Was this page helpful?