Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. The 2022.2 beta is now available for testing. To find out what's new, have a look at our 2022.2 feature highlights.
    Dismiss Notice
  3. We are updating our Terms of Service for all Unity subscription plans, effective October 13, 2022, to create a more streamlined, user-friendly set of terms. Please review them here: unity.com/legal/terms-of-service.
    Dismiss Notice
  4. Have a look at our Games Focus blog post series which will show what Unity is doing for all game developers – now, next year, and in the future.
    Dismiss Notice
Dismiss Notice
Submit bug reports tagged with #Beta2022Win_Unity when you encounter unknown issues while testing the 2022.2 betas for a chance to win a year of Unity Pro. For more information, have a look at our Beta Sweepstakes Announcement.

DOTS Multiplayer discussion

Discussion in 'DOTS NetCode' started by PhilSA, Jun 14, 2019.

Thread Status:
Not open for further replies.
  1. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    443
    Yes, we are planning to support ghosting of enable/disable components, I do not know the exact timing of when it will land yet.
     
    optimise likes this.
  2. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    443
    We have not done a proper investigation of that yet, so I can't really give any recommendation that I know will work. My best guess of what you need to get basic simulation working with prediction would be: move the physics systems to the ghost prediction system group, make the physics system read the correct time (should be the prediction time, not frame time), add ghosting for PhysicsVelocity so it can be rolled back, mark predicted ghosts with physics as kinematic or not based on ShouldPredict (possibly using PhysicsMassOverride).
    I don't know if that is enough, but all of those should be things you need to do. I expect that there are lots of details you need to figure out in order to make that work.
     
    friflo, Samsle, Ashkan_gc and 3 others like this.
  3. anihilnine

    anihilnine

    Joined:
    Jan 29, 2016
    Posts:
    27
    Hi Tim this quote of yours was from January ... I am trying to work out if this has been implemented yet? My testing indicates that unchanged ghosts are still sent.

    Edit: Did more testing. It looks like there 3-4 bytes per ghost that are sent always. Then additional bytes are also sent per ghost if values changed. It seems to only send fields that have changed, not the entire component or entire ghost.

    Edit2: Did more testing. I need to send a coordinate X value across the wire. I assumed by casting it to an int first that would cause it to be sent less often - however the cast actually increases my instance avg size by 2 bytes.
     
    Last edited: Nov 3, 2020
    adammpolak likes this.
  4. anihilnine

    anihilnine

    Joined:
    Jan 29, 2016
    Posts:
    27
    Went through the code. Looks like if nothing in the chunk changed, nothing is sent for that chunk at all and it skips the next section.

    Otherwise if something in the chunk changed it loops through each component and works out which fields are changed. First it sends the changeMask (some bytes which say which fields have changed) then the actual values for those fields.

    When it works out if a field "changed" it means "changed compared to the baseline" which perhaps means the "last acknowledged state". Clients dont always get packets so just because it is unchanged on the server doesn't mean that we can skip updating it.
     
  5. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    443
    Not sending unchanged ghosts was added in 0.3, but only for ghosts using static optimization (which will make them compress less aggessively when they are changing).

    Not sending ghosts which are not visible is also possible since 0.3, see GhostSendSystem.GhostRelevancyMode and GhostSendSystem.GhostRelevancySet
     
  6. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson What is the best way to access a specific child GameObject in DOTS netcode running in ScheduleParallel()?

    I have a ghost Player GameObject with 4 children.
    upload_2020-11-7_11-12-6.png
    I am trying to spawn the bullet prefab at the Bullet Spawn location.

    Below is the modification to SteeringSystem.cs from Asteroids sample where I use GetBufferFromEntity<Child> on the Player entities to access the converted child GameObjects and then check for a Bullet Spawn Tag.

    Code (CSharp):
    1.         Entities
    2.         .WithReadOnly(inputFromEntity)
    3.         .WithAll<PlayerTag, PlayerCommand>()
    4.         .ForEach((Entity entity, int nativeThreadIndex, ref Translation position,
    5.         ref Rotation rotation, ref PhysicsVelocity physicsVelocity, ref PlayerStateComponent state,
    6.         in GhostOwnerComponent ghostOwner, in PredictedGhostComponent prediction) =>
    7.         {
    8.             if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
    9.                 return;
    10.  
    11.             var input = inputFromEntity[entity];
    12.             PlayerCommand inputData;
    13.             if (!input.GetDataAtTick(currentTick, out inputData))
    14.                 inputData.shoot = 0;
    15.  
    16.             var canShoot = state.WeaponCooldown == 0 || SequenceHelpers.IsNewer(currentTick, state.WeaponCooldown);
    17.             if (inputData.shoot != 0 && canShoot)
    18.             {
    19.                 if (bulletPrefab != Entity.Null)
    20.                 {
    21.                     // This gets the children array from the player object
    22.                     var childFromEntity = GetBufferFromEntity<Child>();
    23.                     var children = childFromEntity[entity];
    24.  
    25.                     // We will use this for the Bullet Spawn object to get the instatiation position.
    26.                     var localToWorldFromEntity = GetComponentDataFromEntity<LocalToWorld>();
    27.  
    28.                     // We create the bullet here
    29.                     var e = commandBuffer.Instantiate(nativeThreadIndex, bulletPrefab);
    30.  
    31.                     for (int i = 0; i < children.Length; ++i)
    32.                     {
    33.                         if(HasComponent<BulletSpawnTag>(children[i].Value))
    34.                         {
    35.                             var bulletSpawnPosition = localToWorldFromEntity[children[i].Value].Position;
    36.                             var newPosition = new Translation {Value = bulletSpawnPosition};
    37.                             commandBuffer.SetComponent(nativeThreadIndex, e, newPosition);
    38.                         }
    39.                     }
    I receive this error:
    InvalidOperationException: <>c__DisplayClass_OnUpdate_LambdaJob0.JobData._BufferFromEntity_Child_0 is not declared [ReadOnly] in a IJobParallelFor job. The container does not support parallel writing. Please use a more suitable container type.


    How can I access the child GameObjects and still run ScheduleParallel(), is there a "recommended" DOTS netcode approach?
     
    olenka_moetsi likes this.
  7. WAYN_Games

    WAYN_Games

    Joined:
    Mar 16, 2019
    Posts:
    817
    Instead of looking for it in the children, why not store a reference to the Bullet spawn entity on the player entity ? You then should be able to get the location of the Bullet spawn entity.

    Or you could even get a delta between the player entity and the Bullet spawn entity at conversion and use that as offset when spawning your bullet avoiding all the lookup costs.
     
    olenka_moetsi likes this.
  8. kanesteven

    kanesteven

    Joined:
    Aug 30, 2018
    Posts:
    34
    @adammpolak I believe it is considered a better practice ( and I do this extensively in a large project ) to create your ComponentDataFromEntity outside the job and then use it to read/write from specific entities from within the job. When I look at your code, I see you doing no writing to any child components ( which makes sense given what you are trying to do ) so I would suggest the following organization:

    Code (CSharp):
    1. // declare your Child component access to be "read-only"
    2. var childFromEntity = GetComponentDataFromEntity<Child>(isReadonly: true);
    3.  
    4. Entities
    5. .ForEach((Entity entity, ...) => {
    6.   var child = childFromEntity[entity];
    7.  
    8.   // use the child data however you see fit
    9. })
    10. .WithReadOnly(childFromEntity) // declare the captured local variable read-only
    11. .WithBurst()
    12. .ScheduleParallel();
    Hope this helps. If you DID actually want to write to the child entity and you could guarantee that your job would write to one and only one unique child then you can use the
    WithNativeDisableParallelForRestriction(childFromEntity)
    in Entities... fluent API to declare to the job that you can safely write to this component from a parallel job.
     
    olenka_moetsi likes this.
  9. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    Super appreciate the help!

    I am probably misunderstanding what you mean but when I add an authoring component to the Player GameObject to store a reference to the Bullet Spawn GameObject my player spawns "funny". On the right is the list of components on my root player object. At the bottom you will see "Bullet Spawn Object Authoring" which is storing a reference to the bullet spawn.
    upload_2020-11-8_14-41-29.png
    ArgumentException: BulletSpawnObject contains a field of UnityEngine.GameObject, which is neither primitive nor blittable.


    Normal player spawn (notice I commented out the Bullet Spawn Object fields):
    upload_2020-11-8_14-40-3.png

    Regarding "get a delta between the player entity and the Bullet spawn entity at conversion", that sounds great as I am hoping to support a thousand simultaneous players and any compute savings would be ideal. How would I go about finding the difference during conversion?
     
    olenka_moetsi likes this.
  10. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven thank you so much! That did it! Able to steer and do bullet instatiation on .ScheduleParallel()
    Really appreciate it!


    (for anyone looking for the answer to the same question in this forum here is the resulting OnUpdate()):
    Code (CSharp):
    1.  
    2.         var level = GetSingleton<LevelComponent>();
    3.         var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
    4.         var deltaTime = Time.DeltaTime;
    5.         var displacement = 100.0f;
    6.         var playerForce = level.playerForce;
    7.         var bulletVelocity = level.bulletVelocity;
    8.         var bulletPrefab = m_BulletPrefab;
    9.         var currentTick = m_PredictionGroup.PredictingTick;
    10.         var inputFromEntity = GetBufferFromEntity<PlayerCommand>(true);
    11.         var childFromEntity = GetBufferFromEntity<Child>(true);
    12.         var localToWorldFromEntity = GetComponentDataFromEntity<LocalToWorld>(true);
    13.  
    14.  
    15.         Entities
    16.         .WithReadOnly(inputFromEntity)
    17.         .WithReadOnly(childFromEntity)
    18.         .WithReadOnly(localToWorldFromEntity)
    19.         .WithAll<PlayerTag, PlayerCommand>()
    20.         .ForEach((Entity entity, int nativeThreadIndex, ref Translation position,
    21.         ref Rotation rotation, ref PhysicsVelocity physicsVelocity, ref PlayerStateComponent state,
    22.         in GhostOwnerComponent ghostOwner, in PredictedGhostComponent prediction) =>
    23.         {
    24.  
    25.             if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
    26.                 return;
    27.             var input = inputFromEntity[entity];
    28.             PlayerCommand inputData;
    29.             if (!input.GetDataAtTick(currentTick, out inputData))
    30.                 inputData.shoot = 0;
    31.  
    32.             state.State = inputData.thrust;
    33.             if (inputData.left == 1)
    34.             {
    35.                 rotation.Value = math.mul(rotation.Value,
    36.                 quaternion.RotateY(math.radians(-displacement * deltaTime)));
    37.             }
    38.  
    39.             if (inputData.right == 1)
    40.             {
    41.                 rotation.Value = math.mul(rotation.Value,
    42.                     quaternion.RotateY(math.radians(displacement * deltaTime)));
    43.             }
    44.  
    45.             if (inputData.thrust == 1)
    46.             {
    47.                 // find the rotation
    48.                 // that direction is the addition of velocity
    49.                 physicsVelocity.Linear += math.mul(rotation.Value, new float3(0,0,1)).xyz;
    50.             }
    51.             var canShoot = state.WeaponCooldown == 0 || SequenceHelpers.IsNewer(currentTick, state.WeaponCooldown);
    52.             if (inputData.shoot != 0 && canShoot)
    53.             {
    54.  
    55.                 if (bulletPrefab != Entity.Null)
    56.                 {
    57.                     // This gets the children array from the player object
    58.                     var children = childFromEntity[entity];
    59.  
    60.                     // We create the bullet here
    61.                     var e = commandBuffer.Instantiate(nativeThreadIndex, bulletPrefab);
    62.  
    63.                     for (int i = 0; i < children.Length; ++i)
    64.                     {
    65.                         // we check if the child entity is the bullet spawn entity
    66.                         if(HasComponent<BulletSpawnTag>(children[i].Value))
    67.                         {
    68.                             // we grab the entity's local to world to use for instantiation location
    69.                             var bulletSpawnGameObjectPosition = localToWorldFromEntity[children[i].Value].Position;
    70.                             var newPosition = new Translation {Value = bulletSpawnGameObjectPosition};
    71.                             commandBuffer.SetComponent(nativeThreadIndex, e, newPosition);
    72.                         }
    73.                     }
    74.  
    75.                     // bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
    76.                     // adding to the players physics Velocity makes sure that it takes into account the already existing player velocity (so if shoot backwards while moving forwards it stays in place)
    77.                     var vel = new PhysicsVelocity {Linear = (bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + physicsVelocity.Linear};
    78.  
    79.                     commandBuffer.SetComponent(nativeThreadIndex, e, rotation);
    80.                     commandBuffer.SetComponent(nativeThreadIndex, e, vel);
    81.                     commandBuffer.SetComponent(nativeThreadIndex, e,
    82.                         new GhostOwnerComponent {NetworkId = ghostOwner.NetworkId});
    83.                 }
    84.  
    85.                 state.WeaponCooldown = currentTick + k_CoolDownTicksCount;
    86.             }
    87.         }).ScheduleParallel();
    88.         m_BeginSimEcb.AddJobHandleForProducer(Dependency);
     
    Last edited: Nov 8, 2020
    olenka_moetsi likes this.
  11. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson

    Is there a way to make a child Game Object only exist on the predicted client?
    Similar to how
    [GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
    causes those components to only appear on the client-spawned (predicted) prefabs.


    Above is my player GameObject with a child Camera GameObject.
    Currently, the camera switches to whatever player was last spawned. I believe this is because that player has a camera component and so Unity chooses the last instantiated camera.
     
    olenka_moetsi likes this.
  12. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,366
    Your camera should probably not be attached to your player prefab anyway.
     
    olenka_moetsi likes this.
  13. WAYN_Games

    WAYN_Games

    Joined:
    Mar 16, 2019
    Posts:
    817
    Are you storing a reference to the Bullet spawn game object or entity ? You need to use the declare prefab reference interface in your authoring component to get the entity of the Bullet spawn game object.
    For the delta, I'm not good at this king of math but I assume in you case storing the local to parent from the Bullet spawn entity Would be enough. If it were not the direct parent you would need to compute the delta based on the 2 local to world matrices of the player and bullets pawn.
     
    olenka_moetsi likes this.
  14. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @tertle any suggestions?
     
    olenka_moetsi likes this.
  15. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    Thanks for the tip! How do you declare the prefab reference interface in the authoring component? Here is what I have:

    Code (CSharp):
    1. using Unity.Entities;
    2. using UnityEngine;
    3. using Unity.Collections;
    4.  
    5. [GenerateAuthoringComponent]
    6. public struct BulletSpawnObject : IComponentData
    7. {
    8.     public GameObject bulletSpawnObject;
    9. }
    10.  
     
    olenka_moetsi likes this.
  16. WAYN_Games

    WAYN_Games

    Joined:
    Mar 16, 2019
    Posts:
    817
    I'm not sure GenerateAuthoringComponent supports GameObject/Entity Reference. First you can try that :

    Code (CSharp):
    1. using Unity.Entities;
    2. using UnityEngine;
    3. using Unity.Collections;
    4.  
    5. [GenerateAuthoringComponent]
    6. public struct BulletSpawnObject : IComponentData
    7. {
    8.     public Entity bulletSpawnObject;
    9. }
    10.  
    If it does not work you should make your own authoring component I think.

    Relevent documentation can be found here :
    https://docs.unity3d.com/Packages/c...Unity.Entities.IDeclareReferencedPrefabs.html
    https://docs.unity3d.com/Packages/com.unity.entities@0.16/api/Global Namespace.GameObjectConversionSystem.html#Global_Namespace_GameObjectConversionSystem_GetPrimaryEntity_UnityEngine_Object_
    https://docs.unity3d.com/Packages/c...nity.Entities.IConvertGameObjectToEntity.html

    Somehting like that (not tested, written in form code block directly :p ):

    Code (CSharp):
    1. using Unity.Entities;
    2. using UnityEngine;
    3. using Unity.Collections;
    4.  
    5. public class BulletSpawnObjectAuthoring : Monobehavior ,IConvertGameObjectToEntity, IDeclareReferencedPrefabs
    6. {
    7.     public GameObject BulletSpawnGO;
    8.  
    9.     void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs){
    10.         referencedPrefabs.Add(BulletSpawnGO);
    11.     }
    12.  
    13.     void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem){
    14.               Entity bulletSpawnEntity= GetPrimaryEntity(BulletSpawnGO);
    15.                dstManager.AddComponentData(entity, new BulletSpawnObject (){bulletSpawnObject=bulletSpawnEntity});                
    16.     }
    17. }
    18.  
    19. }
    20. public struct BulletSpawnObject : IComponentData
    21. {
    22.     public Entity bulletSpawnObject;
    23. }
    24.  
     
    Last edited: Nov 9, 2020
    olenka_moetsi likes this.
  17. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    Ahhhhhhhh, I see what you mean now. Thank you @WAYN_Games! I will try this out :)
     
    olenka_moetsi likes this.
  18. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson I am trying to implement collision triggers by following the example in UnityPhysicsSamples

    (for context it is the one where balls pass through volumes and the collision triggers changing the material of the ball)
    upload_2020-11-11_12-52-47.png

    When a bullet passes through an Asteroid it should raise a trigger event where:
    - On entering it turns the Asteroid red
    - On exit we destroy the Asteroid entity

    I have set up my Asteroid and Bullet matching the set up as the Physics demo.
    upload_2020-11-11_12-54-5.png

    Code for system (slightly modified from demo sample)
    Code (CSharp):
    1.  
    2. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    3. [UpdateAfter(typeof(TriggerEventConversionSystem))]
    4. public class TriggerVolumeChangeMaterialSystem : SystemBase
    5. {
    6.     private EndFixedStepSimulationEntityCommandBufferSystem m_CommandBufferSystem;
    7.     private TriggerEventConversionSystem m_TriggerSystem;
    8.     private EntityQueryMask m_NonTriggerMask;
    9.  
    10.     protected override void OnCreate()
    11.     {
    12.         m_CommandBufferSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
    13.         m_TriggerSystem = World.GetOrCreateSystem<TriggerEventConversionSystem>();
    14.         m_NonTriggerMask = EntityManager.GetEntityQueryMask(
    15.             GetEntityQuery(new EntityQueryDesc
    16.             {
    17.                 None = new ComponentType[]
    18.                 {
    19.                     typeof(StatefulTriggerEvent)
    20.                 }
    21.             })
    22.         );
    23.         RequireForUpdate(GetEntityQuery(new EntityQueryDesc
    24.         {
    25.             All = new ComponentType[]
    26.             {
    27.                 typeof(TriggerVolumeChangeMaterial)
    28.             }
    29.         }));
    30.     }
    31.  
    32.     protected override void OnUpdate()
    33.     {
    34.         Dependency = JobHandle.CombineDependencies(m_TriggerSystem.OutDependency, Dependency);
    35.  
    36.         var commandBuffer = m_CommandBufferSystem.CreateCommandBuffer();
    37.  
    38.         // Need this extra variable here so that it can
    39.         // be captured by Entities.ForEach loop below
    40.         var nonTriggerMask = m_NonTriggerMask;
    41.  
    42.         Entities
    43.             .WithName("ChangeMaterialOnTriggerEnter")
    44.             .WithoutBurst()
    45.             .ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> triggerEventBuffer, ref TriggerVolumeChangeMaterial changeMaterial) =>
    46.             {
    47.                 for (int i = 0; i < triggerEventBuffer.Length; i++)
    48.                 {
    49.                     var triggerEvent = triggerEventBuffer[i];
    50.                     var otherEntity = triggerEvent.GetOtherEntity(e);
    51.  
    52.                     // exclude other triggers and processed events
    53.                     if (triggerEvent.State == EventOverlapState.Stay || !nonTriggerMask.Matches(otherEntity))
    54.                     {
    55.                         continue;
    56.                     }
    57.  
    58.                     if (triggerEvent.State == EventOverlapState.Enter)
    59.                     {
    60.                         var volumeRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
    61.                         var overlappingRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(otherEntity);
    62.                         volumeRenderMesh.material = overlappingRenderMesh.material;
    63.  
    64.                         commandBuffer.SetSharedComponent(e, volumeRenderMesh);
    65.                     }
    66.                     else
    67.                     {
    68.                         // State == PhysicsEventState.Exit
    69.                         if (changeMaterial.ReferenceEntity == Entity.Null)
    70.                         {
    71.                             continue;
    72.                         }
    73.                         commandBuffer.DestroyEntity(e);
    74.                     }
    75.                 }
    76.             }).Run();
    77.  
    78.         m_CommandBufferSystem.AddJobHandleForProducer(Dependency);
    79.     }
    80. }
    It works! The Asteroid turns red and is destroyed


    but I receive this message:
    Found a ghost in the ghost map which does not have an entity connected to it. This can happen if you delete ghost entities on the client.


    How should I interpret "found a ghost in the ghost map which does not have an entity connected to it"? I assume it is because this system is being run on both the client and server and it should only be run on the server. How can I specify for this specific system to only be run on the server? Can I add it only to the "server world"?
    - Or is that not the issue?
    - I tried to change so it is in [UpdateInGroup(typeof(ServerSimulationSystemGroup))] but it broke it
    - if possible I would like to not use asmdef folder lockdown approach that is used in Asteroid multiplayer sample
     
  19. kanesteven

    kanesteven

    Joined:
    Aug 30, 2018
    Posts:
    34
    @adammpolak
    Add this to your Update constraints:

    [UpdateInWorld(UpdateInWorld.TargetWorld.Server)]


    Destroying ghosts on the client is not really currently supported ( and is an extremely deep rabbit hole to go down for myriad reasons right now ).
     
    olenka_moetsi and adammpolak like this.
  20. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven

    nice, that did it, thank you! The Asteroid now gets destroyed without a warning but...

    For some reason that caused the Asteroid to stopped turning red on enter?

    I am trying to wrap my mind around why
    Code (CSharp):
    1.  
    2.                     if (triggerEvent.State == EventOverlapState.Enter)
    3.                     {
    4.                         var volumeRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
    5.                         var overlappingRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(otherEntity);
    6.                         volumeRenderMesh.material = overlappingRenderMesh.material;
    7.                         commandBuffer.SetSharedComponent(e, volumeRenderMesh);
    8.                     }
    Is affected by this now being updated in server world?
     
    olenka_moetsi likes this.
  21. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven I just realized it is because the color only changes on the server, not on the client. So I guess NetCode doesn't automatically sync materials between the client and the sever.

    @timjohansson is there a way to make it so the material is also sync'd between the server and client? A flag I can put somewhere?
    - or how else do I sync this change?
     
  22. kanesteven

    kanesteven

    Joined:
    Aug 30, 2018
    Posts:
    34
    @adammpolak I would do the following:

    Code (csharp):
    1.  
    2. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    3. [UpdateAfter(typeof(TriggerEventConversionSystem))]
    4. public class MySystem : SystemBase {
    5.   protected void OnUpdate() {
    6.     var isServer = World.GetExistingSystem<ServerSimulationSystemGroup>() != null;
    7.  
    8.     Entities
    9.     .ForEach((Entity entity) => {
    10.       if (isServer) {
    11.         Kill(entity);
    12.       } else {
    13.         ChangeColor(entity);
    14.       }
    15.     })
    16.     .WithBurst()
    17.     .ScheduleParallel();
    18.   }
    19. }
    20.  
    This gets into the territory of client-side prediction and a host of other related subjects if you go too deeply but this should help you increment along the path.
     
    olenka_moetsi likes this.
  23. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    This is great thank you, got it going red and destroying :)
     
    olenka_moetsi likes this.
  24. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson

    I used the same "TriggerEventConversionSystem" from the UnityPhysics demo:
    Code (CSharp):
    1.  
    2.     [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    3.     [UpdateAfter(typeof(StepPhysicsWorld))]
    4.     [UpdateBefore(typeof(EndFramePhysicsSystem))]
    5.     public class TriggerEventConversionSystem : SystemBase
    6.     {
    7.         public JobHandle OutDependency => Dependency;
    8.  
    9.         private StepPhysicsWorld m_StepPhysicsWorld = default;
    10.         private BuildPhysicsWorld m_BuildPhysicsWorld = default;
    11.         private EndFramePhysicsSystem m_EndFramePhysicsSystem = default;
    12.         private EntityQuery m_Query = default;
    13.  
    14.         private NativeList<StatefulTriggerEvent> m_PreviousFrameTriggerEvents;
    15.         private NativeList<StatefulTriggerEvent> m_CurrentFrameTriggerEvents;
    16.  
    17.         protected override void OnCreate()
    18.         {
    19.             m_StepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
    20.             m_BuildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
    21.             m_EndFramePhysicsSystem = World.GetOrCreateSystem<EndFramePhysicsSystem>();
    22.             m_Query = GetEntityQuery(new EntityQueryDesc
    23.             {
    24.                 All = new ComponentType[]
    25.                 {
    26.                     typeof(StatefulTriggerEvent)
    27.                 },
    28.                 None = new ComponentType[]
    29.                 {
    30.                     typeof(ExcludeFromTriggerEventConversion)
    31.                 }
    32.             });
    33.  
    34.             m_PreviousFrameTriggerEvents = new NativeList<StatefulTriggerEvent>(Allocator.Persistent);
    35.             m_CurrentFrameTriggerEvents = new NativeList<StatefulTriggerEvent>(Allocator.Persistent);
    36.         }
    37.  
    38.         protected override void OnDestroy()
    39.         {
    40.             m_PreviousFrameTriggerEvents.Dispose();
    41.             m_CurrentFrameTriggerEvents.Dispose();
    42.         }
    43.  
    44.         protected void SwapTriggerEventStates()
    45.         {
    46.             var tmp = m_PreviousFrameTriggerEvents;
    47.             m_PreviousFrameTriggerEvents = m_CurrentFrameTriggerEvents;
    48.             m_CurrentFrameTriggerEvents = tmp;
    49.             m_CurrentFrameTriggerEvents.Clear();
    50.         }
    51.  
    52.         protected static void AddTriggerEventsToDynamicBuffers(NativeList<StatefulTriggerEvent> triggerEventList,
    53.             ref BufferFromEntity<StatefulTriggerEvent> bufferFromEntity, NativeHashMap<Entity, byte> entitiesWithTriggerBuffers)
    54.         {
    55.             for (int i = 0; i < triggerEventList.Length; i++)
    56.             {
    57.                 var triggerEvent = triggerEventList[i];
    58.                 if (entitiesWithTriggerBuffers.ContainsKey(triggerEvent.EntityA))
    59.                 {
    60.                     bufferFromEntity[triggerEvent.EntityA].Add(triggerEvent);
    61.                 }
    62.                 if (entitiesWithTriggerBuffers.ContainsKey(triggerEvent.EntityB))
    63.                 {
    64.                     bufferFromEntity[triggerEvent.EntityB].Add(triggerEvent);
    65.                 }
    66.             }
    67.         }
    68.  
    69.         public static void UpdateTriggerEventState(NativeList<StatefulTriggerEvent> previousFrameTriggerEvents, NativeList<StatefulTriggerEvent> currentFrameTriggerEvents,
    70.             NativeList<StatefulTriggerEvent> resultList)
    71.         {
    72.             int i = 0;
    73.             int j = 0;
    74.  
    75.             while (i < currentFrameTriggerEvents.Length && j < previousFrameTriggerEvents.Length)
    76.             {
    77.                 var currentFrameTriggerEvent = currentFrameTriggerEvents[i];
    78.                 var previousFrameTriggerEvent = previousFrameTriggerEvents[j];
    79.  
    80.                 int cmpResult = currentFrameTriggerEvent.CompareTo(previousFrameTriggerEvent);
    81.  
    82.                 // Appears in previous, and current frame, mark it as Stay
    83.                 if (cmpResult == 0)
    84.                 {
    85.                     currentFrameTriggerEvent.State = EventOverlapState.Stay;
    86.                     resultList.Add(currentFrameTriggerEvent);
    87.                     i++;
    88.                     j++;
    89.                 }
    90.                 else if (cmpResult < 0)
    91.                 {
    92.                     // Appears in current, but not in previous, mark it as Enter
    93.                     currentFrameTriggerEvent.State = EventOverlapState.Enter;
    94.                     resultList.Add(currentFrameTriggerEvent);
    95.                     i++;
    96.                 }
    97.                 else
    98.                 {
    99.                     // Appears in previous, but not in current, mark it as Exit
    100.                     previousFrameTriggerEvent.State = EventOverlapState.Exit;
    101.                     resultList.Add(previousFrameTriggerEvent);
    102.                     j++;
    103.                 }
    104.             }
    105.  
    106.             if (i == currentFrameTriggerEvents.Length)
    107.             {
    108.                 while (j < previousFrameTriggerEvents.Length)
    109.                 {
    110.                     var triggerEvent = previousFrameTriggerEvents[j++];
    111.                     triggerEvent.State = EventOverlapState.Exit;
    112.                     resultList.Add(triggerEvent);
    113.                 }
    114.             }
    115.             else if (j == previousFrameTriggerEvents.Length)
    116.             {
    117.                 while (i < currentFrameTriggerEvents.Length)
    118.                 {
    119.                     var triggerEvent = currentFrameTriggerEvents[i++];
    120.                     triggerEvent.State = EventOverlapState.Enter;
    121.                     resultList.Add(triggerEvent);
    122.                 }
    123.             }
    124.         }
    125.  
    126.         protected override void OnUpdate()
    127.         {
    128.             if (m_Query.CalculateEntityCount() == 0)
    129.             {
    130.                 return;
    131.             }
    132.  
    133.             Dependency = JobHandle.CombineDependencies(m_StepPhysicsWorld.FinalSimulationJobHandle, Dependency);
    134.  
    135.             Entities
    136.                 .WithName("ClearTriggerEventDynamicBuffersJobParallel")
    137.                 .WithBurst()
    138.                 .WithNone<ExcludeFromTriggerEventConversion>()
    139.                 .ForEach((ref DynamicBuffer<StatefulTriggerEvent> buffer) =>
    140.                 {
    141.                     buffer.Clear();
    142.                 }).ScheduleParallel();
    143.  
    144.             SwapTriggerEventStates();
    145.  
    146.             var currentFrameTriggerEvents = m_CurrentFrameTriggerEvents;
    147.             var previousFrameTriggerEvents = m_PreviousFrameTriggerEvents;
    148.  
    149.             var triggerEventBufferFromEntity = GetBufferFromEntity<StatefulTriggerEvent>();
    150.             var physicsWorld = m_BuildPhysicsWorld.PhysicsWorld;
    151.  
    152.             var collectTriggerEventsJob = new CollectTriggerEventsJob
    153.             {
    154.                 TriggerEvents = currentFrameTriggerEvents
    155.             };
    156.  
    157.             var collectJobHandle = collectTriggerEventsJob.Schedule(m_StepPhysicsWorld.Simulation, ref physicsWorld, Dependency);
    158.  
    159.             // Using HashMap since HashSet doesn't exist
    160.             // Setting value type to byte to minimize memory waste
    161.             NativeHashMap<Entity, byte> entitiesWithBuffersMap = new NativeHashMap<Entity, byte>(0, Allocator.TempJob);
    162.  
    163.             var collectTriggerBuffersHandle = Entities
    164.                 .WithName("CollectTriggerBufferJob")
    165.                 .WithBurst()
    166.                 .WithNone<ExcludeFromTriggerEventConversion>()
    167.                 .ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> buffer) =>
    168.                 {
    169.                     entitiesWithBuffersMap.Add(e, 0);
    170.                 }).Schedule(Dependency);
    171.  
    172.             Dependency = JobHandle.CombineDependencies(collectJobHandle, collectTriggerBuffersHandle);
    173.  
    174.             Job
    175.                 .WithName("ConvertTriggerEventStreamToDynamicBufferJob")
    176.                 .WithBurst()
    177.                 .WithCode(() =>
    178.                 {
    179.                     currentFrameTriggerEvents.Sort();
    180.  
    181.                     var triggerEventsWithStates = new NativeList<StatefulTriggerEvent>(currentFrameTriggerEvents.Length, Allocator.Temp);
    182.  
    183.                     UpdateTriggerEventState(previousFrameTriggerEvents, currentFrameTriggerEvents, triggerEventsWithStates);
    184.                     AddTriggerEventsToDynamicBuffers(triggerEventsWithStates, ref triggerEventBufferFromEntity, entitiesWithBuffersMap);
    185.                 }).Schedule();
    186.  
    187.             m_EndFramePhysicsSystem.AddInputDependency(Dependency);
    188.             entitiesWithBuffersMap.Dispose(Dependency);
    189.         }
    190.  
    191.         [BurstCompile]
    192.         public struct CollectTriggerEventsJob : ITriggerEventsJob
    193.         {
    194.             public NativeList<StatefulTriggerEvent> TriggerEvents;
    195.  
    196.             public void Execute(TriggerEvent triggerEvent)
    197.             {
    198.                 TriggerEvents.Add(new StatefulTriggerEvent(
    199.                     triggerEvent.EntityA, triggerEvent.EntityB, triggerEvent.BodyIndexA, triggerEvent.BodyIndexB,
    200.                     triggerEvent.ColliderKeyA, triggerEvent.ColliderKeyB));
    201.             }
    202.         }
    203.     }
    But when using it with my Asteroids NetCode project I get these warnings:
    upload_2020-11-11_15-54-20.png

    But when I commented out the
    Code (CSharp):
    1.  
    2.     [UpdateAfter(typeof(StepPhysicsWorld))]
    3.     [UpdateBefore(typeof(EndFramePhysicsSystem))]
    The warnings go away.

    Is it "okay" to comment out those UpdateAfter and UpdateBefore to get rid of the warnings or will this introduce unexpected behavior? How else do I remove these warnings using Unity Physics package in NetCode? (I'm guessing the separation of different worlds/systems between servers/clients in NetCode is the reason these attributes are causing warnings in the NetCode proejct)
     
    Last edited: Nov 12, 2020
    ichiyzzr and olenka_moetsi like this.
  25. WAYN_Games

    WAYN_Games

    Joined:
    Mar 16, 2019
    Posts:
    817

    hello, these warning are telling you your TriggerEventConversionSystem does not update in the same group as StepPhysicsWorld and EndFramePhysicsSystem so they can't be ordered because their respective group ordering will take precedence.

    So if your system behave has expected, you can remove safely those Update after/before.
    If you see unexpected behavior (not working or error traces) you should look at the group the physics system are updating in and put your system in the same group.
    Maybe you are not using the same version of the physics package in your project than the one in the unity demo you took reference from, so the group changed maybe.
     
    olenka_moetsi and adammpolak like this.
  26. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven as an FYI I went with:
    Code (CSharp):
    1.                         var overlappingRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(otherEntity);
    2.                         overlappingRenderMesh.material = volumeRenderMesh.material;
    3.  
    4.                         commandBuffer.SetSharedComponent(otherEntity, overlappingRenderMesh);
    5.                         commandBuffer.AddComponent(otherEntity, new DestroyTag());
    - So I add a destroy tag on both the client and server
    - but only the server runs the system which destroys entities with the DestroyTag
     
    Last edited: Nov 13, 2020
    olenka_moetsi likes this.
  27. kanesteven

    kanesteven

    Joined:
    Aug 30, 2018
    Posts:
    34
    @adammpolak This is more or less exactly what I am doing as well and what I assume is probably the ideal solution unless you want to implement true, predictive destruction ( which I don't think is actually worth doing ). Nice work.
     
    olenka_moetsi and adammpolak like this.
  28. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson I am getting some funny behavior incorporating Unity Physics + NetCode

    I am using the following the example in UnityPhysicsSamples to create trigger events for when a bullet collides with Physics shapes.

    In my project
    TriggerChangeMaterialAddDestroyTagSystem
    parses through trigger events and when

    triggerEvent.State == EventOverlapState.Enter
    it runs:
    Code (CSharp):
    1.                         var overlappingRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(otherEntity);
    2.                         overlappingRenderMesh.material = volumeRenderMesh.material;
    3.  
    4.                         commandBuffer.SetSharedComponent(otherEntity, overlappingRenderMesh);
    5.                         commandBuffer.AddComponent(otherEntity, new DestroyTag());
    This changes the material of the entity the bullet collides with to the color of the bullet as well as adds a DestroyTag to the entity. The Destroy system is run only on the server.

    I have also added some logs before the material change and adding the DestroyTag to better understand how this system is operating. It checks if it is the server/client and logs which one it is, and if there was already a DestroyTag on the entity that the bullet collided with:

    Code (CSharp):
    1. var isServer = World.GetExistingSystem<ServerSimulationSystemGroup>() != null;
    2.  
    3.                         if (isServer)
    4.                         {
    5.                             Debug.Log("this is the server running it");
    6.                             if (HasComponent<DestroyTag>(otherEntity))
    7.                             {
    8.                                 Debug.Log("already had destroy tag");
    9.                                 return;
    10.                             }
    11.                         }
    12.                         else
    13.                         {
    14.                             Debug.Log("this is the client running it");
    15.                             if (HasComponent<DestroyTag>(otherEntity))
    16.                             {
    17.                                 Debug.Log("already had destroy tag");
    18.                                 return;
    19.                             }
    20.                         }
    I wanted to understand what happens on the client/server on collisions when a bullet collides with an Asteroid (server spawned player interpolated) and a Player (player spawned and predicted).

    When intersecting with an Asteroid, the client seems to run the "on enter" trigger twice, some times more. The server always seems to run the collision only once.

    upload_2020-11-13_13-32-40.png

    When intersecting with a Player, the server seems to always run the "on enter" trigger twice. The client seems to run it between 1-3 times.

    upload_2020-11-13_13-37-57.png


    Why are "on enter" collisions causing more than 1 trigger? Why are there multiple triggers that meet "on enter" and why do they vary based on what type of object is being collided with and whether the server or client is running the system?
    - It seems like the entity already being deleted before the next trigger in the buffer is processed causes the "A component with type:RenderMesh has not been added to the entity" issue

    ~10% of the time the client witnesses a collision but the server does not, why would this occur?
    upload_2020-11-13_13-57-29.png
    - Asteroid turned red because the client said the bullet collided with the asteroid
    - I assume the server did not feel the same way and that is why it has not been deleted
    (there shouldn't be any red asteroids)



    Full system here:
    Code (CSharp):
    1. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    2. [UpdateAfter(typeof(TriggerEventConversionSystem))]
    3. public class TriggerChangeMaterialAddDestroyTagSystem : SystemBase
    4. {
    5.     private EndFixedStepSimulationEntityCommandBufferSystem m_CommandBufferSystem;
    6.     private TriggerEventConversionSystem m_TriggerSystem;
    7.     private EntityQueryMask m_NonTriggerMask;
    8.  
    9.     protected override void OnCreate()
    10.     {
    11.         m_CommandBufferSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
    12.         m_TriggerSystem = World.GetOrCreateSystem<TriggerEventConversionSystem>();
    13.         m_NonTriggerMask = EntityManager.GetEntityQueryMask(
    14.             GetEntityQuery(new EntityQueryDesc
    15.             {
    16.                 None = new ComponentType[]
    17.                 {
    18.                     typeof(StatefulTriggerEvent)
    19.                 }
    20.             })
    21.         );
    22.         RequireForUpdate(GetEntityQuery(new EntityQueryDesc
    23.         {
    24.             All = new ComponentType[]
    25.             {
    26.                 typeof(TriggerVolumeChangeMaterial)
    27.             }
    28.         }));
    29.     }
    30.  
    31.     protected override void OnUpdate()
    32.     {
    33.         Dependency = JobHandle.CombineDependencies(m_TriggerSystem.OutDependency, Dependency);
    34.  
    35.         var commandBuffer = m_CommandBufferSystem.CreateCommandBuffer();
    36.  
    37.         // Need this extra variable here so that it can
    38.         // be captured by Entities.ForEach loop below
    39.         var nonTriggerMask = m_NonTriggerMask;
    40.  
    41.         Entities
    42.             .WithName("ChangeMaterialOnTriggerEnter")
    43.             .WithoutBurst()
    44.             .ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> triggerEventBuffer, ref TriggerVolumeChangeMaterial changeMaterial) =>
    45.             {
    46.                 for (int i = 0; i < triggerEventBuffer.Length; i++)
    47.                 {
    48.                     var triggerEvent = triggerEventBuffer[i];
    49.                     var otherEntity = triggerEvent.GetOtherEntity(e);
    50.  
    51.                     // exclude other triggers and processed events
    52.                     if (triggerEvent.State == EventOverlapState.Stay || !nonTriggerMask.Matches(otherEntity))
    53.                     {
    54.                         continue;
    55.                     }
    56.  
    57.                     if (triggerEvent.State == EventOverlapState.Enter)
    58.                     {
    59.                         var volumeRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
    60.                         //this is where the issue occurs right below
    61.                         // so when it just intersects the other entity doesn't have a render mesh
    62.                         // so the asteroid/player doesn't have a render mesh yet
    63.                         // and this shows up when i just die and come back to life
    64.                         // this happens as it dies.
    65.                         var isServer = World.GetExistingSystem<ServerSimulationSystemGroup>() != null;
    66.  
    67.                         if (isServer)
    68.                         {
    69.                             Debug.Log("this is the server running it");
    70.                             if (HasComponent<DestroyTag>(otherEntity))
    71.                             {
    72.                                 Debug.Log("already had destroy tag");
    73.                                 return;
    74.                             }
    75.                         }
    76.                         else
    77.                         {
    78.                             Debug.Log("this is the client running it");
    79.                             if (HasComponent<DestroyTag>(otherEntity))
    80.                             {
    81.                                 Debug.Log("already had destroy tag");
    82.                                 return;
    83.                             }
    84.                         }
    85.                         var overlappingRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(otherEntity);
    86.                         overlappingRenderMesh.material = volumeRenderMesh.material;
    87.  
    88.                         commandBuffer.SetSharedComponent(otherEntity, overlappingRenderMesh);
    89.                         commandBuffer.AddComponent(otherEntity, new DestroyTag());
    90.                     }
    91.                     else
    92.                     {
    93.                         // State == PhysicsEventState.Exit
    94.                         if (changeMaterial.ReferenceEntity == Entity.Null)
    95.                         {
    96.                             continue;
    97.                         }
    98.                     }
    99.                 }
    100.             }).Run();
    101.  
    102.         m_CommandBufferSystem.AddJobHandleForProducer(Dependency);
    103.     }
    104. }
    105.  
     
    Last edited: Nov 13, 2020
    olenka_moetsi likes this.
  29. kanesteven

    kanesteven

    Joined:
    Aug 30, 2018
    Posts:
    34
    @adammpolak I believe you have now hit the next branch of pain with this problem-space.

    There is an extremely subtle thing happening with physics and NetCode that you must completely understand to eliminate this problem:

    The physics world in a given tick is only built ONE time.
    The client builds it one time.
    The server builds it one time.

    The problem here, is that the client should actually be re-building it INSIDE the GhostPredictionSystemGroup because it needs the excatly-correct current physics world when you are running your predictive code. If you were to move the BuildPhysicsWorld system into the beginning of the GhostPredictionSystemGroup you would then be re-building the physics world during all re-predicted ticks ensuring that you have the correct data to run your detection against. If you actually want to use the physics system itself for collision-resolution etc then you'll need to run both the UpdatePhysicsWorld and ExportPhysicsWorld systems somewhere in your GhostPredictionSystemGroup as well.

    I debugged an exactly-similar problem myself when trying to get smooth, continuous collision working in my own game and it took a lot of mental exercise to sort out what was going on here. For what it's worth, I cheated at that time to get this working as I only cared about having the correct Collision world during re-prediction. To do this, I wrote the following extremely-dirty but simple system which you are free to copy if you like:


    Code (CSharp):
    1. using Unity.Entities;
    2. using Unity.Physics.Systems;
    3. using Unity.NetCode;
    4.  
    5. [UpdateInWorld(UpdateInWorld.TargetWorld.Client)]
    6. [UpdateInGroup(typeof(GhostPredictionSystemGroup), OrderFirst = true)]
    7. public class ClientRebuildPhysicsWorldSystem : SystemBase {
    8.   public BuildPhysicsWorld BuildPhysicsWorld;
    9.  
    10.   protected override void OnCreate() {
    11.     BuildPhysicsWorld = World.GetExistingSystem<BuildPhysicsWorld>();
    12.   }
    13.  
    14.   protected override void OnUpdate() {
    15.     BuildPhysicsWorld.AddInputDependency(Dependency);
    16.     BuildPhysicsWorld.Update();
    17.     Dependency = BuildPhysicsWorld.GetOutputDependency();
    18.   }
    19. }
    I am not sure what the "right" way to override the Group/Order that physics systems run in. However, you could certainly get this done more holistically by writing your own Bootstrap which extends the NetCode-provided "ClientServerBootstrap" and then finding the existing instances of the Physics systems and moving them to your desired groups.
     
    Last edited: Nov 13, 2020
    friflo, olenka_moetsi and adammpolak like this.
  30. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven , really really appreciate the pointer here and breaking down the problem, and explaining the new branch lol. I am going to see if I can make a similar fix using your approach. I was thinking about messing with Bootstrap.cs but couldn't really wrap my head around what was happening in that file tbh.

    Thank you!
     
    olenka_moetsi likes this.
  31. kanesteven

    kanesteven

    Joined:
    Aug 30, 2018
    Posts:
    34
    @adammpolak Here is the code from my custom bootstrap. This particular code is establishing a world for the client's "Application state" which is completely independent of any so-called "client world" associated with being connected to some specific server. The details of what is happening here may or may not interest you but at least it would be an example of what a custom bootstrap looks like "deriving from ClientServerBootstrap while preserving only the behaviors from it that I wanted".


    Code (CSharp):
    1. using Unity.Entities;
    2. using Unity.NetCode;
    3. using Unity.Networking.Transport;
    4.  
    5. public class NetPongClientServerBootstrap : ClientServerBootstrap {
    6.   public static ushort DEFAULT_PORT = 7979;
    7.  
    8.   public override bool Initialize(string defaultWorldName) {
    9.     World.DefaultGameObjectInjectionWorld = new World(defaultWorldName);
    10.     GenerateSystemLists(DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default));
    11.     DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(World.DefaultGameObjectInjectionWorld, ExplicitDefaultWorldSystems);
    12.     #if !UNITY_DOTSRUNTIME
    13.     ScriptBehaviourUpdateOrder.AddWorldToCurrentPlayerLoop(World.DefaultGameObjectInjectionWorld);
    14.     #endif
    15.  
    16.  
    17.     // Client-only initialization
    18.     if (RequestedPlayType != PlayType.Server) {
    19.       var defaultWorld = World.DefaultGameObjectInjectionWorld;
    20.       var defaultSimulationSystemGroup = defaultWorld.GetExistingSystem<SimulationSystemGroup>();
    21.       var clientMenuWorld = new World("Client Menu World");
    22.       var clientMenuSystemGroup = clientMenuWorld.CreateSystem<ClientMenuSystemGroup>();
    23.       var clientMenuSystem = clientMenuWorld.CreateSystem<ClientMenuSystem>();
    24.  
    25.       clientMenuSystemGroup.AddSystemToUpdateList(clientMenuSystem);
    26.       defaultSimulationSystemGroup.AddSystemToUpdateList(clientMenuSystemGroup);
    27.     }
    28.  
    29.     // Server-only initialization
    30.     if (RequestedPlayType != PlayType.Client) {
    31.       var defaultWorld = World.DefaultGameObjectInjectionWorld;
    32.       var serverWorld = CreateServerWorld(defaultWorld, "ServerWorld");
    33.       var networkStream = serverWorld.GetExistingSystem<NetworkStreamReceiveSystem>();
    34.       var endPoint = NetworkEndPoint.AnyIpv4;
    35.  
    36.       endPoint.Port = DEFAULT_PORT;
    37.       networkStream.Listen(endPoint);
    38.       UnityEngine.Debug.Log($"Server listening on port {endPoint.Port}");
    39.     }
    40.     return true;
    41.   }
    42. }
    IMPORTANT: If you should happen to copy this please understand this will NOT even create a client world by default! Instead, my application creates this later when the client actually tries to connect to a server. Just don't want you to be massively tripped-up by this.

    For what it's worth, the vast, vast majority of the lines of code / complexity in NetCode's ClientServerBootstrap have to do w/ interpreting the
    UpdateInWorld
    UpdateInGroup
    UpdateBefore/After
    Attributes such that your client and server worlds are setup as you expect. If you can sort of ... see past that complexity the rest of what is going on is pretty mundane/straightforward.
     
    Last edited: Nov 15, 2020
    bb8_1, adammpolak and florianhanke like this.
  32. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven this is phenomenal!

    Once I get the physics for entity destruction solved the next piece is working on the app-launch "pre-game view". This is:
    - start local match
    - join local match
    - join internet match

    Using Unity UI. I need to broadcast games and trigger joining as a client/server. This gets me a leg up on that application state because it seems like you have been able to "prevent" the client/server worlds from launching until triggered.

    Really really appreciate the heads up on this and saving me a nightmare :)
     
  33. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven sorry for coming back to the well but...

    I have been trying to implement your approach by using bootstrap to "group" systems correctly and create worlds. I want to also create the server world on trigger. The initial game menu will be where triggers occur to create client or client/server build types if the user wants to host a LAN match.

    1. Just to make sure I am interpreting the code sample correctly, "Client Menu World" is the world which will be storing the "Application State"? (I assume the state is modified by the ClientMenuSystem which works on "application state component" or something along those lines.)

    For some reason I cannot get the systems running. I tried to mimic the way you set up your client world but instead of
    var serverWorld = CreateServerWorld(defaultWorld, "ServerWorld");
    I used
    var clientWorld = CreateClientWorld(defaultWorld, "ClientWorld");


    2. Am I using the right approach of mimicking your server world set up (and need to just debug more) or is there more to setting up the client world?

    Sorry for nagging again after all your help, really appreciate it
     
  34. kanesteven

    kanesteven

    Joined:
    Aug 30, 2018
    Posts:
    34
    @adammpolak if you paste your bootstrap code it will be easier to know what's happening. Your assumption 1 is correct. In theory, yes your approach described in 2 should be correct as well.
     
  35. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson

    After working through physics issues (debugging with @kanesteven) I checked the Physics/Netcode/HybridRenderer change logs and it looked like upgrading to the latest Physics and NetCode seemed like it would handle a lot of the system group issues.

    upload_2020-11-21_15-7-33.png

    It seems like upgrading from NetCode 0.4 to 0.5 requires updating RPC logic. To schedule in the RpcQueue now requires 3 parameters:

    public void Schedule(DynamicBuffer<OutgoingRpcDataStreamBufferComponent> buffer, ComponentDataFromEntity<GhostComponent> ghostFromEntity, TActionRequest data)

    https://docs.unity3d.com/Packages/com.unity.netcode@0.5/api/Unity.NetCode.RpcQueue-2.html

    I am trying to send from client to server that the level has been loaded similar to LoadLevelSystem.cs on the client in the Asteroids sample

    Code (CSharp):
    1. protected override void OnUpdate()
    2.         {
    3.             if (!HasSingleton<LevelComponent>())
    4.             {
    5.                 // The level always exist, "loading" just resizes it
    6.                 m_LevelSingleton = EntityManager.CreateEntity();
    7.                 EntityManager.AddComponentData(m_LevelSingleton, new LevelComponent {width = 0, height = 0});
    8.             }
    9.             var commandBuffer = m_Barrier.CreateCommandBuffer().AsParallelWriter();
    10.             var rpcFromEntity = GetBufferFromEntity<OutgoingRpcDataStreamBufferComponent>();
    11.             var levelFromEntity = GetComponentDataFromEntity<LevelComponent>();
    12.             var levelSingleton = m_LevelSingleton;
    13.             var rpcQueue = m_RpcQueue;
    14.             Entities.ForEach((Entity entity, int nativeThreadIndex, in LevelLoadRequest request, in ReceiveRpcCommandRequestComponent requestSource) =>
    15.             {
    16.                 commandBuffer.DestroyEntity(nativeThreadIndex, entity);
    17.                 // Check for disconnects
    18.                 if (!rpcFromEntity.HasComponent(requestSource.SourceConnection))
    19.                     return;
    20.                 // set the level size - fake loading of level
    21.                 levelFromEntity[levelSingleton] = new LevelComponent
    22.                 {
    23.                     width = request.width,
    24.                     height = request.height,
    25.                     playerForce = request.playerForce,
    26.                     bulletVelocity = request.bulletVelocity
    27.                 };
    28.                 commandBuffer.AddComponent(nativeThreadIndex, requestSource.SourceConnection, new PlayerStateComponentData());
    29.                 commandBuffer.AddComponent(nativeThreadIndex, requestSource.SourceConnection, default(NetworkStreamInGame));
    30.                 rpcQueue.Schedule(rpcFromEntity[requestSource.SourceConnection], new RpcLevelLoaded());
    31.             }).Schedule();
    32.             m_Barrier.AddJobHandleForProducer(Dependency);
    What should pass in for ComponentDataFromEntity<GhostComponent> ghostFromEntity in Schedule()?
    - I don't know from what entity I should pull the GhostComponent
    - There is no player entity yet, I am just loading the level
    - trying to get the GhostComponent from the Network Connection Entity causes an error

    when I add
    var ghostFromEntity = GetComponentDataFromEntity<GhostComponent>();

    and include it in .Schedule
    rpcQueue.Schedule(rpcFromEntity[requestSource.SourceConnection], ghostFromEntity[requestSource.SourceConnection], new RpcLevelLoaded())


    I get:
    Assets\ScriptsAndPrefabs\Client\Systems\ClientLoadLevelSystem.cs(55,78): error CS1503: Argument 2: cannot convert from 'Unity.NetCode.GhostComponent' to 'Unity.Entities.ComponentDataFromEntity<Unity.NetCode.GhostComponent>'
     
    Last edited: Nov 21, 2020
  36. adam_unity302

    adam_unity302

    Joined:
    Nov 23, 2020
    Posts:
    2
    @timjohansson

    I updated to Netcode 0.5 and the latest entities/physics.

    How do I get rid of "Large serverTick prediction error. Server tick rollback to" error?

    upload_2020-11-23_17-4-45.png

    Code (CSharp):
    1. Large serverTick prediction error. Server tick rollback to 53 delta: -13.41136
    2. UnityEngine.Debug:LogError(Object)
    3. Unity.NetCode.NetworkTimeSystem:OnUpdate() (at Library/PackageCache/com.unity.netcode@0.5.0-preview.5/Runtime/Connection/NetworkTimeSystem.cs:201)
    4. Unity.Entities.SystemBase:Update() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/SystemBase.cs:399)
    5. Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystemGroup.cs:435)
    6. Unity.Entities.ComponentSystemGroup:OnUpdate() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystemGroup.cs:387)
    7. Unity.Entities.ComponentSystem:Update() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystem.cs:113)
    8. Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystemGroup.cs:435)
    9. Unity.Entities.ComponentSystemGroup:OnUpdate() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystemGroup.cs:387)
    10. Unity.Entities.ComponentSystem:Update() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystem.cs:113)
    11. Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystemGroup.cs:435)
    12. Unity.Entities.ComponentSystemGroup:OnUpdate() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystemGroup.cs:387)
    13. Unity.Entities.ComponentSystem:Update() (at Library/PackageCache/com.unity.entities@0.16.0-preview.21/Unity.Entities/ComponentSystem.cs:113)
    So far all I have done is connect a client and server. I have commented out all other systems. I follow the same flow as in the Asteroids sample

    - NetCodeBootstrap.cs to create the worlds
    - ClientServerConnection.cs to listen on a port when server and connect to a port when client
    - ServerLoadLevelSystem.cs to send a "load level" rpc to client
    - ClientLoadLevelSystem.cs to send a "loaded the level" rpc to the server

    What causes this error to occur, what should I be looking into?
     
    bb8_1 and adammpolak like this.
  37. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    It seems like BuildPhysicsWorld and StepPhysicsWorld are taking ~250ms, maybe that is causing some kind of backup which causes the issue between client/server?

    upload_2020-11-24_16-54-14.png
     
  38. StickyMitchel

    StickyMitchel

    Joined:
    Sep 2, 2019
    Posts:
    19
    This error sometimes happens with us when the fps gets too low. In a build if the Server does not respond for a small while the client spams these messages and it seems to be a normal rollback message.
    The difference between what the Client predicts the Server tick will be and what it actually is is too big so it will send this message to let you know it's rolling back to the correct Server tick on the Client.
    It's possible that this message has been included in 0.5 so you can be aware of this problem, so it might not be a bug but rather letting you know the Server is running slower than expected.
     
    adammpolak likes this.
  39. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    Appreciate it StickyMitchel! The confusing part is that the slow frame rate drop only occurred when I upgraded to latest NetCode/Physics/Entities. Before upgrade it was running smooth.

    There is only a single prefab initiated (the player). I can't figure out why those two physics systems would take 250ms when they didn't before :(
     
  40. StickyMitchel

    StickyMitchel

    Joined:
    Sep 2, 2019
    Posts:
    19
    Oh yeah sorry I somehow missed that adammpolak. It does seem weird that the Physics systems are running that slow.
    The only thing I could think of might have something to do with Burst. What version are you using and how are your Burst settings? (JobsDebugger/Leak Detection/Job Threads/Compliation etc)
     
  41. Kelevra

    Kelevra

    Joined:
    Dec 27, 2012
    Posts:
    87
    Hello @timjohansson !
    Didn't found a separate thread for unity.transport, so will try to ask here. Did you know about such an issue with unity.transport. When you run the game on ios and establish the connection manually everything works great if you do that before locking the screen. But if you minimize or lock the screen then open the game and try to establish the connection it will hang in the connecting state.

    I'm still on netcode 0.2.0, transport 0.3.1, maybe this issue is already fixed.

    Thanks!
     
  42. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    I went through the debugger and I think it was because I commented out 2 lines when initially implementing the Physics system.
    Code (CSharp):
    1. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    2. [UpdateAfter(typeof(TriggerEventConversionSystem))]
    This is because initially with Netcode 0.4 this was causing warnings.

    It seems like with NetCode 0.5 re-ordering system groups these lines needed to be commented back in! (cc @kanesteven you were right, those UpdateInGroup/UpdateBefore/UpdateAfter are no joke!)
     
  43. friflo

    friflo

    Joined:
    Jan 16, 2016
    Posts:
    10
    adammpolak likes this.
  44. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
  45. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson

    How are Ghosts and their children (child GameObjects which when converted become a buffer of entities) sent from the Server to Client in NetCode, is it "all at once" or one entity at a time?

    I have a very odd bug where a .ForEach has
    .WithAll<PlayerTag, PlayerCommand>()
    runs, but returns a: "A component with type:Child[] has not been added to the entity." error

    It is like the parent Player entity is sent to the client (without buffer of children) which causes the system to run because the client locates an entity with a PlayerTag and PlayerCommand. It tries to access the child entities and then returns an error that there is no buffer of Child entities.

    This doesn't happen every time, just once in a while.

    When an entity with a buffer of child entities is spawned on the server, how is the full entity transferred to the client. Is there a way to "wait" until the "whole" entity has been received?

    EDIT: I updated the .ForEach to be:
    .WithAll<PlayerTag, PlayerCommand, Child>()
    and the error stopped. I am guessing because this means it waits until the full buffer of entities are received? Still curious about how buffer of children are sent from server to client...
     
    Last edited: Nov 29, 2020
  46. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @kanesteven After updating to NetCode 0.5 and changing the ordering of the SystemGroups the issue is gone! I no longer need to build the physics world, it seems like Netcode + Physics plays nice now :)
     
  47. adammpolak

    adammpolak

    Joined:
    Sep 9, 2018
    Posts:
    447
    @timjohansson how does overriding ClientServerBootstrap alter subscene conversion and loading into worlds?

    - I start in a
    NavigationScene
    where there is UI to either host a game or join a game
    - Choosing to host a game launches
    MainScene
    (which contains a subscene) and creates a
    InitializeClientAndServerComponent
    Singleton
    - Creating the
    InitializeClientAndServerComponent
    Singleton allows
    ClientServerConnectionControl
    system to update and listen/connect on a port triggering a load level flow (same as Asteroids sample)

    If I do not override ClientServerBootstrap (so it runs in default) the Client/Server worlds are created on the
    NavigationScene
    (as expected because the NavigationScene is the initial scene loaded). When I load
    MainScene
    the Client/Server worlds are populated with the subscene's entities.



    This is okay, but I want to keep client/server logic/data out of
    NavigationScene
    . So to do this I override
    ClientServerBootstrap
    .
    Code (CSharp):
    1. public class NetCodeBootstrap : ClientServerBootstrap
    2. {
    3.     public override bool Initialize(string defaultWorldName)
    4.     {
    5.         var systems = DefaultWorldInitialization.GetAllSystems(WorldSystemFilterFlags.Default);
    6.         GenerateSystemLists(systems);
    7.  
    8.         var world = new World(defaultWorldName);
    9.         World.DefaultGameObjectInjectionWorld = world;
    10.  
    11.  
    12.         DefaultWorldInitialization.AddSystemsToRootLevelSystemGroups(world, ExplicitDefaultWorldSystems);
    13. #if !UNITY_DOTSRUNTIME
    14.         ScriptBehaviourUpdateOrder.AddWorldToCurrentPlayerLoop(world);
    15. #endif
    16.         return true;
    17.     }
    18. }
    Now in addition to loading
    MainScene
    creating a
    InitializeClientAndServerComponent
    singleton, it also creates the client and server world.
    Code (CSharp):
    1. public class InitializeClientAndServer : MonoBehaviour
    2. {
    3.  
    4.     void Awake()
    5.     {
    6.         var world = World.DefaultGameObjectInjectionWorld;
    7. #if !UNITY_CLIENT || UNITY_SERVER || UNITY_EDITOR
    8.         ClientServerBootstrap.CreateServerWorld(world, "ServerWorld");
    9.  
    10. #endif
    11.  
    12. #if !UNITY_SERVER
    13.         ClientServerBootstrap.CreateClientWorld(world, "ClientWorld");
    14. #endif
    15.         World.DefaultGameObjectInjectionWorld.EntityManager.CreateEntity(typeof(InitializeClientAndServerComponent));
    16.     }
    Now on
    NavigationScene
    there is no Client/Server world (good), when I load
    MainScene
    Client/Server world are created (good), but the subscene entities do not get loaded into either world (bad).


    Where are the entities from the subscene going? Has the subscene not been converted, what do I need to trigger it? Why did overriding ClientServerBootstrap alter the subscene conversion and entity generation process?


    EDIT: I moved creating the client and server worlds into the NavigationScene. Clicking the UI button creates client/server worlds then loads MainScene. It fixes the issue but I would like to know how to order subscene entity conversion in loading a scene.
     
    Last edited: Dec 4, 2020
  48. MarcelArioli

    MarcelArioli

    Joined:
    May 31, 2014
    Posts:
    26
    I have a question regarding dynamic and static send mode:

    Wouldn't it make more sense to define that on a per-component basis instead of per ghost ?
    Components like Position and Rotation make IMO sense to be dynamic as they change often.
    But components on the same ghost that update less often (like i don't know, for example a UnitLevel component) would make more sense to not get sent at all unless they change which would mean static send mode, right ?
     
  49. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    443
    No, there is no way to do remove child entities from a ghost. You could make the child entity a separate prefab and have a predicted client only component which you use to trigger instantiation of the extra entity.

    No, but you can write a custom component storing the color which you change on the server, then have the client copy that color to the actual material. So the server does not update the material directly, it updates something else that is serialized and the client applies it to the material.

    You hsould pass in GetComponentDataFromEntity<GhostComponent>() - without any [entity] access on it. The RPC needs to lookup a GhostComponent from an entity it finds in the RPC data so it needs the full map.

    There were two bugs related to this in 0.5 - one related to clamping of delta time in NetworkTimeSystem and one related to how physics calculates its new fixed timestep that was not compatible with how netcode limits the number of simulation steps causing it to be more expensive than expected when performance is low.
    The issue only happens when your update takes more than simFrequency * maxSimStepsPerFrame. Until we get a fix for those issues out the only workaround is to make your simulation fast enough that it doesn't hit that time limit.
     
    adammpolak and bb8_1 like this.
  50. timjohansson

    timjohansson

    Unity Technologies

    Joined:
    Jul 13, 2016
    Posts:
    443
    I can't recall or find a bug related to that so it is probably not fixed, would be great if you can file one!
     
    bb8_1 likes this.
Thread Status:
Not open for further replies.
unityunity