Asynced multiple rooms and multiple scenes problem.

Options

Hello,

I am revisiting an issue that I have now extracted into a self contained project so I can test thoroughly. I also based my starting code on the Basic Tutorial code and broke things down from there. I will include the 4 classes below.

My goal is to have players be able to move from scene to scene asynchronously as each scene has a specific player prefab and game mechanic. Example: POV-Humanoid and POV-Car. So players should be able to come and go at will joining each room and scene accordingly. RoomOptions are deployed and a hash with the scene name for a prefix to associate each room with it's appropriate scene.

Theory: 4 parts needed:

1 - PUNLauncher - Similar to Launcher in Basic Tutorial. Get's the player connected.

2 - PUNSceneManager - Handles what prefabs to instantiate, and tracks previous scene name via static property.

3 - PUNScenePortal - Handles Player's request to navigate to new scene (through trigger). Upon TriggerEntered: Disconnect from room, wait for OnConnectedToMaster and then attempt JoinRandomRoom(with roomOptions), if OnJoinRandomFail() then CreateRoom() with same room options.

4 - SceneNavigationDataObject - Contains Scene Name, RoomOptions, and and method to return a hash containing the scene name/key pair.

Implementation:

Launcher and UI needed for entry (same as Tutorial). Each scene will require a PUNSceneManager object and any PUNScenePortal objects to connect between scenes. Currently I have 3 scenes named "SpacePort", "Gallery", "FunGame". Each Scene has 2 portals linking linking to each adjacent scene.

On launching the game and moving to the SpacePort scene, everything works as expected and players can see each other. However when using the portal, the first player, regardless of if he is master client or not throws this nasty little error:

CreateRoom failed. Client is on MasterServer (must be Master Server for matchmaking)but not ready for operations (State: Joining). Wait for callback: OnJoinedLobby or OnConnectedToMaster. UnityEngine.Debug:LogError (object)

I am indeed waiting for the OnConnectToMaster, so I am a bit confused as to the message. Running the game solo with no other instances connected also produces the error. Any idea on what might be causing this? I have included the code from the 4 classes described below:

public class PUNLauncher : MonoBehaviourPunCallbacks
{
    public static event Action<string> OnLogMessage;
    public static event Action OnPlayerHasJoinedRoom;

    #region PUN Fields
    /// <summary>
    /// Keep track of the current process. Since connection is asynchronous and is based on several callbacks from Photon, 
    /// we need to keep track of this to properly adjust the behavior when we receive call back by Photon.
    /// Typically this is used for the OnConnectedToMaster() callback.
    /// </summary>
    private bool _isConnecting;

    [BoxGroup("Network Config")]
    public string gameVersion = "1";

    // Used to sync scene loads from Master Client to other players.
    [BoxGroup("Network Config")]
    public bool automaticallyLoadScene = false;

    [BoxGroup("Network Config")] 
    public bool loadNextSceneOnRandomRoomJoined = true;
    
    [BoxGroup("Scene Navigation")]
    [SerializeField] private SceneNavigationDataObject _sceneNavigationDataObject;

    public bool playerIsInRoom => _playerIsInRoom;
    private bool _playerIsInRoom;
    
    
    #endregion
    
    #region MonoBehaviour CallBacks

    private void Awake()
    {
        // #Critical
        // this makes sure we can use PhotonNetwork.LoadLevel() on the master client and all clients in the same room sync their level automatically. If false,
        // Scene navigation happens independently.  This will be false for Babelverse as we are navigating scenes independently.
        PhotonNetwork.AutomaticallySyncScene = automaticallyLoadScene;
    }

    #endregion
    
    #region Public Methods
    
    /// <summary>
    /// Start the connection process. 
    /// - If already connected, we attempt joining a random room
    /// - if not yet connected, Connect this application instance to Photon Cloud Network
    /// </summary>
    public void Connect(string nickName)
    {
        // Set the NickName of the player to the PhotonNetwork.
        PhotonNetwork.NickName = nickName;
        // keep track of the will to join a room, because when we come back from the game we will get a callback that we are connected, so we need to know what to do then
        _isConnecting = true;

        // Here we can start loader animation for visual effect if you have one...
                
        // we check if we are connected or not, we join if we are , else we initiate the connection to the server.
        if (PhotonNetwork.IsConnected)
        {
            LogFeedback("Joining Room...");
            // #Critical we need at this point to attempt joining a Random Room. If it fails, we'll get notified in OnJoinRandomFailed() and we'll create one.
            var roomOps = _sceneNavigationDataObject.GetRoomOptions();
            PhotonNetwork.JoinRandomRoom(roomOps.CustomRoomProperties, roomOps.MaxPlayers);
        }else{
    
            LogFeedback("Connecting...");
                
            // #Critical, we must first and foremost connect to Photon Online Server.
            PhotonNetwork.ConnectUsingSettings();
            PhotonNetwork.GameVersion = gameVersion;
        }
    }
    
