---
title: "EOS multiplayer and voice"
description: "Use EOS lobbies, sessions, P2P, and RTC with their distinct lifecycles."
seo:
  noindex: true
---

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

Sign in to [EOS](/integrations/eos/identity) before creating or joining. Use [Crossplay lobbies](/crossplay/lobbies) and [sessions](/crossplay/sessions) when the same gameplay must work across providers.

| Service | Use it for | Lifetime |
| --- | --- | --- |
| Lobby | Parties, member attributes, owner changes, lobby voice | Members join and leave; the owner can destroy it |
| Session | Advertising a game, player slots, match state | Create, start, end, then destroy |

EOS lobbies and sessions are separate services. Searching one does not find entries created in the other. Neither creates an Unreal listen server by itself.

## Create a lobby with voice

**Blueprint**

<Accordion>
  <AccordionItem title="Create a co-op lobby with voice">
<BlueprintViewer title="Create a co-op lobby with voice" src="/blueprints/crossplay-eos-create-lobby.txt" />
  </AccordionItem>
  <AccordionItem title="Find co-op lobbies">
<BlueprintViewer title="Find co-op lobbies" src="/blueprints/crossplay-eos-find-lobbies.txt" />
  </AccordionItem>
  <AccordionItem title="Join a selected lobby without travel">
<BlueprintViewer title="Join a selected lobby without travel" src="/blueprints/crossplay-eos-join-lobby.txt" />
  </AccordionItem>
  <AccordionItem title="Leave a lobby">
<BlueprintViewer title="Leave a lobby" src="/blueprints/crossplay-eos-leave-lobby.txt" />
  </AccordionItem>
  <AccordionItem title="Destroy an owned lobby">
<BlueprintViewer title="Destroy an owned lobby" src="/blueprints/crossplay-eos-destroy-lobby.txt" />
  </AccordionItem>
</Accordion>

**C++**

Game module dependencies: `EIKCore`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKLobbies.h"

bool CreateEosLobby(UGameInstance &GameInstance,
                    const FEIKAsyncCallback<FEIKCreateLobbyResult> &Completion)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Lobbies = Services->GetLobbiesInterface();
    if (!Lobbies)
        return false;
    FEIKLobbySettings Settings;
    Settings.MaxMembers = 4;
    Settings.JoinPolicy = EEIKJoinPolicy::PublicAdvertised;
    Settings.bEnableVoiceChat = true;
    Settings.Attributes.Add(TEXT("Mode"), FEIKAttribute::String(TEXT("Coop")));
    Lobbies->CreateLobby(0, TEXT("GameLobby"), Settings, {}, Completion);
    return true;
}

void FindEosCoopLobbies(IEIKLobbies &Lobbies,
                        const FEIKAsyncCallback<FEIKFindLobbiesResult> &Completion)
{
    FEIKLobbySearchParams Search;
    Search.MaxResults = 20;
    Search.Attributes.Add(TEXT("Mode"), FEIKAttribute::String(TEXT("Coop")));
    Lobbies.FindLobbies(0, Search, Completion);
}

bool JoinSelectedEosLobby(IEIKLobbies &Lobbies, const FEIKLobby &Selected,
                          const FEIKAsyncCallback<FEIKJoinLobbyResult> &Completion)
{
    if (!Selected.IsValid())
        return false;
    Lobbies.JoinLobby(0, TEXT("GameLobby"), Selected, {}, Completion);
    return true;
}

bool LeaveEosLobby(IEIKLobbies &Lobbies, const FEIKAsyncCallbackVoid &Completion)
{
    const auto Lobby = Lobbies.GetLobbyByName(TEXT("GameLobby"));
    if (!Lobby.IsValid())
        return false;
    Lobbies.LeaveLobby(0, Lobby.LobbyId, Completion);
    return true;
}

