How to do Additive scene loading

The whole answer can be found below.

Please note: The Photon forum is closed permanently. After many dedicated years of service we have made the decision to retire our forum and switch to read-only: we've saved the best to last! And we offer you support through these channels:

Try Our
Documentation

Please check if you can find an answer in our extensive documentation on PUN.

Join Us
on Discord

Meet and talk to our staff and the entire Photon-Community via Discord.

Read More on
Stack Overflow

Find more information on Stack Overflow (for Circle members only).

Write Us
an E-Mail

Feel free to send your question directly to our developers.

How to do Additive scene loading

mrstruijk
2021-07-27 10:13:14

Hi all!

Over the last few days I’ve been trying to implement something that allows me to additively load scenes, which should be synchronized over the network. I’ve been hitting the wall for most of that time now. I really can’t seem to get how I can do this. I hope someone can offer some advice.

Why
I want to get additive scene loading because I think there’s great value in having one (or multiple) ‘base’ scenes, which can hold core game logic. I want to be able to load scenes additively on top of those, which might only hold a new level, or the UI.

Here’s the things I’ve tried, and what happened:
I created a GameObject called ManageScenes with a PhotonView attached to it. This had a script ManageScenes.cs, which registered to the attached PhotonView. Additive scene loading was done via a small method chain:
1. public void PUNLoadSceneAdditively(string sceneName)
{
photonView.RPC("LoadSceneAdditively", RpcTarget.All, sceneName);
}

2. [PunRPC] private void LoadSceneAdditively(string sceneName)
{
StartCoroutine(AsyncLoadSceneAdditively(sceneName));
}

3.
private IEnumerator AsyncLoadSceneAdditively(string sceneName)
{
yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
}

I hoped that the RpcTarget.All would make sure the additive levels were loaded individually on each client. However, I must be doing something wrong.

One issue I encountered was that the PhotonView ViewID was not generated automatically, resulting in conflicting ViewIDs. I solved this by creating a Prefab of the ManageScenes, and having that be loaded via PhotonNetwork.Instantiate during early runtime. This also gave me issues, because now multiple of the ManageScenes prefab spawned in the scene. I tried using various forms of DoNotDestroyOnLoad or Static Instance versions, but none seemed to work the way I needed to. For instance, if at some point I didn’t get multiple of these spawned ManageScenes prefabs, I did end up with multiple identical scenes loaded additively…

I had PhotonNetwork.AutomaticallySyncScene = true, is that correct? I didn't think it would matter, since I wasn’t using PhotonNetwork.LoadScene anymore. However, I didn’t really understand that documentation of that call: If the Master Client loads a level directly via Unity's API, PUN will notify the other players after the scene loading completed (using SceneManager.sceneLoaded). Even though it says that PUN deals with it, does that mean I should still register to SceneManager.sceneLoaded? Should then all (non-Master) clients still load those new scenes individually? How does this relate to the PunRPC call?

How should I be fixing this? Should I keep trying to use PunRPC , or should I try the Photon Events? Should I be calling to the PhotonView.IsMine == true, and get that to PhotonNetwork.Instantiate the ManageScenes prefab, and assign it as the Static object? Should that entire GameObject only be Instantiated for the Master Client? Should these calls only be made by the MasterClient, or by all clients?

Without the additive scene loading I can make it work, but I really don’t like it: core logic lives in a DoNotDestroyOnLoad + Static gameobject. I can drop this GameObject into any scene and start the game from whereever, which I suppose is nice. I use PhotonNetwork.LoadScene to load a single scene. All scenes can either hold everything the game needs, or I use DoNotDestroyOnLoad + Static instances, resulting in needing to move through scenes in a predetermined order. I feel that using this current approach is going to clutter up and complicate development later on. It forces me to construct prefabs and scenes in (to me) inefficient ways, and provides all kinds of constraints for using persistent data (such as on Scriptable Objects etc).

It works. However… it just feels blegh.

That might sound stupid, but I’ve been trying to follow my gut a lot more (from suggestions by Uncle Bob from Clean Coding and the guys from Pragmatic Programmer). And seeing this video by Game Dev Guide, really locked down my desire to get these additive scenes done right.

It seems like something fundamentally possible to load scenes additively, and I just can’t seem to wrap my head around why this is not working out, or why this is not a native Photon functionality.

What should I be doing? Do you have additive scene loading working in your game? How did you do this?
Your expertise would be greatly appreciated!

Thank you!

Comments

mrstruijk
2021-07-27 16:57:06

I fixed it! Apparently, it really helped to write it out, because it gave me a few new ideas to try.

The ManageScenes prefab lives in the Resources folder, has it's own PhotonView, and is instantiated via:

public override void OnJoinedRoom()  
{  
    if (PhotonNetwork.IsMasterClient)  
    {  
        PhotonNetwork.InstantiateRoomObject(ManageScenes.name, transform.position, Quaternion.identity, 0);  
       sceneManagement = FindObjectOfType<SceneManagement>();  
    }  
   else  
   {  
       StartCoroutine(FindSceneManagement());  
   }  
}  

The 'InstantiateRoomObject' call allows it to be of the Room, instead of owned by a particular player. This allows switching MasterClient when needed via:

PhotonNetwork.SetMasterClient(PhotonNetwork.LocalPlayer);  

Since it's a networked object, it may be a while for other non-Master clients to actually find the scene manger:

private IEnumerator FindSceneManagement()  
{  
    bool found = false;

    while (found == false)  
    {  
        found = FindObjectOfType<SceneManagement>();  
        yield return null;  
    }

    sceneManagement = FindObjectOfType<SceneManagement>();  
}  

The code below handles scene switching. This first method only runs the RPC request if it is the MasterClient, and simultaneously tells all other clients to load their own scene

public void RPCLoadSceneAddtively(string sceneName)  
{  
    if (!PhotonNetwork.IsMasterClient)  
    {  
        return;  
    }  
    photonView.RPC("LoadMasterLocalScene", RpcTarget.All, sceneName);  
    LoadClientLocalScene(sceneName);  
}  

This effectively is only run by the Master

[PunRPC] public void LoadMasterLocalScene(string sceneName)  
{  
    StartCoroutine(LoadLocalSceneAsync(sceneName));  
}  

Basically the same call, but now only for the non-Master clients

public void LoadClientLocalScene(string sceneName)  
{  
    if (!PhotonNetwork.IsMasterClient)  
    {  
        StartCoroutine(LoadLocalSceneAsync(sceneName));  
    }  
}  

In the Coroutine below the new scenes are loaded. This current setup allows for an int amount of 'base scenes', which can hold core game logic. I imagine 1 more scene to hold all non-core logic & gameobjects. The code checks if this can be loaded, or if I want to swap one of these scenes for another.

If this is the case, currently it loads a new scene prior to unloading the old one. I'll need to figure out if this is actually the way to go.

private IEnumerator LoadLocalSceneAsync(string sceneName)  
{  
    var sceneCount = SceneManager.sceneCount;  
    var latestLoadedScene = SceneManager.GetSceneAt(sceneCount - 1);

    if (sceneCount <= maxLoadedBaseScenes)  
    {  
        yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);  
    }  
    else  
    {  
        yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);  
        yield return SceneManager.UnloadSceneAsync(latestLoadedScene.name);  
    }

    SceneHasBeenLoaded();  
}  

A Static Action (SceneHasBeenLoaded) is called at the end of it, which can be picked up elsewhere.

I hope this is helpful for someone else looking for a similar solution!
Best

Back to top