    // Send messages to a debug panel.
    private void LogFeedback(string message)
    {
        OnLogMessage?.Invoke(message);
    }
    
    #endregion
            
    #region MonoBehaviourPunCallbacks CallBacks
    // below, we implement some callbacks of PUN
    // you can find PUN's callbacks in the class MonoBehaviourPunCallbacks


    /// <summary>
    /// Called after the connection to the master is established and authenticated
    /// </summary>
    public override void OnConnectedToMaster()
    {
        // we don't want to do anything if we are not attempting to join a room. 
        // this case where _isConnecting is false is typically when you lost or quit the game, when this level is loaded, OnConnectedToMaster will be called, in that case
        // we don't want to do anything.
        if (_isConnecting)
        {
            LogFeedback("OnConnectedToMaster: Next -> try to Join Random Room");
            Debug.Log("PUN Basics Tutorial/Launcher: OnConnectedToMaster() was called by PUN. Now this client is connected and could join a room.\n Calling: PhotonNetwork.JoinRandomRoom(); Operation will fail if no room found");

            // #Critical: The first we try to do is to join a potential existing room. If there is, good, else, we'll be called back with OnJoinRandomFailed()
            var roomOps = _sceneNavigationDataObject.GetRoomOptions();
            PhotonNetwork.JoinRandomRoom(roomOps.CustomRoomProperties, roomOps.MaxPlayers);
        }
    }

    /// <summary>
    /// Called when a JoinRandom() call failed. The parameter provides ErrorCode and message.
    /// </summary>
    /// <remarks>
    /// Most likely all rooms are full or no rooms are available. <br/>
    /// </remarks>
    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        LogFeedback("OnJoinRandomFailed: Next -> Create a new Room");
        Debug.Log("PUN Basics Tutorial/Launcher:OnJoinRandomFailed() was called by PUN. No random room available, so we create one.\nCalling: PhotonNetwork.CreateRoom");

        // #Critical: we failed to join a random room, maybe none exists or they are all full. No worries, we create a new room.
        PhotonNetwork.CreateRoom(null, _sceneNavigationDataObject.GetRoomOptions());

    }


    /// <summary>
    /// Called after disconnecting from the Photon server.
    /// </summary>
    public override void OnDisconnected(DisconnectCause cause)
    {
        LogFeedback("OnDisconnected "+cause);
        Debug.LogError("PUN Basics Tutorial/Launcher:Disconnected");

        // #Critical: we failed to connect or got disconnected. There is not much we can do. Typically, a UI system should be in place to let the user attemp to connect again.
        // loaderAnime.StopLoaderAnimation();

        _isConnecting = false;
        _playerIsInRoom = false;

    }
    
    /// <summary>
    /// Called when entering a room (by creating or joining it). Called on all clients (including the Master Client).
    /// </summary>
    /// <remarks>
    /// This method is commonly used to instantiate player characters.
    /// If a match has to be started "actively", you can call an [PunRPC](@ref PhotonView.RPC) triggered by a user's button-press or a timer.
    ///
    /// When this is called, you can usually already access the existing players in the room via PhotonNetwork.PlayerList.
    /// Also, all custom properties should be already available as Room.customProperties. Check Room..PlayerCount to find out if
    /// enough players are in the room to start playing.
    /// </remarks>
    public override void OnJoinedRoom()
    {
        LogFeedback("OnJoinedRoom with "+PhotonNetwork.CurrentRoom.PlayerCount+" Player(s)");
        Debug.Log("PUN Basics Tutorial/Launcher: OnJoinedRoom() called by PUN. Now this client is in a room.\nFrom here on, your game would be running.");

        _playerIsInRoom = true;
        
        

        if (loadNextSceneOnRandomRoomJoined)
        {
            PhotonNetwork.LoadLevel(_sceneNavigationDataObject.toScene);
        }
        else
        {
            OnPlayerHasJoinedRoom?.Invoke();
        }
    }

    public override void OnErrorInfo(ErrorInfo errorInfo)
    {
        base.OnErrorInfo(errorInfo);
        OnLogMessage?.Invoke(errorInfo.Info);
    }

    #endregion
}