bool DestroyOwnedEosLobby(IEIKLobbies &Lobbies, const FEIKAsyncCallbackVoid &Completion)
{
    const auto Lobby = Lobbies.GetLobbyByName(TEXT("GameLobby"));
    if (!Lobby.IsValid())
        return false;
    Lobbies.DestroyLobby(0, Lobby.LobbyId, Completion);
    return true;
}
```

Resolve `IEIKLobbies` from the current world’s services as shown above. Bind completion callbacks with `CreateWeakLambda`. A `true` return means dispatched; check the asynchronous result. Search returns `Lobbies`; joining through this interface does not travel.

Enable **Lobby Voice Chat** when creating the lobby. Lobby membership manages the RTC room; do not join that room again using a token-based voice flow.

Create `EosLobbyResults` (EIK Lobby array) for the search example and pass a selected entry to the join example. Search attributes match by equality in **Find EIK Lobbies**. Keep attribute names and types consistent between creation and search. Pass the selected result to **Join EIK Lobby**. Turn **Auto Travel** off for a party or pre-match lobby; joining successfully does not prove that a game server is ready.

For a direct EOS pre-match flow, publish **Mark EIK Lobby Game Ready** only after the server is reachable. Members observe **On Lobby Attributes Changed**, check readiness, and use **Travel To EIK Lobby Game**. Clear readiness when that game ends. In C++, the publishing calls are on `UEIK_UpdateLobby_AsyncFunction` in `Lobbies/EIK_UpdateLobby_AsyncFunction.h`.

Use **Update EIK Lobby** for lobby data and **Update EIK Lobby Member Attributes** for a member's data. Bind lobby events before joining so your UI receives membership and attribute changes. **Leave EIK Lobby** leaves membership; **Destroy EIK Lobby** is an owner operation. Owner election alone does not migrate the Unreal world—see [host recovery](/crossplay/recovery).

## Advertise a session

**Blueprint**

<Accordion>
  <AccordionItem title="Advertise a co-op session">
<BlueprintViewer title="Advertise a co-op session" src="/blueprints/crossplay-eos-create-session.txt" />
  </AccordionItem>
  <AccordionItem title="Find co-op sessions">
<BlueprintViewer title="Find co-op sessions" src="/blueprints/crossplay-eos-find-sessions.txt" />
  </AccordionItem>
  <AccordionItem title="Join a selected session">
<BlueprintViewer title="Join a selected session" src="/blueprints/crossplay-eos-join-session.txt" />
  </AccordionItem>
  <AccordionItem title="Leave a session">
<BlueprintViewer title="Leave a session" src="/blueprints/crossplay-eos-leave-session.txt" />
  </AccordionItem>
</Accordion>

**C++**

Using the same dependencies:

```cpp
#include "Engine/GameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKSessions.h"
#include "GameFramework/PlayerController.h"

bool CreateEosSession(UGameInstance &GameInstance,
                      const FEIKAsyncCallback<FEIKCreateSessionResult> &Completion)
{
    const auto Services = FEIKOnlineServicesFactory::Get(GameInstance.GetWorld());
    if (!Services)
        return false;
    const auto Sessions = Services->GetSessionsInterface();
    if (!Sessions)
        return false;
    FEIKSessionSettings Settings;
    Settings.MaxPlayers = 4;
    Settings.JoinPolicy = EEIKJoinPolicy::PublicAdvertised;
    Settings.Attributes.Add(TEXT("Mode"), FEIKAttribute::String(TEXT("Coop")));
    Sessions->CreateSession(0, TEXT("GameSession"), Settings, Completion);
    return true;
}

void FindEosCoopSessions(IEIKSessions &Sessions,
                         const FEIKAsyncCallback<FEIKFindSessionsResult> &Completion)
{
    FEIKSessionSearchParams Search;
    Search.MaxResults = 20;
    Search.Attributes.Add(TEXT("Mode"), FEIKAttribute::String(TEXT("Coop")));
    Sessions.FindSessions(0, Search, Completion);
}

