Unity, ECS, Actors: how to raise FPS in your game ten times, when there’s nothing to optimize [with edits]

What is ECS

What is Actors



I have often heard how good the ECS template is, and that Jobs and Burst from the Unity library are the solution to all performance issues. In order not to add the word “probably” and “maybe” every time, discussing the speed of the code, I decided to check everything personally.



My goal was to make an open mind about how fast this development tool is and whether to use parallelization for calculations. And if it is, is it better to use Unity.Jobs or System.Threading ? At the same time I found out what is the use of ECS in real tasks.





Test conditions (close to real game tasks):





Testing Positions and Update Code



  1. MonoBehaviour sequential (conditional marking).

    The MonoBehaviour script is "hung" on the object, in the update of which the position, the collider is calculated and the self is moved.



    Update code
    void Update() { //    var velocityToOneFrame = velocityToOneSecond * Time.deltaTime; observedDistance += velocityToOneFrame; var t = observedDistance / distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(posToMove.c0, posToMove.c2,posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); //     tr.position = new Vector3(newPos.x, newPos.y); #if UNITY_EDITOR DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); #endif }
          
          





  2. Actors sequential on component classes without parallelization.



    Update code
     public void Tick(float delta) { foreach (ent entity in groupMoveBezier) { var cMoveBezier = entity.ComponentMoveBezier_noJob(); var cObject = entity.ComponentObject(); ref var obj = ref cObject.obj; //    var velocityToOneFrame = cMoveBezier.velocityToOneSecond * delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2,cMoveBezier.posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); //     cObject.tr.position = new Vector3(newPos.x, newPos.y, 0); #if UNITY_EDITOR DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); #endif } }
          
          





  3. Actors + Jobs + Burst



    Calculation and movement in Jobs from Unity.Jobs 0.1.1, Unity.Burst 1.1.2 libraries.

    Safety Checks - off

    Editor Attaching - off

    JobsDebbuger - off

    For normal operation of IJobParallelForTransform, all movable objects have a “parent object” (up to 255 pieces of objects in each “parent” according to the recommendation for maximum performance).

    Update code
      public void Tick(float delta) { if (index <= 0) return; handlePositionUpdate.Complete(); #if UNITY_EDITOR for (var i = 0; i < index; i++) { var obj = nObj[i]; DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); } #endif jobPositionUpdate.nSetMove = nSetMove; jobPositionUpdate.nObj = nObj; jobPositionUpdate.deltaTime = delta; handlePositionUpdate = jobPositionUpdate.Schedule(transformsAccessArray); } } [BurstCompile] struct JobPositionUpdate : IJobParallelForTransform { public NativeArray<SetMove> nSetMove; public NativeArray<Obj> nObj; [Unity.Collections.ReadOnly] public float deltaTime; public void Execute(int index, TransformAccess transform) { var setMove = nSetMove[index]; var velocityToOneFrame = nSetMove[index].velocityToOneSecond * deltaTime; //    setMove.observedDistance += velocityToOneFrame; var t = setMove.observedDistance / setMove.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(setMove.posToMove.c0, setMove.posToMove.c2,setMove.posToMove.c1); nSetMove[index] = setMove; //   var obj = nObj[index]; obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); nObj[index] = obj; //     transform.position = (Vector2) newPos; } } public struct SetMove { public float2x3 posToMove; public float distanceFull; public float velocityToOneSecond; public float observedDistance; }
          
          



  4. Actors + Parallel.For



    Instead of the usual For loop over a group of moving entities, Parallel.For is used from the System.Threading.Tasks library. It calculates the new position and the collider in parallel flows. Moving an object is carried out in a neighboring group.



    Update code
      public void Tick(float delta) { Parallel.For(0, groupMoveBezier.length, i => { ref var entity = ref groupMoveBezier[i]; var cMoveBezier = entity.ComponentMoveBezier_actorsParallel(); ref var obj = ref entity.ComponentObject().obj; //    var velocityToOneFrame = cMoveBezier.velocityToOneSecond * delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2,cMoveBezier.posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox1.posAndSize.c1 }; obj.collBox1 = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); }); //     foreach (ent entity1 in groupMoveBezier) { var cObject = entity1.ComponentObject(); cObject.tr.position = new Vector3(cObject.obj.properties.c0.x, cObject.obj.properties.c0.y, 0); #if UNITY_EDITOR DebugDrowBox(cObject.obj.collBox1, Color.blue, Time.deltaTime); #endif } }
          
          





Testing with moving [1]:



500 objects





(a picture from the editor near the text with FPS to show what is visually happening there)



  1. MonoBehaviour sequential:



  2. Actors sequential:



  3. Actors + Jobs + Burst:



  4. Actors + Parallel.For:





5000 objects







  1. MonoBehaviour sequential:



  2. Actors sequential:



  3. Actors + Jobs + Burst:



  4. Actors + Parallel.For:







50,000 objects





  1. MonoBehaviour sequential:



  2. Actors sequential:



  3. Actors + Jobs + Burst:



  4. Actors + Parallel.For:





Actors + Threaded (built in Actors parallelization on System.Threading)



Actors has the ability to hold all components of the game in structures instead of classes. This is hemorrhoids in terms of writing code, but under such conditions, the program works more with the stack, rather than with the managed heap, which significantly affects its speed.



Update code
  public void Tick(float delta) { groupMoveBezier.Execute(delta); for (int i = 0; i < groupMoveBezier.length; i++) { ref var cObject = ref groupMoveBezier.entities[i].ComponentObject(); cObject.tr.position = new Vector3(cObject.obj.properties.c0.x, cObject.obj.properties.c0.y, 0); #if UNITY_EDITOR DebugDrowBox(cObject.obj.collBox, Color.blue, Time.deltaTime); #endif } } static void HandleCalculation(SegmentGroup segment) { for (int i = segment.indexFrom; i < segment.indexTo; i++) { ref var entity = ref segment.source.entities[i]; ref var cMoveBezier = ref entity.ComponentMoveBezier(); ref var cObject = ref entity.ComponentObject(); ref var obj = ref cObject.obj; //    var velocityToOneFrame = cMoveBezier.velocityToOneSecond * segment.delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2, cMoveBezier.posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); } }
      
      







on class components



on structure components



In this case, we get + 10% to FPS, but in the example there are only two component-structures, and not tens, as it should be in the final product. Non-linear growth of FPS is possible here as the components of the reference types program are replaced by value types .



Conclusion





[1] I do not know what the performance is in other ECS frameworks, in ECS-Unity / DOTS systems.



Test source
Repository





Thanks to Oleg Morozov (BenjaminMoore) for editing on jobs, adding SceneSelector and a new fps counter.

Thanks to iurii zakipnyi for the instructions, revisions, and the additional test Actors + Jobs + Burst [four Job]



All Articles