public class PUNSceneManager : MonoBehaviour
{
    // Instance reference
    public static PUNSceneManager Instance;

    // For tracking the previous scene we were in. This is used to help 
    // resolve the spawn point of the new scene we are entering.
    public static string PreviousSceneName;
    
    #region Private Fields
    [BoxGroup("Scene References and Settings")]
    [Tooltip("The prefab to use for representing the player")]
    [SerializeField]
    private GameObject playerPrefab;

    [BoxGroup("Scene References and Settings")]
    [SerializeField] 
    private Transform _playerStartTransform;

    [BoxGroup("Scene References and Settings")]
    [SerializeField] 
    [Tooltip("Future:: Allow for scene to be played as single player in offline mode.")]
    private bool _allowOfflineMode;

    [BoxGroup("Scene References and Settings")] 
    [SerializeField]
    private List<PUNScenePortal> _punScenePortals;

    #endregion
    
    #region MonoBehaviour CallBacks

    private void Awake()
    {
        if (!playerPrefab) Debug.LogError("PUNSceneManager - playerPrefab is null.");
        if (!_playerStartTransform) Debug.LogError("PUNSceneManager - _playerStartTransform is null.");
        Instance = this;
    }

    private void Start()
    {
        if (!PhotonNetwork.IsConnected && _allowOfflineMode)
        {
            // Allow for offline mode.  For future use, not implemented at this time.
            PhotonNetwork.OfflineMode = true;
            Debug.Log("Offline Mode has been set to true.");
        }
        else if (!PhotonNetwork.IsConnected)
        {
            Debug.Log("There is no connection established to the PhotonNetwork.");
        }
        
        // 1 - See if we can find a matching portal to the previous scene name.
        Transform spawnPoint = _playerStartTransform;
        
        foreach (var portal in _punScenePortals)
        {
            if (portal.toSceneName == PreviousSceneName)
            {
                spawnPoint = portal.portalSpawnTarget;
            }
        }
        
        // Instantiate the player prefab.
        var go = PhotonNetwork.Instantiate(playerPrefab.name, spawnPoint.position, Quaternion.identity, 0);
        // Set the rotation to match the spawnPoint.
        go.transform.rotation = spawnPoint.localRotation;
    }

    #endregion
}


public class PUNScenePortal : MonoBehaviourPunCallbacks
{
    
    [BoxGroup("Scene Navigation")]
    [SerializeField] 
    private SceneNavigationDataObject _sceneNavigationDataObject;
    private bool _isCurrentPortal;
    
    // Getters
    public string toSceneName => _sceneNavigationDataObject.toScene;
    public Transform portalSpawnTarget => _sceneNavigationDataObject.portalSpawnTarget;

    #region Public Methods.
    public void DisconnectFromCurrentRoom()
    {
        PhotonNetwork.LeaveRoom();
    }
    #endregion
    
    
    #region PUN Callbacks
    // As per documentation, we need to wait for OnConnectionToMaster to fire
    // before we move on to attempting to join a new random room.
    public override void OnConnectedToMaster()
    {
        base.OnConnectedToMaster();
        // Get the room options associated with this portal and where we want to go.
        var roomOps = _sceneNavigationDataObject.GetRoomOptions();
        // Prevent other portals in this scene from responding to PUN Callbacks that were not triggered.
        if (!_isCurrentPortal) return;
        // We are interacting with the correct portal, so attempt to join a new random room.
        PhotonNetwork.JoinRandomRoom(roomOps.CustomRoomProperties, roomOps.MaxPlayers);
        Debug.Log("PUNScenePortal.OnConnectedToMaster() Callback");
    }

    public override void OnJoinRandomFailed(short returnCode, string message)
    {
        base.OnJoinRandomFailed(returnCode, message);
        Debug.Log("PUNScenePortal.OnJoinRandomFailed() Callback");
        // Create a new random room with roomOptions.
        PhotonNetwork.CreateRoom(null, _sceneNavigationDataObject.GetRoomOptions());
    }