void JoinSelectedEosSession(UGameInstance &GameInstance, IEIKSessions &Sessions,
                            const FEIKSessionSearchResult &Selected,
                            const FEIKAsyncCallback<FEIKJoinSessionResult> &Completion)
{
    const TWeakObjectPtr<UGameInstance> WeakInstance(&GameInstance);
    Sessions.JoinSession(
        0, TEXT("GameSession"), Selected,
        FEIKAsyncCallback<FEIKJoinSessionResult>::CreateWeakLambda(
            &GameInstance,
            [WeakInstance, Completion](const TEIKAsyncResult<FEIKJoinSessionResult> &Result) {
                if (!WeakInstance.IsValid())
                    return;
                if (Result.bWasSuccessful && Result.Result.IsSet() &&
                    !Result.Result->ConnectString.IsEmpty())
                {
                    if (APlayerController *Player = WeakInstance->GetFirstLocalPlayerController())
                        Player->ClientTravel(Result.Result->ConnectString, TRAVEL_Absolute);
                }
                Completion.ExecuteIfBound(Result);
            }));
}

void LeaveEosSession(IEIKSessions &Sessions, const FEIKAsyncCallbackVoid &Completion)
{
    Sessions.LeaveSession(0, TEXT("GameSession"), Completion);
}
```

Resolve `IEIKSessions` from the current world’s services as above. Bind callbacks with `CreateWeakLambda` and check their results. `StartSession`, `EndSession`, and `DestroySession` also take completion callbacks.

Create `EosSessionResults` (EIK Session Search Result array) and pass a selected entry to the join example. Join requests travel only when a connect string and local player controller exist; success does not confirm arrival. Leaving removes local membership and, for the host, ends the advertised session. It does not return players to a menu.

Keep `GameSession` consistent across the lifecycle. Set **Dedicated Server** only on an actual dedicated server.

### Start and end a match

Run these on the session owner after creating `GameSession`. Start marks it in progress; end keeps the session for later use. Neither operation travels players or destroys the session.

**Blueprint**

<BlueprintViewer title="EOS session match state" src="/blueprints/crossplay-eos-session-lifecycle.txt" />

Variable: `SessionChangePending` — Boolean, default false.

**C++**

Module dependencies: `EIKCore`, `BetideCore`, `Engine`.

```cpp
// EosMatchGameInstance.h
#pragma once
#include "Engine/GameInstance.h"
#include "EosMatchGameInstance.generated.h"

UCLASS()
class UEosMatchGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void StartMatch();
    UFUNCTION(BlueprintCallable)
    void EndMatch();
    UPROPERTY(BlueprintReadOnly)
    bool SessionChangePending = false;

  private:
    void ChangeMatchState(bool bStart);
};
```

```cpp
// EosMatchGameInstance.cpp
#include "EosMatchGameInstance.h"
#include "Interfaces/IEIKOnlineServices.h"
#include "Interfaces/IEIKSessions.h"
#include "Logging/LogMacros.h"

DEFINE_LOG_CATEGORY_STATIC(LogEosMatch, Log, All);

void UEosMatchGameInstance::StartMatch()
{
    ChangeMatchState(true);
}
void UEosMatchGameInstance::EndMatch()
{
    ChangeMatchState(false);
}

