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

Steam multiplayer

Use Steam lobbies, server discovery, authentication, and networking.

Complete Steam setup and test with two signed-in accounts. For players from several stores, use the Crossplay lobby or session API and choose a transport all players can use.

Create a Steam lobby

Create a Steam lobby
Create a Steam lobby
Preparing your graph…

Loading the interactive viewer.

Find Steam lobbies
Find Steam lobbies
Preparing your graph…

Loading the interactive viewer.

Join a selected lobby
Join a selected lobby
Preparing your graph…

Loading the interactive viewer.

Leave a Steam lobby
Leave a Steam lobby
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: SIKCore, SteamIntegrationShared, BetideCore, Engine.

#include "Engine/GameInstance.h"
#include "Interfaces/SIKOnlineServicesFactory.h"
#include "Interfaces/ISIKLobbies.h"

bool CreateSteamLobby(UGameInstance &GameInstance, FSIKAsyncCallback<FSIKLobby> Completion)
{
    const auto Services = FSIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Lobbies = Services->GetLobbiesInterface();
    if (!Lobbies)
        return false;
    FSIKLobbySettings Settings;
    Settings.MaxMembers = 4;
    Settings.LobbyType = ESIKLobbyType::Public;
    Settings.Attributes.Add(TEXT("Mode"), FSIKAttribute::String(TEXT("Coop")));
    Lobbies->CreateLobby(0, TEXT("GameLobby"), Settings, MoveTemp(Completion));
    return true;
}

void FindSteamLobbies(ISIKLobbies &Lobbies, FSIKAsyncCallback<FSIKLobbySearchResult> Completion)
{
    FSIKLobbySearchQuery Query;
    Query.MaxResults = 20;
    Query.DistanceFilter = ESIKLobbyDistanceFilter::Worldwide;
    FSIKLobbySearchFilter Filter;
    Filter.Key = TEXT("Mode");
    Filter.Value = TEXT("Coop");
    Filter.Comparison = ESIKLobbyComparison::EqualTo;
    Query.Filters.Add(Filter);
    Lobbies.FindLobbies(0, Query, MoveTemp(Completion));
}

void JoinSteamLobby(ISIKLobbies &Lobbies, const FSIKLobby &Selected,
                    FSIKAsyncCallback<FSIKLobby> Completion)
{
    Lobbies.JoinLobby(0, TEXT("GameLobby"), Selected, MoveTemp(Completion));
}

bool LeaveSteamLobby(ISIKLobbies &Lobbies, FSIKAsyncCallbackVoid Completion)
{
    const FSIKLobby Lobby = Lobbies.GetLobby(TEXT("GameLobby"));
    if (!Lobby.IsValid())
        return false;
    Lobbies.LeaveLobby(0, Lobby.LobbyId, MoveTemp(Completion));
    return true;
}

Bind callbacks with FSIKAsyncCallback<FSIKLobby>::CreateWeakLambda and inspect the asynchronous result. The function’s return value only confirms dispatch.

Keep a stable local name such as GameLobby; it is separate from Steam’s lobby ID. Create and search use Mode=Coop. Pass the selected search result to the join node. Joining establishes membership; your game still needs a running server and travel flow.

Destroy SIK Lobby leaves the local user’s lobby membership. Steam removes an empty lobby; an owner leaving a populated lobby can transfer ownership. Ownership transfer does not migrate the Unreal world.

Listen and dedicated sessions

In OSSv2, listen/presence sessions use Steam lobbies so discovery, invites, and game travel share an identity. Dedicated sessions use Steam server-browser entries. Session discovery can return both.

Host a Steam listen session
Host a Steam listen session
Preparing your graph…

Loading the interactive viewer.

Find Steam sessions
Find Steam sessions
Preparing your graph…

Loading the interactive viewer.

Join and travel to a session
Join and travel to a session
Preparing your graph…

Loading the interactive viewer.

Destroy a Steam session
Destroy a Steam session
Preparing your graph…

Loading the interactive viewer.

#include "Engine/GameInstance.h"
#include "Interfaces/SIKOnlineServicesFactory.h"
#include "Interfaces/ISIKSessions.h"