    public override void OnJoinedRoom()
    {
        Debug.Log("PUNScenePortal.OnJoinedRoom() Callback");
        base.OnJoinedRoom();
        // Clear the flag for safety purposes.
        _isCurrentPortal = false;
        PUNSceneManager.PreviousSceneName = _sceneNavigationDataObject.fromScene;
        // Load the next scene we are using this portal to travel to.
        PhotonNetwork.LoadLevel(_sceneNavigationDataObject.toScene);
    }

    public override void OnErrorInfo(ErrorInfo errorInfo)
    {
        base.OnErrorInfo(errorInfo);
        Debug.LogWarning("PUNScenePortal - Error: " + errorInfo.Info);
    }

    public override void OnCreatedRoom()
    {
        base.OnCreatedRoom();
        Debug.Log("PUNScenePortal.OnOnCreatedRoomJoinedRoom() Callback");
    }
    #endregion
    
    #region Collider Trigger Handlers
    private void OnTriggerEnter(Collider other)
    {
        // Attempt to get the PunManager component from the player.
        var pm = other.GetComponent<CMF_PunManager>();
        if (pm)
        {
            // we have located a player.  Validate local client ownership.
            if (pm.photonView.IsMine)
            {
                // Here is where we could send an event to notify the player if 
                // they want to continue to the next scene/location. For testing purposes,
                // we are just going to go ahead and disconnect.
                
                // Set true for the current portal. This prevents PunCallback execution
                // on other portals in the same scene.
                _isCurrentPortal = true;
                DisconnectFromCurrentRoom();
            }
            
        }
    }
    #endregion
}


[Serializable]
public class SceneNavigationDataObject
{
    // Key constant for scene lookup in hash.
    public const string SCENE_NAME_KEY = "s";

    #region Public Fields
    [Header("HashTable Fields")]
    //[BoxGroup("HashTable Fields")]
    [Tooltip("The scene we will be loading.")]
    [SerializeField] private string _sceneName;
    [SerializeField] private Transform _portalSpawnTarget;

    [Header("RoomOptions Fields")]
    //[BoxGroup("RoomOptions Fields")]
    [SerializeField] private bool _isVisible = true;
    //[BoxGroup("RoomOptions Fields")]
    [SerializeField] private bool _isOpen = true;
    //[BoxGroup("RoomOptions Fields")]
    [SerializeField] private int _maxPlayers = 20;
    //[BoxGroup("RoomOptions Fields")]
    [SerializeField] private bool _publishUserId = true;
    
    [Header("Navigation Fields")]
    //[BoxGroup("Navigation Fields")]
    public string fromScene;
    //[BoxGroup("Navigation Fields")]
    public bool toSceneIsMultiplayer;
    //[BoxGroup("Navigation Fields")]
    public bool useSceneStartPosition;

    public Transform portalSpawnTarget => _portalSpawnTarget;
    
    public string toScene => _sceneName;
    
    #endregion
    private Hashtable GetHashTable()
    {
        var ht = new Hashtable
        {
            { SCENE_NAME_KEY, _sceneName }
        };
        return ht;
    }
    
    public RoomOptions GetRoomOptions()
    {
        var roomOps = new RoomOptions();
        roomOps.IsVisible = _isVisible;
        roomOps.IsOpen = _isOpen;
        roomOps.MaxPlayers = (byte)_maxPlayers;
        roomOps.CustomRoomProperties = GetHashTable();
        roomOps.CustomRoomPropertiesForLobby = new string[] { SCENE_NAME_KEY };
        roomOps.PublishUserId = _publishUserId;
        return roomOps;
    }
}


