does anyone figured out how to implement host migration on relay?

im using unity relay and lobby services, i know host migration is not officialy supported for a fact, but could be realy… lol…

tried to look for a while but seems i cant find anyone who actualy did host migration. i found few theoretical solutions but have zero knowledge how to implement it.

my idea is to detect when host leaves, then when if host offline, pick first client in lobby, asign new ownership of the lobby, start new relay, then invide everyone from lobby to new relay. then just sync every spawned object and positions from previous relay few problems, i cant figure out how to change lobby ownership from client side when original host disconects, neither how to tell clients to join to new relay.

if anyone have experience with that, could you share code and solution :slight_smile: thank you

Looking for the same solution

actualy i just managed to implement host migration my self… aparently the looby it self detects when host leaves and assign new host,unless game crashes or force quit, host need to leave trough void Leave() function with proper code, what i did is every time client joins he caches current host id, when cached host is diferent than current host id, he looks for new relay join code and joins new relay, same with new lobby host , when he detects that he is new host, he will create new relay server and updates join code to the lobby.

for game state synchronization i just save position, rotation, health, all data i need to player pref when client detects that cashed host id != current host id, and load it when player join new relay, after that i just clear those specific player prefs, also in case someone crashes or ocasional fail in creating/joining new relay, i make sure that those player prefs is also cleared every time player is in lobby/log in scene

problem, i only managed to test this with 2 players so far, im working on vr game and i have only my laptop and one vr headset to test, im hoping i will get one old vr headset from my friend soon, if it will manage to handle my game, or run at all

please if you will manage to implement host migration you self tell me, also i could share some example code i have. its little bit a mess code, but it works

the [lobby itself] detects when host leaves and assign new host, unless game crashes or force quit, host need to leave trough void Leave() function with proper code, what i did is every time client joins he caches current host id, when cached host is [different] than current host id, he looks for new relay join code and joins new relay, same with new lobby host , when he detects that he is new host, he will create new relay server and updates join code to the lobby.

This is the recommended solution. The new host creates a new Relay session and join code, and posts it to the lobby. Existing members reconnect.

How you handle game state re-sync is where it becomes very title specific. The answer above looks like a good start. The main issue is avoiding having any important state that is exclusively known to the host – because that will be permanently lost in the event of a host disconnection. You can replicate more of this state to players the normal way (RPCs, NetworkVariables), or send it to the side in case you need it.

An external storage mechanism like Cloud Save might be useful in that regard. Check out this page to get an idea how state data periodically uploaded by the host could be readable by the new host: Cloud Save Player Data

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 :wink:

i explained how in theory this works in my previous post

this is a game i tried to make host migrate for

i posted my code as example for host migration, you can look in to it

oh for game state synchronization. yah it might be tricky, well i use client authoritative movement, so every client can just place them selves in positions where they were before, thats not wery safe thing to do if you dont want cheaters to teleport or use speed hack. what i did i save everything to player prefs as i said before and load ot on recconect. if client movements is controled by the host, then its a tricky one, i guess clients should save everything to player pref, then with clientRPC or serverRPC send data to the host, so the host would sync new position and rotation.

ObjectNet multiplayer system support host migration out of the box for steam

Do you know what to do in this scenario? I’m noticing that in this case the lobby does not detect the host leave and that means no automatic host-migration occurs on the lobby. Any help would be greatly appreciated!

yah i have no idea, actually i dropped this system and using distributed authority for netcode, this way there are no dedicated host player and no host migration needed… and to protect for cheating im planing to use anticheat tool from assets store. anticheat pro
there is also free version, but i got pro for my self.