void UEosMatchGameInstance::ChangeMatchState(bool bStart)
{
    if (SessionChangePending)
    {
        UE_LOG(LogEosMatch, Warning, TEXT("A session change is already running"));
        return;
    }
    const auto Services = FEIKOnlineServicesFactory::Get(GetWorld());
    const auto Sessions = Services ? Services->GetSessionsInterface() : nullptr;
    if (!Sessions)
    {
        UE_LOG(LogEosMatch, Warning, TEXT("EOS sessions are unavailable"));
        return;
    }
    SessionChangePending = true;
    const auto Completion = FEIKAsyncCallbackVoid::CreateWeakLambda(
        this, [this, bStart](const TEIKAsyncResultVoid &Result) {
            SessionChangePending = false;
            if (!Result.IsSuccess())
            {
                UE_LOG(LogEosMatch, Warning, TEXT("%s"), *Result.Error.Message);
                return;
            }
            UE_LOG(LogEosMatch, Log, TEXT("Session %s"), bStart ? TEXT("started") : TEXT("ended"));
        });
    if (bStart)
        Sessions->StartSession(TEXT("GameSession"), Completion);
    else
        Sessions->EndSession(TEXT("GameSession"), Completion);
}
```

Select the EOS transport through the [Crossplay preset](/start/presets), then use the [listen-server](/crossplay/listen-server) or [dedicated-server](/crossplay/dedicated-server) flow. A discoverable session can still have an unavailable host or incorrect net driver.

## Positional voice

Add **EIK Voice Chat Synth Component** to the pawn class and give it an attenuation asset with spatialization on. The component plays its pawn's player (matched by Product User ID from the player state) from the pawn instead of through EOS. Fill **Supported Rooms** with the RTC room names it should take, or turn on **Use Global Room** to take every joined room. Other players keep ordinary voice. With the crossplay kit, use **Crossplay Positional Voice** instead; it picks the rooms itself.

## Control lobby voice

**Blueprint**

<Accordion>
  <AccordionItem title="List joined voice rooms">
<BlueprintViewer title="List joined voice rooms" src="/blueprints/crossplay-eos-lobby-voice-channels.txt" />
  </AccordionItem>
  <AccordionItem title="List microphones">
<BlueprintViewer title="List microphones" src="/blueprints/crossplay-eos-lobby-microphones.txt" />
  </AccordionItem>
  <AccordionItem title="Select a microphone">
<BlueprintViewer title="Select a microphone" src="/blueprints/crossplay-eos-lobby-select-microphone.txt" />
  </AccordionItem>
  <AccordionItem title="Mute or unmute your microphone">
<BlueprintViewer title="Mute or unmute your microphone" src="/blueprints/crossplay-eos-lobby-mute.txt" />
  </AccordionItem>
</Accordion>

**C++**

Add `EIKBlueprints` to the game module dependencies.

```cpp
#include "VoiceChat/EIKVoiceChatLibrary.h"

bool SetEosMicrophoneMuted(UObject *WorldContext, bool bMuted, bool &OutMuted)
{
    OutMuted = false;
    if (!UEIKVoiceChatLibrary::SetEIKLobbyVoiceChatInputMuted(WorldContext, 0, bMuted))
        return false;
    OutMuted = UEIKVoiceChatLibrary::IsEIKLobbyVoiceChatInputMuted(WorldContext, 0);
    return true;
}

bool GetEosMicrophones(UObject *WorldContext, TArray<FEIKVoiceChatDeviceInfo> &OutDevices)
{
    OutDevices.Reset();
    if (!UEIKVoiceChatLibrary::IsVoiceChatAvailable(WorldContext, 0))
        return false;
    OutDevices = UEIKVoiceChatLibrary::GetEIKLobbyVoiceChatAvailableInputDevices(WorldContext, 0);
    return true;
}

bool SelectEosMicrophone(UObject *WorldContext, const FEIKVoiceChatDeviceInfo &Device)
{
    return UEIKVoiceChatLibrary::SetEIKLobbyVoiceChatInputDevice(WorldContext, 0, Device.DeviceId);
}

