Intended way to create client/server worlds outside of Bootstrapper.Initialize()

I am making a multiplayer game with dedicated servers and need to be able to test locally. Right now I can make my server / client builds, deploy my server to PlayFab, and connect to it with my client. However local testing is simply not working. It’s like there’s something special about calling CreateDefaultClientServerWorlds() from inside of your Bootstrapper.Initialize() as opposed to calling it anywhere else.

Right now my bootstrapper is the following:

    public override bool Initialize(string defaultWorldName)
    {
        DefaultWorldName = defaultWorldName;
        DefaultWorld = CreateDefaultWorld(DefaultWorldName);
        World.DefaultGameObjectInjectionWorld = DefaultWorld;
#if UNITY_SERVER
        StartServer();
        return true;
#endif
        if (RequestedPlayType == PlayType.ClientAndServer)
        {
            AutoConnectPort = 7979;
            CreateDefaultClientServerWorlds(DefaultWorld);
            return true;
        }
#if !UNITY_SERVER
        StartClient();
#endif
        return true;
    }

This works relatively well. Like I said I can deploy to PlayFab and do local testing, but in order to do local testing I have to change the RequestedPlayType to ClientAndServer and completely bypass my PlayFab login backend. I’ve tried calling ClientServerBootstrap.CreateDefaultClientServerWorlds() from my login code, so that once the player tries to join “remote” server it instead creates the client/server worlds and connects the player to the localhost. Instead this creates an empty client and server world with no prefabs, just a networkConnection entity. What am I doing wrong? How do I properly delay the creation of the client/server worlds? Why is the behavior of “CreateDefaultClientServerWorlds(DefaultWorld);” different when called in the Initialize() method vs by a MonoBehaviour later?

In case my post didn’t make any sense I recorded a short video explaining my problem:
WARNING: TURN DOWN THE AUDIO. I don’t know how to change levels in obs.

I haven’t watched your video but I have no issue creating client/server worlds from outside the auto bootstrap. I don’t call
CreateDefaultClientServerWorlds instead I just call the create client/server world methods directly

ClientServerBootstrap.CreateClientWorld
ClientServerBootstrap.CreateServerWorld

But I don’t see why this would be any different.

Are you using subscenes? Subscenes only auto load during OnEnable. If you create your worlds after OnEnable() you need to manually load your subscenes into your client/server worlds.

This is probably my problem. How do I manually load a subscene like you describe?

The easiest (and best) way to achieve all of that it is to create the worlds in a “init” or setup scene and the just load the game scene from there using a class SceneManager.LoadSceneAsync();
that would load the game world and all the subscenes as usual.

In case you create the world when the game scene is already loaded you need to load the subscene in each world respectively using the SceneSystem.LoadSceneAsync. This would require the sub-scene guid that can be easily retrieved in different ways. For a game targeting hybrid (so not DOTS runtime) you can use for example the SubScene.AllSubScenes and get the guid from there.

So like, have a scene called ClientWorldInit with the subscene in it and a script that runs ClientServerBootstrap.CreateClientWorld() inside of Awake()? Is that what you mean by this?

How do I load a subscene “into a world” using SceneSystem.LoadSceneAsync()? Does invoking that method simply load the scene “into” all active worlds? Can I target a world I’d like to load a subscene into?

I may have misinterpreted this. Is this meant to mean that I start in an init scene and then load the game scene afterwords?

Yes, this solution worked. I now have a scene with a single monobehaviour in it which, in Awake() calls CreateClientWorld() and (depending on whether this is a test build) CreateServerWorld(), then calls LoadSceneAsync(“Game scene”). Everything loads and works as expected. Thank you CMarastoni and tertle!

Thanks for sharing this info with using OnEnable(). Worked for me. Before I tried creating client and server world in the Start() method of a MonoBehavior but the subscene entities were missing then in the just created worlds. It took me half a day to find your post stating the solution. Sadly when you leave the default patterns described in the ECS documentation (like ICustomBootstrap) you have a hard time figuring out the solutions for very simple problems. Also ECS has a shallow learning curve in my opinion. I really like the data-driven approach of ECS but when solving the next issues also took that long I will abandon it for sure.

As CMarastoni proposed I switched to this solution just to get a bit more flexibility. Loading a subscene into a world looks like this (ECS 1.0.0-pre.65):

      World world = ...
      // BlockOnStreamIn: Disable asynchronous streaming, SubScene section will be fully loaded during the next update of the streaming system
      SceneSystem.LoadParameters loadParameters = new SceneSystem.LoadParameters() { Flags = SceneLoadFlags.BlockOnStreamIn };
    
      var sceneEntity = SceneSystem.LoadSceneAsync(world.Unmanaged, new Unity.Entities.Hash128("e2eb80bdad29f9043b530ce784fa261a"), loadParameters);
    
      while (!SceneSystem.IsSceneLoaded(world.Unmanaged, sceneEntity))
      {
        world.Update(); // Important for loading the scene into the world      
      }

There are various ways to get the GUID of the subscene. I simply copied the GUID from the Unity Inspector:

8922218--1222304--upload_2023-4-2_19-29-4.png

1 Like

Another way getting the GUID/hash of a sub scene CMarastoni proposed was using SubScene.AllSubScenes. This enables getting the sub scene hash at runtime. Beware that the subscene has to be loaded then before executing the code. If used in an Awake method of a MonoBehavior NO sub scene is loaded, but in the Start method of MonoBehavior this has already be done. So executed it in Start method or later:

    private Unity.Entities.Hash128 DetermineFirstSubSceneHash()
    {
      var loadedSceneCount = UnityEngine.SceneManagement.SceneManager.loadedSceneCount;

      for (var i = 0; i < loadedSceneCount; i++)
      {
        var scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i);

        if (scene.isSubScene)
        {
          // Use the name of the sub scene to find the loaded sub scene using the scene from the SceneManager
          var subScene = Unity.Scenes.SubScene.AllSubScenes.SingleOrDefault(subScene => subScene.SceneName == scene.name);

          if (subScene != null)
          {
            return subScene.SceneGUID;
          }
        }
      }

      throw new GameException("Cannot determine sub scene. No sub scene currently loaded. Ensure having the sub scene opened while in editor mode.");
    }
1 Like