Search Unity

Question NetCode and SubScenes Question

Discussion in 'NetCode for ECS' started by flyQuixote, Sep 19, 2020.

  1. flyQuixote

    flyQuixote

    Joined:
    Jun 23, 2013
    Posts:
    25
    I recently updated my project to use the new netcode v 0.4.0. It fixed a few bugs I was having but I have to use the 'sub-scene conversion workflow' as the previous scripts have been deprecated. I tried to do a bit of research on this topic and created a sub scene that contained all the networked objects. So far this seems to work great, the scene loads and the ghost authoring component objects on the server are synchronized across the various clients.

    The problem is that when a player disconnects and then reconnects without closing their game, some duplicates of the ghosts that spawn on the server remain as entities. An example of this is a push-able box. The box will have a duplicate created that the player can't interact with but still exists.

    Seeing how this only happens when a player connects and disconnects I think the problem is either with how the sub scenes are loaded/unloaded during disconnect and connect, there is a problem with my disconnect flow, or there is an issue with the configuration of the ghost entities.

    I could write an ad-hoc script to just delete all ghost entities that shouldn't exist after a player connects to the server but this feels like a bad solution to the problem that doesn't actually address the issue with subscenes.

    Here is an example of what happens during runtime
    • Before connecting there are 7 entities with ghost components
    • After connecting there are 8 (one for the new player character added)
    • Upon disconnecting there are still 7
    • When reconnecting there are 15 total ghosts components (every of the 7 ghosts is duplicated)
    Here is an image of what those ghost duplicates look and act like (there should only be one blue and one pink cube).


    I can link code and configuration resources that I'm using now (rather not post so much code when I don't know where the source of the error is coming from). Here is a link to the branch of the repository using this version of netcode. https://github.com/nicholas-maltbie/PropHunt/tree/refactor/NetCode_v0.4.0
     
    adammpolak and Lionious like this.
  2. flyQuixote

    flyQuixote

    Joined:
    Jun 23, 2013
    Posts:
    25
    In an attempt to answer my own question, I did a bit more research on what system is handling removing and spawning ghosts. I realized that the player entities (which have a ghost component) are handled correctly (they are deleted and reloaded corrected when connecting and disconnecting). So I thought to myself, why not try deleting these entities whenever the player disconnects and when the game starts. And it works! here is the system(s) I used.

    Code (CSharp):
    1.  
    2.      /// <summary>
    3.     /// System to clear all ghosts on the client
    4.     /// </summary>
    5.     [UpdateBefore(typeof(ConnectionSystem))]
    6.     [UpdateInGroup(typeof(ClientSimulationSystemGroup))]
    7.     public class ClearClientGhostEntities : SystemBase
    8.     {
    9.         protected EndSimulationEntityCommandBufferSystem commandBufferSystem;
    10.  
    11.         public struct ClientClearGhosts : IComponentData {};
    12.  
    13.         protected override void OnCreate()
    14.         {
    15.             RequireSingletonForUpdate<ClientClearGhosts>();
    16.             this.commandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    17.         }
    18.  
    19.         protected override void OnUpdate()
    20.         {
    21.             var buffer = this.commandBufferSystem.CreateCommandBuffer().AsParallelWriter();
    22.             // Also delete the existing ghost objects
    23.             Entities.ForEach((
    24.                 Entity ent,
    25.                 int entityInQueryIndex,
    26.                 ref GhostComponent ghost) =>
    27.             {
    28.                 buffer.DestroyEntity(entityInQueryIndex, ent);
    29.             }).ScheduleParallel();
    30.             this.commandBufferSystem.CreateCommandBuffer().DestroyEntity(GetSingletonEntity<ClientClearGhosts>());
    31.         }
    32.     }
    33.  
    34.     /// <summary>
    35.     /// System to handle disconnecting client from the server
    36.     /// </summary>
    37.     [UpdateBefore(typeof(GhostReceiveSystem))]
    38.     [UpdateInGroup(typeof(ClientSimulationSystemGroup))]
    39.     public class ConnectionSystem : ComponentSystem
    40.     {
    41.  
    42.         /// <summary>
    43.         /// Has a disconnect been requested
    44.         /// </summary>
    45.         public static bool disconnectRequested;
    46.  
    47.         // Some extra code that doesn't matter...
    48.  
    49.         protected override void OnUpdate()
    50.         {
    51.             if (ConnectionSystem.disconnectRequested)
    52.             {
    53.                 Debug.Log("Attempting to disconnect");
    54.                 Entities.ForEach((Entity ent, ref NetworkStreamConnection conn) =>
    55.                 {
    56.                     EntityManager.AddComponent(ent, typeof(NetworkStreamRequestDisconnect));
    57.                     EntityManager.CreateEntity(ComponentType.ReadOnly(typeof(ClearClientGhostEntities.ClientClearGhosts)));
    58.                 });
    59.                 ConnectionSystem.disconnectRequested = false;
    60.             }
    61.         }
    62.     }
    63.  
    I looked into why this fixed the error and I found the class
    Unity.NetCode.GhostReceiveSystem
    . It has a portion that handles resetting the 'ghost map' of spawned ghosts but will only delete ghosts that were spawned while the game was connected to the server (This is like the player entities) but doesn't remove entities like those that initially spawned. I think the fact that the subscene loads slightly after the client world is created leads to this issue (if I would have to guess).

    My best guess as to why this error occurred is:
    1. When the world initially connects it's able to sync the entities from the sub scene with the server entities.
    2. Once I disconnect by adding a `NetworkStreamRequestDisconnect` component to my `NetworkStreamConnection` entity, then it doesn't know how to handle the entities that spawned as part of the sub system and leaves them in.
    3. Since it disconnected it's missing those entities from the ghost map and spawns them in when I reconnect. Therefore it spawns a copy of all the entities
    4. When I disconnect again, it's able to remove all those duplicate entities since they were spawned while connected to the server but the original entitles are still remaining.
    the roundabout solution I created is to ensure that all entities with the 'GhostComponent' component are deleted after the player disconnects (this handles when the GhostReceive System is not able to delete them). This is somewhat how netcode 0.3.0 worked as they didn't spawn server controlled entities on the client until they connected. I can see how with a sub scene this workflow might be more confusing to manage.

    I'm certain that my solution to the problem is not the indented way of solving the problem and would love any insight or suggestions as to improve this or how I can setup my sub scenes so the server controlled entities only are created when the client joins the server. I can link more code to my repository (it's a public github repo https://github.com/nicholas-maltbie/PropHunt) if you have any questions about what is happening.
     
    bb8_1 likes this.
  3. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    473
    When you place instances of ghosts in a subscene they will be instantiated on both the client and server as pre-spawned ghosts, and then hooked up to synchronize based on current server state. This assumes that you unload and reload the subscene on the client when you reconnect - if you don't you can end up with multiple instances of the ghosts on the client. You should be getting an error or warning if doing it wrong, sounds like you did not and that would be a bug.
     
    bb8_1 likes this.
  4. flyQuixote

    flyQuixote

    Joined:
    Jun 23, 2013
    Posts:
    25

    hey, I tried quite a few solutions and doing a bit of research on this problem but I'm a bit stuck as to how to 'reload' a sub scene. Is there a specific link to the documentation you could help me with? I keep finding outdated material when I do research.
     
  5. Kirkules_

    Kirkules_

    Joined:
    Aug 5, 2014
    Posts:
    65
    I've found a way to Reload a SubScene that I'd like to share. Perhaps there's a better way?

    We'd like this to work?

    Code (CSharp):
    1.  
    2. void LoadSubScene( Entity subSceneEntity, SceneSystem.LoadParameters loadParameters)
    3. {
    4.     sceneSystem.LoadSceneAsync(subSceneEntity, loadParameters);
    5. }
    6.  
    7. void UnloadSubScene( Entity subSceneEntity, SceneSystem.UnloadParameters unloadParameters)
    8. {
    9.      sceneSystem.UnloadScene(subSceneEntity, unloadParameters);
    10. }
    11.  
    Cycling back and forth between loaded and unloaded state, destroying all the entities spawned from the scene.

    But it doesn't work.

    This unload method works.

    Code (CSharp):
    1.  
    2. void UnloadSubScene( Entity subSceneEntity, Hash128 subSceneGUID, SceneSystem.UnloadParameters unloadParameters)
    3. {
    4.     // remove loaded scene artifacts that aren't removed with SceneSystem.UnloadScene, which then allows SceneSystem.LoadSceneAsync to function properly
    5.  
    6.    EntityManager.RemoveComponent<RequestSceneLoaded>(subSceneEntity);
    7.  
    8.    foreach (var s in EntityManager.GetBuffer<ResolvedSectionEntity>(subSceneEntity))
    9.    {
    10.       EntityManager.RemoveComponent<RequestSceneLoaded>(s.SectionEntity);
    11.    }
    12. }
    13.  
    After calling the UnloadSubScene method above, the SceneSystem.LoadSceneAsync method works.

    Is there a better way?
     
    Last edited: Jun 18, 2021
  6. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    894
    To reload a subscene you must first unloading it and the loading it again.
    You can use both the sync method SceneSystem.UnloadScene or just remove the RequestSceneLoaded the component from the scene sections. Unless there is a bug in the Entities 0.17 the UnloadScene does exactly the same things (it cycle through the resolved sections and destroy all of them).

    Reload the subscenes with prespawn requires also a bunch of changes in NetCode 0.6-preview-7 to make it works as expected.
    In particular there are some entities that are not destroyed and that are instantiated by the PopulatePrespawnGhostsSystem, and another issue in the ghost receive system (that now I forgot about).
     
  7. optimise

    optimise

    Joined:
    Jan 22, 2014
    Posts:
    2,129
    @CMarastoni Can u provide unofficial fix for NetCode 0.6-preview-7? Btw does it fix at Netcode 0.7?
     
  8. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    894
    I will check what I can do in that regards.
     
    optimise likes this.
  9. Kirkules_

    Kirkules_

    Joined:
    Aug 5, 2014
    Posts:
    65
    @CMarastoni It must be a bug then because Load/Unload/Load wasn't working for me. Code aborts in the package before adding the component during the (Re)Load command (2nd time it's called). I wasn't even working in NetCode, I was just trying to learn the SubScene load/unload workflow.

    I had everything working, build systems working for, Unity.Physics, Client/ClientServer/Server builds and imported the latest NetCode 0.6.0 and then I couldn't build anymore because of the issues with Collections required by NetCode. So I'm using MLAPI for now.
     
    Last edited: Jun 28, 2021
  10. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    894
    Hmm.. that is curious. Indeed I didn't try with entities 0.17 in while but looks a little strange to me.
    When you remove the component, the scene is unloaded by the SceneStreamingSystem in the next frame (so it is sort of asyncronous). Calling right after the LoadAsync, will just re-add the components but nothing will happen. So I'm assuming you are waiting at least 1 frame before calling the LoadSceneAsync again.

    Also, depending on what LoadParameters you pass the behaviour can be quite different. In particular if you set the
    DestroySectionProxyEntities or DontRemoveRequestSceneLoaded (so you know what are you doing).
    But using the default (so flags = 0) should do the exact same thing.

    So my next question then is: what you mean with "It didn't work" ?. Just to clarify exactly what the problem is :)

    If we are speaking about prespawned ghosts, unload a scene by removing the component is not sufficient either. You also need to remove another components added to the scene entity. Or invoke the unload scene with
    DestroySceneProxyEntity flag set.