Unity,ECS,Actor:在没有任何要优化的情况下,如何在游戏中提高FPS十倍[编辑]

什么是ECS
什么是演员

我经常听到ECS模板有多好, Unity库中的JobsBurst是解决所有性能问题的方法。 为了避免每次都在讨论代码速度时添加“可能”和“也许”一词,我决定亲自检查所有内容。

我的目标是对这个开发工具的速度以及是否使用并行化进行计算持开放态度。 如果是的话,最好使用Unity.JobsSystem.Threading吗? 同时,我发现了ECS在实际任务中的用途是什么。


测试条件(接近真实游戏任务):

  • i5 2500处理器(无超级交易的4核)和Unity2019.3.0f1
  • 每个游戏对象每一帧...

    A)从起点到终点沿二次Bezier曲线移动10分钟。

    B)计算其方形对撞机(方框10f10f),该对撞机使用math.sincos,math.asin,math.sqrt(所有测试都使用相同,非常复杂的计算方法)。
  • FPS测量之前的对象被设置在720fx1280f区域内的随机位置,然后移动到该区域内的随机点。
  • 一切都在PC上的IL2CPP版本中进行了测试
  • 测试会在启动后几秒钟记录下来,因此所有开始的初步计算和包含Unity系统都不会影响FPS。 出于相同的原因,仅显示每个帧的更新代码。
  • 对象在发行版中没有视觉显示,因此渲染不会影响FPS。

测试位置和更新代码


  1. MonoBehaviour顺序 (条件标记)。
    MonoBehaviour脚本“挂”在对象上,在更新位置时,将计算对撞机并移动自身。

    更新代码
    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. Actor在组件类上是顺序的 ,无需并行化。

    更新代码
     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. 演员+工作+爆发

    来自Unity.Jobs 0.1.1,Unity.Burst 1.1.2库的Jobs中的计算和移动。
    安全检查-关闭
    编辑器附加-关闭
    JobsDebbuger-关闭
    对于IJobParallelForTransform的正常运行,所有可移动对象都具有一个“父对象”(根据最大性能的建议,每个“父”对象中最多255个对象)。
    更新代码
      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. 演员+平行

    System.Threading.Tasks库中使用Parallel.For,而不是通常通过一组移动实体的For循环。 它计算新位置和平行流中的对撞机。 在相邻组中移动对象。

    更新代码
      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 } } 

移动测试[1]:


500物件



(来自编辑器的图片,靠近带有FPS的文字,以显示在那里视觉发生的变化)

  1. MonoBehaviour顺序:

  2. 演员顺序:

  3. 演员+工作+突发:

  4. 演员+平行演员


5000个对象




  1. MonoBehaviour顺序:

  2. 演员顺序:

  3. 演员+工作+突发:

  4. 演员+平行演员



50,000个对象



  1. MonoBehaviour顺序:

  2. 演员顺序:

  3. 演员+工作+突发:

  4. 演员+平行演员


Actors + Threaded(在System.Threading上的Actors并行化中内置)


演员具有将游戏的所有组件保持在结构而不是类中的能力。 就编写代码而言,这是更多的痔疮,但是在这种情况下,程序更多地使用堆栈,而不是托管堆,这大大影响了它的速度。

更新代码
  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()); } } 


上课组件

在结构组件上

在这种情况下,我们的FPS为+ 10%,但是在示例中,只有两个组件结构,而不是十,因为最终产品中应该是十。 FPS的非线性增长在这里是可能的,因为引用类型程序的组件已被值类型替代。

结论


  • 在所有情况下,没有平行的Actor中的FPS大约增加两倍,并且与MonoBehaviour顺序相比增加三倍。 随着数学计算的增加,这些比例仍然存在。
  • 对我而言,与MonoBehaviour顺序相比,ECS Actor的另一个优势是,基本增加了计算的并行性,这增加了速度。
  • 与MonoBehaviour顺序相比,使用Actor + Jobs + Burst可将FPS提升约十倍
  • 诚然,FPS的增加主要是由于爆裂。 当然,为了使其正常运行,您需要使用Unity.Mathematics中的数据类型(例如,将Vector3替换为float3)
    这非常重要:在我的处理器上,屏幕上有50,000个对象,以提高FPS, 之前
    必须注意以下几点:
    1)如果在计算过程中可以不使用库,则最好不要使用它(红色标记-不好,绿色-很好)

    2)您不能使用Mathf库-只能使用数学,否则,burst将无法向量化和处理数据。

  • 从多个第三方测试来看,按顺序具有50,000个对象的MonoBehavior到处都显示相同的〜50fps。 但是在Actor + Jobs或Threaded上的工作是非常不同的。
    而且,处理器越现代化,将工作分解成几个“排队”的工作就越有用:位置计算,对撞机,移动到某个位置。
    您可以下载测试程序,并将Actor + Jobs + Burst [一个作业]与Actors + Jobs + Burst [四个作业]的工作进行比较。 (在没有超级交易的四核处理器上,第一次测试的速度为-0.2毫秒,处理50,000个对象)
  • ECS的有效性取决于其他元素的数量(渲染,Unity物理等)。

[1]我不知道其他ECS框架在ECS-Unity / DOTS系统中的性能如何。

测试源

感谢Oleg Morozov(BenjaminMoore)对作业进行编辑,添加了SceneSelector和新的fps计数器。
感谢iurii zakipnyi的指导,修订和其他测试演员+工作+爆裂[四个工作]

Source: https://habr.com/ru/post/zh-CN478824/


All Articles