for those who wonder, this is my lobby manager script.
its a mess, but i will try to explain it
im using codemonkey lobby example
what i did is just implemented relay to his lobbyManager script and done few changes.
this is full project link of his lobby from code monkey (not mine)
this is full lobbymanager script i edited with host migration and relay features, dont copy and paste this, wont work, instead try to understand and implement it line by line. in other case if you copy paste, then you will need to repome some code lines to fix it.
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;
using Unity.Services.Relay;
using Unity.Networking.Transport.Relay;
using Unity.Services.Relay.Models;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine.SceneManagement;
public class LobbyManager : MonoBehaviour
{
public bool isEditor;
public GameObject Canvas;
public GameObject XrRig;
public bool isPaused;
private bool once;
public GameObject testCube;
public string oldHost;
float time;
private const string KEY_RELAY_JOIN_CODE = "RelayJoinCode";
public static LobbyManager Instance { get; private set; }
public const string KEY_PLAYER_NAME = "PlayerName";
public const string KEY_PLAYER_CHARACTER = "Character";
public const string KEY_GAME_MODE = "GameMode";
public event EventHandler OnLeftLobby;
public event EventHandler<LobbyEventArgs> OnJoinedLobby;
public event EventHandler<LobbyEventArgs> OnJoinedLobbyUpdate;
public event EventHandler<LobbyEventArgs> OnKickedFromLobby;
public event EventHandler<LobbyEventArgs> OnLobbyGameModeChanged;
public class LobbyEventArgs : EventArgs
{
public Lobby lobby;
}
public event EventHandler<OnLobbyListChangedEventArgs> OnLobbyListChanged;
public class OnLobbyListChangedEventArgs : EventArgs
{
public List<Lobby> lobbyList;
}
public enum GameMode
{
CaptureTheFlag,
Conquest
}
public enum PlayerCharacter
{
Marine,
Ninja,
Zombie
}
private float heartbeatTimer;
private float lobbyPollTimer;
private float refreshLobbyListTimer = 5f;
private Lobby joinedLobby;
// private Lobby joinedLobbyTwo;
private string playerName;
private bool flag;
private string playerToKickId;
private JoinAllocation joinAllcation;
private string OldRelayJoinCode;
private bool leaveLobbyOnce;
private HostMigrationDataSave dataSaveScript;
public GameObject[] windows;
private void Awake()
{
Instance = this;
// await UnityServices.InitializeAsync();
dataSaveScript = GetComponent<HostMigrationDataSave>();
}
public async void JoinRelay()
{
string relayJoinCode = joinedLobby.Data[KEY_RELAY_JOIN_CODE].Value;
OldRelayJoinCode = joinedLobby.Data[KEY_RELAY_JOIN_CODE].Value;
joinAllcation = await JoinRelay(relayJoinCode);
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(joinAllcation, "dtls"));
NetworkManager.Singleton.StartClient();
flag = true;
oldHost = joinedLobby.HostId;
time = 0.0f;
Debug.Log(" succesfully joined new relay");
}
private void Update()
{
if (SceneManager.GetSceneByBuildIndex(1).IsValid())
{
if (PlayerPrefs.HasKey("playerSpawned"))
{
PlayerPrefs.SetString("playerSpawned", "false");
}
}
//if player tried to exit game completely, he leaves the lobby
if (isPaused)
{
if (joinedLobby != null)
{
if (!leaveLobbyOnce)
{
LeaveLobby();
leaveLobbyOnce = true;
}
}
}
if (joinedLobby != null)
{
if (oldHost != joinedLobby.HostId)
{
if (joinedLobby.HostId == AuthenticationService.Instance.PlayerId)
{
// NetworkManager.Singleton.Shutdown();
CreateNewRelayOnHostDc();
oldHost = joinedLobby.HostId;
Debug.Log("starting new relay");
time = 0.0f;
}
else
{
if (joinedLobby.Data.ContainsKey(KEY_RELAY_JOIN_CODE))
{
if (OldRelayJoinCode != joinedLobby.Data[KEY_RELAY_JOIN_CODE].Value)
{
Debug.Log("joining new relay");
JoinRelay();
}
}
}
dataSaveScript.SaveData();
}
}
//HandleRefreshLobbyList(); // Disabled Auto Refresh for testing with multiple builds
HandleLobbyHeartbeat();
HandleLobbyPolling();
}
public async void Authenticate(string playerName)
{
this.playerName = playerName;
// this.playerName =
InitializationOptions initializationOptions = new InitializationOptions();
initializationOptions.SetProfile(playerName);
await UnityServices.InitializeAsync(initializationOptions);
AuthenticationService.Instance.SignedIn += () =>
{
// do nothing
Debug.Log("Signed in! " + AuthenticationService.Instance.PlayerId);
RefreshLobbyList();
};
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
private void HandleRefreshLobbyList()
{
if (UnityServices.State == ServicesInitializationState.Initialized && AuthenticationService.Instance.IsSignedIn)
{
refreshLobbyListTimer -= Time.deltaTime;
if (refreshLobbyListTimer < 0f)
{
float refreshLobbyListTimerMax = 5f;
refreshLobbyListTimer = refreshLobbyListTimerMax;
RefreshLobbyList();
}
}
}
private async void HandleLobbyHeartbeat()
{
if (IsLobbyHost())
{
heartbeatTimer -= Time.deltaTime;
if (heartbeatTimer < 0f)
{
float heartbeatTimerMax = 15f;
heartbeatTimer = heartbeatTimerMax;
Debug.Log("Heartbeat");
await LobbyService.Instance.SendHeartbeatPingAsync(joinedLobby.Id);
}
}
}
private async void HandleLobbyPolling()
{
if (joinedLobby != null)
{
lobbyPollTimer -= Time.deltaTime;
if (lobbyPollTimer < 0f)
{
float lobbyPollTimerMax = 1.1f;
lobbyPollTimer = lobbyPollTimerMax;
joinedLobby = await LobbyService.Instance.GetLobbyAsync(joinedLobby.Id);
OnJoinedLobbyUpdate?.Invoke(this, new LobbyEventArgs { lobby = joinedLobby });
if (!IsPlayerInLobby())
{
// Player was kicked out of this lobby
Debug.Log("Kicked from Lobby!");
OnKickedFromLobby?.Invoke(this, new LobbyEventArgs { lobby = joinedLobby });
joinedLobby = null;
}
}
}
}
public Lobby GetJoinedLobby()
{
return joinedLobby;
}
public bool IsLobbyHost()
{
return joinedLobby != null && joinedLobby.HostId == AuthenticationService.Instance.PlayerId;
}
private bool IsPlayerInLobby()
{
if (joinedLobby != null && joinedLobby.Players != null)
{
foreach (Player player in joinedLobby.Players)
{
if (player.Id == AuthenticationService.Instance.PlayerId)
{
// This player is in this lobby
playerToKickId = player.Id;
return true;
}
}
}
return false;
}
private Player GetPlayer()
{
return new Player(AuthenticationService.Instance.PlayerId, null, new Dictionary<string, PlayerDataObject> {
{ KEY_PLAYER_NAME, new PlayerDataObject(PlayerDataObject.VisibilityOptions.Public, playerName) },
{ KEY_PLAYER_CHARACTER, new PlayerDataObject(PlayerDataObject.VisibilityOptions.Public, PlayerCharacter.Marine.ToString()) }
});
}
public void ChangeGameMode()
{
if (IsLobbyHost())
{
GameMode gameMode =
Enum.Parse<GameMode>(joinedLobby.Data[KEY_GAME_MODE].Value);
switch (gameMode)
{
default:
case GameMode.CaptureTheFlag:
gameMode = GameMode.Conquest;
break;
case GameMode.Conquest:
gameMode = GameMode.CaptureTheFlag;
break;
}
UpdateLobbyGameMode(gameMode);
}
}
//RELAY ADDED
private async Task<Allocation> AllocateRelay()
{
try
{
Allocation allocation = await RelayService.Instance.CreateAllocationAsync(10);
return allocation;
}
catch (RelayServiceException)
{
Debug.Log("e");
return default;
}
}
private async Task<JoinAllocation> JoinRelay(string joinCode)
{
try
{
JoinAllocation joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
return joinAllocation;
}
catch (RelayServiceException e)
{
Debug.Log(e);
return default;
}
}
private async Task<string> GetRelayJoinCode(Allocation allocation)
{
try
{
string relayJoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
return relayJoinCode;
}
catch (RelayServiceException e)
{
Debug.Log("ggf");
return default;
}
}
public async void CreateNewRelayOnHostDc()
{
if (joinedLobby.HostId == AuthenticationService.Instance.PlayerId)
{
Allocation allocation = await AllocateRelay();
string relayJoinCode = await GetRelayJoinCode(allocation);
await LobbyService.Instance.UpdateLobbyAsync(joinedLobby.Id, new UpdateLobbyOptions
{
Data = new Dictionary<string, DataObject> {
{ KEY_RELAY_JOIN_CODE , new DataObject(DataObject.VisibilityOptions.Member, relayJoinCode) }
}
});
NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(allocation, "dtls"));
NetworkManager.Singleton.StartHost();
// StartGame();
}
}
public async void CreateLobby(string lobbyName, int maxPlayers, bool isPrivate, GameMode gameMode)
{
Player player = GetPlayer();
CreateLobbyOptions options = new CreateLobbyOptions
{
Player = player,
IsPrivate = isPrivate,
Data = new Dictionary<string, DataObject> {
{ KEY_GAME_MODE, new DataObject(DataObject.VisibilityOptions.Public, gameMode.ToString()) }
}
};
Lobby lobby = await LobbyService.Instance.CreateLobbyAsync(lobbyName, maxPlayers, options);
joinedLobby = lobby;
OnJoinedLobby?.Invoke(this, new LobbyEventArgs { lobby = lobby });
Debug.Log("Created Lobby " + lobby.Name);
oldHost = joinedLobby.HostId;
CreateNewRelayOnHostDc();
}
public async void RefreshLobbyList()
{
try
{
QueryLobbiesOptions options = new QueryLobbiesOptions();
options.Count = 25;
// Filter for open lobbies only
options.Filters = new List<QueryFilter> {
new QueryFilter(
field: QueryFilter.FieldOptions.AvailableSlots,
op: QueryFilter.OpOptions.GT,
value: "0")
};
// Order by newest lobbies first
options.Order = new List<QueryOrder> {
new QueryOrder(
asc: false,
field: QueryOrder.FieldOptions.Created)
};
QueryResponse lobbyListQueryResponse = await Lobbies.Instance.QueryLobbiesAsync();
OnLobbyListChanged?.Invoke(this, new OnLobbyListChangedEventArgs { lobbyList = lobbyListQueryResponse.Results });
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
public async void JoinLobbyByCode(string lobbyCode)
{
Player player = GetPlayer();
Lobby lobby = await LobbyService.Instance.JoinLobbyByCodeAsync(lobbyCode, new JoinLobbyByCodeOptions
{
Player = player
});
joinedLobby = lobby;
OnJoinedLobby?.Invoke(this, new LobbyEventArgs { lobby = lobby });
}
public async void JoinLobby(Lobby lobby)
{
Player player = GetPlayer();
joinedLobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobby.Id, new JoinLobbyByIdOptions
{
Player = player
});
JoinRelay();
OnJoinedLobby?.Invoke(this, new LobbyEventArgs { lobby = lobby });
oldHost = joinedLobby.HostId;
}
public async void UpdatePlayerName(string playerName)
{
this.playerName = playerName;
if (joinedLobby != null)
{
try
{
UpdatePlayerOptions options = new UpdatePlayerOptions();
options.Data = new Dictionary<string, PlayerDataObject>() {
{
KEY_PLAYER_NAME, new PlayerDataObject(
visibility: PlayerDataObject.VisibilityOptions.Public,
value: playerName)
}
};
string playerId = AuthenticationService.Instance.PlayerId;
Lobby lobby = await LobbyService.Instance.UpdatePlayerAsync(joinedLobby.Id, playerId, options);
joinedLobby = lobby;
OnJoinedLobbyUpdate?.Invoke(this, new LobbyEventArgs { lobby = joinedLobby });
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
}
public async void UpdatePlayerCharacter(PlayerCharacter playerCharacter)
{
if (joinedLobby != null)
{
try
{
UpdatePlayerOptions options = new UpdatePlayerOptions();
options.Data = new Dictionary<string, PlayerDataObject>() {
{
KEY_PLAYER_CHARACTER, new PlayerDataObject(
visibility: PlayerDataObject.VisibilityOptions.Public,
value: playerCharacter.ToString())
}
};
string playerId = AuthenticationService.Instance.PlayerId;
Lobby lobby = await LobbyService.Instance.UpdatePlayerAsync(joinedLobby.Id, playerId, options);
joinedLobby = lobby;
OnJoinedLobbyUpdate?.Invoke(this, new LobbyEventArgs { lobby = joinedLobby });
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
}
public async void QuickJoinLobby()
{
try
{
QuickJoinLobbyOptions options = new QuickJoinLobbyOptions();
Lobby lobby = await LobbyService.Instance.QuickJoinLobbyAsync(options);
///
// string relayJoinCode = joinedLobby.Data[KEY_RELAY_JOIN_CODE].Value;
// JoinAllocation joinAllcation = await JoinRelay(relayJoinCode);
// NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(joinAllcation, "dtls"));
///
// NetworkManager.Singleton.StartClient();
joinedLobby = lobby;
OnJoinedLobby?.Invoke(this, new LobbyEventArgs { lobby = lobby });
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
public async void LeaveLobby()
{
if (joinedLobby != null)
{
try
{
await LobbyService.Instance.RemovePlayerAsync(joinedLobby.Id, AuthenticationService.Instance.PlayerId);
joinedLobby = null;
OnLeftLobby?.Invoke(this, EventArgs.Empty);
leaveLobbyOnce = false;
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
NetworkManager.Singleton.Shutdown();
oldHost = "0";
if (!SceneManager.GetSceneByBuildIndex(1).IsValid())
{
PlayerPrefs.SetString("playerSpawned", "false");
SceneManager.LoadScene("LobbyTutorial_Done");
}
}
public async void KickPlayer(string playerId)
{
if (IsLobbyHost())
{
try
{
await LobbyService.Instance.RemovePlayerAsync(joinedLobby.Id, playerId);
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
}
public async void UpdateLobbyGameMode(GameMode gameMode)
{
try
{
Debug.Log("UpdateLobbyGameMode " + gameMode);
Lobby lobby = await Lobbies.Instance.UpdateLobbyAsync(joinedLobby.Id, new UpdateLobbyOptions
{
Data = new Dictionary<string, DataObject> {
{ KEY_GAME_MODE, new DataObject(DataObject.VisibilityOptions.Public, gameMode.ToString()) }
}
});
joinedLobby = lobby;
OnLobbyGameModeChanged?.Invoke(this, new LobbyEventArgs { lobby = joinedLobby });
}
catch (LobbyServiceException e)
{
Debug.Log(e);
}
}
public void StartGame()
{
if (joinedLobby.HostId == AuthenticationService.Instance.PlayerId)
{
SceneManager.LoadScene("gameplay");
}
}
void OnApplicationFocus(bool hasFocus)
{
if (!isEditor)
{
isPaused = !hasFocus;
}
}
public void scaleLobbyWindow()
{
if(windows[0].GetComponent<RectTransform>().localScale.y < 1.0f)
{
foreach (GameObject window in windows)
{
window.GetComponent<RectTransform>().localScale = new Vector3(1.0f, 1.0f, 1.0f);
}
}
else
{
foreach (GameObject window in windows)
{
window.GetComponent<RectTransform>().localScale = new Vector3(0.0f, 0.0f, 0.0f);
}
}
}
}
first player creates lobby wirh " public async void CreateLobby(string lobbyName, int maxPlayers, bool isPrivate, GameMode gameMode)"
inside that i call
" CreateNewRelayOnHostDc();" where i create relay connection and start host
when client joins lobby with " public async void JoinLobby(Lobby lobby)"
i cache current lobby host id with “oldHost = joinedLobby.HostId;”
then in Update() i run this code
if (joinedLobby != null)//when host leaves, lobby service automaticaly pick new host and updates hois host id
{
if (oldHost != joinedLobby.HostId) // so if my cahed "oldhost" is not equol to new host id
{
if (joinedLobby.HostId == AuthenticationService.Instance.PlayerId) //if im the host i start new relay
{
// NetworkManager.Singleton.Shutdown();
CreateNewRelayOnHostDc();
oldHost = joinedLobby.HostId;
Debug.Log("starting new relay");
time = 0.0f;
}
else
{
if (joinedLobby.Data.ContainsKey(KEY_RELAY_JOIN_CODE)) // if im client (also i didnt checked if this part works with more than 2 players)
{
if (OldRelayJoinCode != joinedLobby.Data[KEY_RELAY_JOIN_CODE].Value) // check if cached relay is diferent than new relay
{
Debug.Log("joining new relay");
JoinRelay(); // join new relay
}
}
}
dataSaveScript.SaveData(); // inside dataSaveScript i save player prefs. player position, rotation etc... and load after joining new relay
}
}
“JoinRelay();” is called everytime new client joins lobby or when client migrate to new lobby. there i cahche new relay join code everytime, so next time when player migartes he could check if relayjoin code is diferent or same
sorry as i said my code is a mess, im just a hobby person who barely graduated middle school, also i need to check if it work for more than with two players but it should be good start for those who needs it. there exacly 0 examples how to do it, so i guess i will be first one 
i explained how in theory this works in my previous post
this is a game i tried to make host migrate for