bool CreateSteamSession(UGameInstance &GameInstance, bool bDedicated,
                        FSIKAsyncCallback<FSIKSession> Completion)
{
    const auto Services = FSIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Sessions = Services->GetSessionsInterface();
    if (!Sessions)
        return false;
    FSIKSessionSettings Settings;
    Settings.MaxPlayers = 4;
    Settings.bIsDedicatedServer = bDedicated;
    Settings.bUsePresence = !bDedicated;
    Settings.bUseLobbies = !bDedicated;
    Settings.Attributes.Add(TEXT("Mode"), FSIKAttribute::String(TEXT("Coop")));
    Sessions->CreateSessionWithName(0, TEXT("GameSession"), Settings, MoveTemp(Completion));
    return true;
}

void FindSteamSessions(ISIKSessions &Sessions,
                       FSIKAsyncCallback<FSIKSessionSearchResult> Completion)
{
    FSIKSessionSearchQuery Query;
    Query.MaxResults = 20;
    Query.RequiredAttributes.Add(TEXT("Mode"), FSIKAttribute::String(TEXT("Coop")));
    Sessions.FindSessions(0, Query, MoveTemp(Completion));
}

void DestroySteamSession(ISIKSessions &Sessions, FSIKAsyncCallbackVoid Completion)
{
    Sessions.DestroySession(TEXT("GameSession"), MoveTemp(Completion));
}
Host and join with travel
#include "Kismet/GameplayStatics.h"

DEFINE_LOG_CATEGORY_STATIC(LogSteamGameTravel, Log, All);

bool HostSteamGame(UGameInstance &GameInstance, FName MapName)
{
    if (MapName.IsNone())
        return false;
    const TWeakObjectPtr<UGameInstance> WeakGameInstance(&GameInstance);
    return CreateSteamSession(
        GameInstance, false,
        FSIKAsyncCallback<FSIKSession>::CreateWeakLambda(
            &GameInstance, [WeakGameInstance, MapName](const TSIKAsyncResult<FSIKSession> &Result) {
                if (!WeakGameInstance.IsValid())
                    return;
                if (!Result.IsSuccess())
                {
                    UE_LOG(LogSteamGameTravel, Warning, TEXT("%s"), *Result.GetError().Message);
                    return;
                }
                UGameplayStatics::OpenLevel(WeakGameInstance.Get(), MapName, true, TEXT("listen"));
            }));
}

bool JoinSteamGame(UGameInstance &GameInstance, const FSIKSession &Selected)
{
    const auto Services = FSIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    const auto Sessions = Services ? Services->GetSessionsInterface() : nullptr;
    if (!Sessions)
        return false;
    const TWeakObjectPtr<UGameInstance> WeakGameInstance(&GameInstance);
    Sessions->JoinSessionByIdWithName(
        0, Selected.SessionId, TEXT("GameSession"),
        FSIKAsyncCallbackVoid::CreateWeakLambda(
            &GameInstance, [WeakGameInstance, Sessions](const TSIKAsyncResult<void> &Result) {
                if (!WeakGameInstance.IsValid())
                    return;
                if (!Result.IsSuccess())
                {
                    UE_LOG(LogSteamGameTravel, Warning, TEXT("%s"), *Result.GetError().Message);
                    return;
                }
                const FString Address = Sessions->GetConnectionString(TEXT("GameSession"));
                if (Address.IsEmpty())
                {
                    UE_LOG(LogSteamGameTravel, Warning,
                           TEXT("Joined session has no travel address"));
                    return;
                }
                UGameplayStatics::OpenLevel(WeakGameInstance.Get(), FName(*Address), true);
            }));
    return true;
}

Keep the travel examples in a GameInstance Blueprint and pass a packaged map name to the host. Joining returns an address; Open Level (by Name) starts travel. Session success does not confirm that travel finished.

Create dedicated sessions on the server. Match the advertised address and port to its reachable endpoint. A server-browser entry does not prove the game port is reachable.

Use Steam sockets for Steam-ID connections or IP for an IP/port endpoint, following transport settings. A Steam lobby alone cannot connect a non-Steam client to a Steam-only transport. Shared hosting flows: listen server, dedicated server.