Comments

  • Tobias
    Options

    CreateRoom failed. Client is on MasterServer (must be Master Server for matchmaking)but not ready for operations (State: Joining).

    Glady, I stumbled over "(State: Joining)". This looks as if some of your code is joining or creating a room and you (still) call join or create another time. In theory, the second call should not change state anymore but maybe you use an older PUN 2 version? Just in case, update.

    If you are sure you don't call join or create multiple times, you should possibly wrap this code up in a repro project and send us a link to some dropbox or similar. Mail to: developer@photonengine.com

  • @Tobias, I figured it out. I have multiple PUNScenePortal(s) in each scene. On a hunch, I disabled all but 1 and the error went away. What I failed to do was block all the PUN callbacks in the class from responding to events unless it is specifically the portal that was triggered by the player. It was a complete oversight on my part. Now it works like a charm and I had a couple of team members on with me moving about each between 3 different scenes/room as expected. PUN2 is very powerful and I am excited to dig further under the hood and see what else I can manage to get it to do for our game scenarios.

    Here is the correctly updated class code:

    public class PUNScenePortal : MonoBehaviourPunCallbacks
    {
        
        [BoxGroup("Scene Navigation")]
        [SerializeField] 
        private SceneNavigationDataObject _sceneNavigationDataObject;
        private bool _isCurrentPortal;
        
        // Getters
        public string toSceneName => _sceneNavigationDataObject.toScene;
        public Transform portalSpawnTarget => _sceneNavigationDataObject.portalSpawnTarget;
    
        #region Public Methods.
        public void DisconnectFromCurrentRoom()
        {
            PhotonNetwork.LeaveRoom();
        }
        #endregion
        
        
        #region PUN Callbacks
        // As per documentation, we need to wait for OnConnectionToMaster to fire
        // before we move on to attempting to join a new random room.
        public override void OnConnectedToMaster()
        {
            // Prevent other portals in this scene from responding to PUN Callbacks that were not triggered.
            // NOTE!!!!  This filter MUST be applied to every callback handler if the scene has more
            // than one PUNScenePortal active in it.
            if (!_isCurrentPortal) return;
            base.OnConnectedToMaster();
            // Get the room options associated with this portal and where we want to go.
            var roomOps = _sceneNavigationDataObject.GetRoomOptions();
            // We are interacting with the correct portal, so attempt to join a new random room.
            PhotonNetwork.JoinRandomRoom(roomOps.CustomRoomProperties, roomOps.MaxPlayers);
            Debug.Log("PUNScenePortal.OnConnectedToMaster() Callback");
        }
    
        public override void OnJoinRandomFailed(short returnCode, string message)
        {
            if (!_isCurrentPortal) return;
            base.OnJoinRandomFailed(returnCode, message);
            Debug.Log("PUNScenePortal.OnJoinRandomFailed() Callback. " + returnCode + " " + message);
            // Create a new random room with roomOptions.
            PhotonNetwork.CreateRoom(null, _sceneNavigationDataObject.GetRoomOptions());
        }
    
        public override void OnJoinedRoom()
        {
            if (!_isCurrentPortal) return;
            Debug.Log("PUNScenePortal.OnJoinedRoom() Callback");
            base.OnJoinedRoom();
            // Clear the flag for safety purposes.
            _isCurrentPortal = false;
            PUNSceneManager.PreviousSceneName = _sceneNavigationDataObject.fromScene;
            // Load the next scene we are using this portal to travel to.
            PhotonNetwork.LoadLevel(_sceneNavigationDataObject.toScene);
        }
    
        public override void OnErrorInfo(ErrorInfo errorInfo)
        {
            if (!_isCurrentPortal) return;
            base.OnErrorInfo(errorInfo);
            Debug.LogWarning("PUNScenePortal - Error: " + errorInfo.Info);
        }
    
        public override void OnCreatedRoom()
        {
            if (!_isCurrentPortal) return;
            base.OnCreatedRoom();
            Debug.Log("PUNScenePortal.OnCreatedRoom() Callback");
        }
        #endregion
        
        #region Collider Trigger Handlers
        private void OnTriggerEnter(Collider other)
        {
            // Attempt to get the PunManager component from the player.
            var pm = other.GetComponent<CMF_PunManager>();
            if (pm)
            {
                // we have located a player.  Validate local client ownership.
                if (pm.photonView.IsMine)
                {
                    // Here is where we could send an event to notify the player if 
                    // they want to continue to the next scene/location. For testing purposes,
                    // we are just going to go ahead and disconnect.
                    
                    // Set true for the current portal. This prevents PunCallback execution
                    // on other portals in the same scene.
                    _isCurrentPortal = true;
                    DisconnectFromCurrentRoom();
                }
                
            }
        }
        #endregion
    }
    

    Thanks for your comment and assistance. I am sure I'll be reaching out again soon.

  • Tobias
    Options

    Glad you could find and fix the issue!

    Happy coding during Christmas time.