bool GetJoinedEosVoiceRooms(UObject *WorldContext, TArray<FString> &OutRooms)
{
    OutRooms.Reset();
    if (!UEIKVoiceChatLibrary::IsVoiceChatAvailable(WorldContext, 0))
        return false;
    OutRooms = UEIKVoiceChatLibrary::GetJoinedEIKLobbyVoiceChatChannels(WorldContext, 0);
    return true;
}
```

Pass an entry from the microphone list to the selection example. Selection reports that the voice user received the request, not that the hardware switch succeeded. Device lists can be empty. The room list includes every joined channel for that user, including token rooms. Leaving the lobby manages its room automatically.

Use the current world's local user for voice, especially in PIE. When using EOS voice objects directly, resolve the user for that player's PUID; a global single-user voice object may not be the logged-in player. Check microphone permission and selected input/output devices when membership works but audio does not.

<Accordion>
  <AccordionItem title="Token-based voice rooms">

Use a token room for voice independent of lobby membership. Sign in to EOS first; EIK creates and logs in that player's voice user in both backends. Pass a room name and server-issued credentials for the same EOS Product User ID.

`RoomCredentials` is JSON containing `client_base_url` and `participant_token`, not a bare token. On your server, call **Create Voice Room Token** for the room and its players, then **Get EIK Voice Room Join Credentials** with the response and each player's Product User ID, and send each player their own credentials. Keep the server's privileged credentials on the server. Leave only the room when finished. The voice user is shared with lobby voice, so do not log it out or disconnect shared voice to leave one room.

With the crossplay kit, the crossplay voice nodes (mute, volume, devices, talking state, transmit mode, **Get Joined Crossplay Voice Channels**, **Crossplay Positional Voice**) act on a joined token room while no crossplay lobby voice is active. An active lobby voice keeps the controls. There is no crossplay node that joins a token room.

**Blueprint**

<Accordion>
  <AccordionItem title="Join a private voice room">
<BlueprintViewer title="Join a private voice room" src="/blueprints/crossplay-eos-token-voice-join.txt" />
  </AccordionItem>
  <AccordionItem title="Leave a private voice room">
<BlueprintViewer title="Leave a private voice room" src="/blueprints/crossplay-eos-token-voice-leave.txt" />
  </AccordionItem>
</Accordion>

**C++**

```cpp
#include "VoiceChat/EIKVoiceChatLibrary.h"

void JoinEosVoiceRoom(UObject *WorldContext, const FString &Room, const FString &RoomCredentials,
                      const FEIKVoiceChatChannelResultDelegate &Completion)
{
    if (!UEIKVoiceChatLibrary::IsVoiceChatLoggedIn(WorldContext, 0))
    {
        Completion.ExecuteIfBound(false, Room, TEXT("Sign in to EOS before joining voice"));
        return;
    }
    UEIKVoiceChatLibrary::JoinEIKTokenVoiceChatChannel(WorldContext, 0, Room, RoomCredentials,
                                                       EEIKVoiceChatChannelType::NonPositional,
                                                       Completion);
}

void LeaveEosVoiceRoom(UObject *WorldContext, const FString &Room,
                       const FEIKVoiceChatChannelResultDelegate &Completion)
{
    UEIKVoiceChatLibrary::LeaveEIKTokenVoiceChatChannel(WorldContext, 0, Room, Completion);
}
```

Bind `Completion` to a `UFUNCTION` taking `(bool bWasSuccessful, FString ChannelName, FString Error)`. Handle success and failure there; the function returning does not mean the room is joined or left.

  </AccordionItem>
  <AccordionItem title="Invites, updates, and limits">

Use **Send EIK Lobby Invite** / **Accept EIK Lobby Invite** for lobbies and **Send EIK Session Invite** / **Accept EIK Session Invite** for sessions. Subscribe through `UEIK_LobbyEventsSubsystem` or `UEIK_SessionEventsSubsystem` before sending or accepting requests. Their headers are in `Lobbies/` and `Sessions/` in `EIKBlueprints`.

Client policy, join policy, capacity, and service limits still apply. Keep searchable metadata small; do not put save data or secrets in public attributes. Check the operation's error instead of retrying a denied or full lobby indefinitely.

  </AccordionItem>
</Accordion>
