Search Unity

TransformSystem - Design issue, performance loss

Discussion in 'Entity Component System' started by Enzi, Oct 15, 2018.

  1. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    962
    Hello!

    For a reference what I'm trying to achieve: https://enzisoft.wordpress.com/2016/03/12/factorio-in-unityc-goal-reached/

    I'm updating a lot of positions and most of them won't get rendered on the screen so only a fragment of the items has a MeshInstanceRenderer component.
    What is happening though is that due to a Position being on the entity a LocalToWorld comp is added from some Unity system, which results in the TransformSystem doing a lot of work for entities that are not even rendered. Updating the matrices for 1 million items for example when only 50 are visible.

    The only workaround right now is to put a Frozen component on the entity so the TransformSystem ignores the update.

    I don't feel this is working as intended and I hope my case can lead to a better design.
    Main culprit is the LocalToWorld comp that is added auto-magically somewhere.
     
    davidfrk likes this.
  2. mike_acton

    mike_acton

    Unity Technologies

    Joined:
    Nov 21, 2017
    Posts:
    110
    > I'm updating a lot of positions and most of them won't get rendered on the screen so only a fragment of the items has a MeshInstanceRenderer component.

    The fact that position alone can entirely describe whether or not something is culled in your case (so I imagine there is no hierarchy and thus no parent rotations or scales) is very specific to your problem. I don't know how we could *infer* that without some additional information. What do you suggest you could add here?

    Alternatively, since the culling is so context specific, can that be done in advance of those position updates so that they are not actually updated if you do not want the resulting data?
     
  3. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,684
    Hey if they moves and you want stop all actions if they not visible on screen, how they moves to screen? In this case the will be outside forever and can’t moves to screen, and if any visible entity goes outside frustrum then this entity will be “lost” forever :) (if camera static for example)
     
  4. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    For your case, wouldn't you just either

    a) write your own TransformSystem that depends on MeshInstanceRenderer or something
    b) write your own CustomPositionComponent and just add a PositionComponent to the 50 visible objects which value is simply copied from your own CustomPositionComponent
    c) use the frozen component, as you mentioned you're already doing. Not ideal but should work as a workaround.

    I'll probably be doing something like this myself if I run into performance issues down the track
     
    Last edited: Oct 19, 2018
  5. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    I actually ran into a similar issue right after posting here.

    Apart from not wanting to update off screen entities, I don't want units in fog of war to be rendered but I didn't really want to keep adding and removing the MeshInstanceRenderer, for performance issues and I'd have to track the mesh, material etc on the entity.

    I wrote a simple hidden system that does similar to what you mentioned on the original post (toggling Frozen), except it also removes the LocalToWorld component.

    If anyone needs something similar, here was my solution. Basically just add the Hidden component to any entity and it will no longer be updated in the TransformSystem or be rendered by the MeshInstanceRenderer.

    System

    Code (CSharp):
    1. // --------------------------------------------------------------------------------------------------------------------
    2. // <copyright file="HiddenSystem.cs" >
    3. //     Copyright (c) Timothy Raines. All rights reserved.
    4. // </copyright>
    5. // --------------------------------------------------------------------------------------------------------------------
    6.  
    7. namespace BovineLabs.Toolkit.Systems.Hidden
    8. {
    9.     using JetBrains.Annotations;
    10.  
    11.     using Unity.Collections;
    12.     using Unity.Entities;
    13.     using Unity.Jobs;
    14.     using Unity.Rendering;
    15.     using Unity.Transforms;
    16.  
    17.     /// <summary>
    18.     /// System that disables entities from updating in the <see cref="TransformSystem"/> and stops them rendering in the <see cref="MeshInstanceRendererSystem"/>
    19.     /// </summary>
    20.     [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
    21.     public class HiddenSystem : JobComponentSystem
    22.     {
    23.         /// <summary>
    24.         /// Barrier used by the <see cref="HiddenSystem"/>
    25.         /// </summary>
    26.         [UsedImplicitly(ImplicitUseKindFlags.Assign)]
    27.         [Inject] private HiddenSystemBarrier barrier;
    28.  
    29.         /// <summary> System update </summary>
    30.         /// <param name="handle"> Previous system dependency handle </param>
    31.         /// <returns> Handle from the <see cref="HiddenSystem"/>. </returns>
    32.         protected override JobHandle OnUpdate(JobHandle handle)
    33.         {
    34.             var hideJob = new HideJob
    35.                               {
    36.                                   EntityCommandBuffer = this.barrier.CreateCommandBuffer().ToConcurrent(),
    37.                                   Frozen = this.GetComponentDataFromEntity<Frozen>()
    38.                               };
    39.             var hideHandle = hideJob.Schedule(this);
    40.  
    41.             var showJob = new ShowJob { EntityCommandBuffer = this.barrier.CreateCommandBuffer().ToConcurrent() };
    42.             var showHandle = showJob.Schedule(this, hideHandle);
    43.  
    44.             return showHandle;
    45.         }
    46.  
    47.         /// <summary>
    48.         /// Hide entities that have been marked with <see cref="Hidden"/> component.
    49.         /// </summary>
    50.         [RequireComponentTag(typeof(Hidden), typeof(LocalToWorld))]
    51.         private struct HideJob : IJobProcessComponentDataWithEntity<LocalToWorld>
    52.         {
    53.             /// <summary>
    54.             /// The entity command buffer.
    55.             /// </summary>
    56.             public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    57.  
    58.             /// <summary>
    59.             /// Frozen component data map.
    60.             /// </summary>
    61.             [ReadOnly] public ComponentDataFromEntity<Frozen> Frozen;
    62.  
    63.             /// <summary> Job Execution </summary>
    64.             /// <param name="entity"> The entity. </param>
    65.             /// <param name="index"> The index. </param>
    66.             /// <param name="localToWorld">
    67.             /// Not needed, will remove when unity adds a zero variant IJobProcessComponentDataWithEntity
    68.             /// </param>
    69.             public void Execute(Entity entity, int index, [ReadOnly] ref LocalToWorld localToWorld)
    70.             {
    71.                 if (!this.Frozen.Exists(entity))
    72.                 {
    73.                     this.EntityCommandBuffer.AddComponent(index, entity, default(Frozen));
    74.                 }
    75.  
    76.                 this.EntityCommandBuffer.RemoveComponent<LocalToWorld>(index, entity);
    77.             }
    78.         }
    79.  
    80.         /// <summary>
    81.         /// Hide entities that have been marked with <see cref="Hidden"/> component.
    82.         /// </summary>
    83.         [RequireComponentTag(typeof(Frozen))]
    84.         [RequireSubtractiveComponent(typeof(Hidden), typeof(LocalToWorld))]
    85.         private struct ShowJob : IJobProcessComponentDataWithEntity<Frozen>
    86.         {
    87.             /// <summary>
    88.             /// The entity command buffer.
    89.             /// </summary>
    90.             public EntityCommandBuffer.Concurrent EntityCommandBuffer;
    91.  
    92.             /// <summary> Job Execution </summary>
    93.             /// <param name="entity"> The entity. </param>
    94.             /// <param name="index"> The index. </param>
    95.             /// <param name="frozen">
    96.             /// Not needed, will remove when unity adds a zero variant IJobProcessComponentDataWithEntity
    97.             /// </param>
    98.             public void Execute(Entity entity, int index, [ReadOnly] ref Frozen frozen)
    99.             {
    100.                 this.EntityCommandBuffer.RemoveComponent<Frozen>(index, entity);
    101.             }
    102.         }
    103.  
    104.         /// <summary>
    105.         /// The barrier for the Hidden System. Ensure updates before BarrierSystem to avoid conflicts with Frozen
    106.         /// </summary>
    107.         [UpdateBefore(typeof(TransformSystem))]
    108.         [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
    109.         private class HiddenSystemBarrier : BarrierSystem
    110.         {
    111.         }
    112.     }
    113. }
    Component

    Code (CSharp):
    1. // --------------------------------------------------------------------------------------------------------------------
    2. // <copyright file="Components.cs" >
    3. //     Copyright (c) Timothy Raines. All rights reserved.
    4. // </copyright>
    5. // <summary>
    6. //   Components for the hidden system
    7. // </summary>
    8. // --------------------------------------------------------------------------------------------------------------------
    9.  
    10. namespace BovineLabs.Toolkit.Systems.Hidden
    11. {
    12.     using Unity.Entities;
    13.  
    14.     /// <summary>
    15.     ///     Mark a mesh instance renderer as being hidden
    16.     /// </summary>
    17.     public struct Hidden : IComponentData
    18.     {
    19.     }
    20. }
     
    T-Zee likes this.
  6. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    962
    Forgot to add watch thread, thanks for the responses.

    The way I see it the Position should be just that. Not every entity with a position is rendered, it could be an invisible entity just for simulation and from a coding perspective I see some problems with adding different types of Position components that get handled differently. So I don't consider having a custom Position as a solution. That's not why I use ECS in the first place. Compatibility between components and systems is something I value very much.

    The control should be in the hand of the developers so I'd either:
    - remove the auto dependency of Positon <-> LocalToWorld so we don't have to remove it afterwards like tertle does.
    - add a moving component and get rid of the negate condition case and make clearer system groups
    - add a UpdateWorldPosition comp so the TransformSystem knows that it has to update/add LocalToWorld to any entity that has Position and UpdateWorldPosition

    For anything that has to do with Positions or Matrices, ref return would be great. The overhead is quite drastic right now when matrices have to be calculated after position updates. It could be merged into a single update when done right.
     
  7. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Have you tried adding the Frozen tag component? That should make it so that the TransfromSystem completely ignores the entity and you can still use the same component types & data in your game code.
     
    laurentlavigne and FROS7 like this.
  8. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    962
    Yes Joachim! I use the Frozen comp as a workaround as stated in my first post.
    Frozen doesn't mean the Position is not changing, just the LocalToWorld matrix not being updated by the TransformSystem, right?

    I don't want to open a can of worms because things could always work differently depending on the perspective you are looking at but I don't think the Frozen comp or having subtractive comps in a group for a very low level single-responsible system is good design.

    I'd prefer systems running on more additive comps in a group than subtractive one.
    Adding flag comps so they don't run somewhere is a design that's in the same vein of if (!isDeactivated)
    It's backwards, hard to understand and hard to maintain when working in a group.

    At least in my opinion, am I off here?
    Do you think adding Frozen is a good solution for my case?
    Why is the TransformSystem adding LocalToWorld to entities with Position? Out of convenience or is there more reason?

    Ideal would be if I could add the meshrenderer comp to all those items and it would still work. But the framerate tanks with 1 million items and I don't think the approach is reasonable.

    I take the complexity of Factorio as example. There, the position of items is needed for other
    sytems like the inserter to work and those could happen far away while not being rendered.
     
    Last edited: Oct 22, 2018
  9. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Yes looks like a perfectly fine solution to me.

    Because thats the job of the TransformSystem. Preparing a LocalToWorld matrix that other systems can use to work in global space against already prepared data.
     
  10. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    962
    I know this may sound snarky, but with Performance by Default I don't expect adding a Frozen component.
    Just be aware that this may turn into a common pitfall.

    As it stems from the problem of rendering so many entities with changing positions and the bottleneck of updating so many matrices it would be great if the InstancedMeshRenderer would be more robust so only entities in the frustum get the LocalToWorld matrices updated.

    Isn't a frustum check already going on in the TransformSystem? This path could determine which entities need a meshrenderer/frozen component.

    What are your plans or rather predictions with the InstancedMeshRenderer?
    Do you think it could get optimized so well that we devs don't have to optimize?
     
  11. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Thats just not how that works. We can talk about the need to make TransformSystem to run a bit faster. But the concept of not using a matrix to calculate the world space bounding volume in order to perform culling, is just not realistic as a general purpose solution. You can always make specialized solutions for getting better perf in specific cases. Adding the frozen component is a simple way of doing it.