Lobby data and chat

Set the party mode
Set the party mode
Preparing your graph…

Loading the interactive viewer.

Set member ready state
Set member ready state
Preparing your graph…

Loading the interactive viewer.

Send a party message
Send a party message
Preparing your graph…

Loading the interactive viewer.

Receive party messages
Receive party messages
Preparing your graph…

Loading the interactive viewer.

With ISIKLobbies from the lobby example, use the active lobby ID from GetLobby(TEXT("GameLobby")):

void SendSteamLobbyMessage(ISIKLobbies &Lobbies, const FString &LobbyId, const FString &Message,
                           FSIKAsyncCallbackVoid Completion)
{
    Lobbies.SendChatMessage(0, LobbyId, Message, MoveTemp(Completion));
}

void SetSteamPartyMode(ISIKLobbies &Lobbies, const FString &LobbyId, const FString &Mode,
                       FSIKAsyncCallbackVoid Completion)
{
    Lobbies.SetLobbyData(0, LobbyId, TEXT("Mode"), Mode, MoveTemp(Completion));
}

void SetSteamPlayerReady(ISIKLobbies &Lobbies, const FString &LobbyId, bool bReady,
                         FSIKAsyncCallbackVoid Completion)
{
    Lobbies.SetLobbyMemberData(0, LobbyId, TEXT("Ready"), bReady ? TEXT("true") : TEXT("false"),
                               MoveTemp(Completion));
}

DEFINE_LOG_CATEGORY_STATIC(LogSteamPartyMessages, Log, All);

FDelegateHandle WatchSteamMessages(ISIKLobbies &Lobbies, UObject *Listener)
{
    if (!IsValid(Listener))
        return {};
    return Lobbies.OnLobbyChatMessageReceived().AddWeakLambda(
        Listener, [](const FString &LobbyId, const FSIKLobbyChatMessage &Message) {
            UE_LOG(LogSteamPartyMessages, Log, TEXT("%s"), *Message.Message);
        });
}

void StopWatchingSteamMessages(ISIKLobbies &Lobbies, FDelegateHandle &Binding)
{
    Lobbies.OnLobbyChatMessageReceived().Remove(Binding);
    Binding.Reset();
}

Store the returned binding and remove it when closing chat; bind once before sending. SetLobbyData updates owner-controlled lobby metadata; member data belongs to the local member.

The data and chat nodes act on the lobby named in Lobby Name, GameLobby by default. Bind chat once before sending and unbind on cleanup. The receive example uses SIK Matchmaking Subsystem → On Lobby Chat Msg and Get SIK Lobby Chat Entry; its events cover joined Steam lobbies, so route by Lobby Id when showing several chats.

Keep searchable metadata small and consistent between builds. Treat messages and member attributes as untrusted player input. Use lobby events to refresh UI rather than repeatedly searching for a lobby you already joined.

Invites, ownership, and kicking

ISIKLobbies::InviteToLobby sends an invite; OnLobbyInviteReceived() reports incoming invites. Preserve the invited lobby ID and complete login before joining. Rich-presence join requests also need a valid connection or lobby target—see Crossplay parties and invites.

PromoteMember transfers Steam lobby ownership. Steam has no native owner-side lobby kick: KickMember writes a reserved marker that the addressed client observes and then leaves. Use server-side admission and removal rules for enforcement against a modified client.

Tickets and server authentication

A Steam ID supplied by a client is not proof of identity. Validate a fresh ticket using the appropriate Steam server or Web API flow before trusting that player. Session tickets and Web API tickets have different consumers; follow Valve’s authentication guide.

The native Get SIK Auth Session Ticket node returns a ticket handle and bytes. Wait for the ticket response before using it, retain the handle, and call Cancel SIK Auth Ticket when finished. The receiver ends the corresponding auth session during disconnect cleanup. Keep publisher Web API keys on the backend.

C++ exposes these calls on USIK_UserLibrary in Nodes/User/SIK_UserLibrary.h (SteamIntegrationKit). Server-side validation lives in USIK_GameServerLibrary in Nodes/GameServer/SIK_GameServerLibrary.h.

Was this page helpful?