Unidad, ECS, actores: cómo aumentar FPS en tu juego diez veces, cuando no hay nada que optimizar [con ediciones]

¿Qué es ECS?
¿Qué son los actores?

A menudo he escuchado lo buena que es la plantilla ECS , y que Jobs y Burst de la biblioteca de Unity son la solución a todos los problemas de rendimiento. Para no agregar la palabra "probablemente" y "tal vez" cada vez, discutiendo la velocidad del código, decidí verificar todo personalmente.

Mi objetivo era hacer una mente abierta sobre la rapidez de esta herramienta de desarrollo y si usar la paralelización para los cálculos. Y si es así, ¿es mejor usar Unity.Jobs o System.Threading ? Al mismo tiempo, descubrí cuál es el uso de ECS en tareas reales.


Condiciones de prueba (cercanas a las tareas reales del juego):

  • El procesador i5 2500 (4 núcleos sin hipercomercio) y Unity2019.3.0f1
  • Cada juego Objeto cada cuadro ...

    A) se mueve a lo largo de una curva de Bezier cuadrática durante 10 minutos desde el punto inicial hasta el final.

    B) calcula su colisionador cuadrado (recuadro 10f10f), que utiliza math.sincos, math.asin, math.sqrt (los mismos cálculos bastante complicados para todas las pruebas).
  • Los objetos antes de las mediciones FPS se establecen en posiciones aleatorias dentro de la zona 720fx1280f y se mueven a un punto aleatorio en esta zona.
  • Todo se prueba en el lanzamiento en IL2CPP en PC
  • Las pruebas se registran unos segundos después del lanzamiento, para que todos los cálculos preliminares iniciales y la inclusión de los sistemas Unity no afecten a FPS. Por las mismas razones, solo se muestra el código de actualización de cada cuadro.
  • Los objetos no tienen una pantalla visual en la versión, por lo que la representación no afecta a FPS.

Posiciones de prueba y código de actualización


  1. MonoBehaviour secuencial (marcado condicional).
    El script MonoBehaviour se "cuelga" en el objeto, en cuya actualización se calcula la posición, se calcula el colisionador y se mueve el self.

    Actualizar código
    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. Actores secuenciales en clases de componentes sin paralelización.

    Actualizar código
     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. Actores + Trabajos + Explosión

    Cálculo y movimiento en trabajos de las bibliotecas Unity.Jobs 0.1.1, Unity.Burst 1.1.2.
    Verificaciones de seguridad - desactivado
    Editor Adjunto - apagado
    JobsDebbuger - desactivado
    Para el funcionamiento normal de IJobParallelForTransform, todos los objetos en movimiento tienen un "objeto padre" (hasta 255 piezas de objetos en cada "padre" según la recomendación para un rendimiento máximo).
    Actualizar código
      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. Actores + Paralelo.

    En lugar del bucle For habitual a través de un grupo de entidades en movimiento, se utiliza Parallel.For de la biblioteca System.Threading.Tasks. Calcula la nueva posición y el colisionador en flujos paralelos. Mover un objeto se lleva a cabo en un grupo vecino.

    Actualizar código
      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 } } 

Prueba con movimiento [1]:


500 objetos



(una imagen del editor cerca del texto con FPS para mostrar lo que está sucediendo visualmente allí)

  1. MonoBehaviour secuencial:

  2. Actores secuenciales:

  3. Actores + Trabajos + Explosión:

  4. Actores + Paralelo. Para:


5000 objetos




  1. MonoBehaviour secuencial:

  2. Actores secuenciales:

  3. Actores + Trabajos + Explosión:

  4. Actores + Paralelo. Para:



50,000 objetos



  1. MonoBehaviour secuencial:

  2. Actores secuenciales:

  3. Actores + Trabajos + Explosión:

  4. Actores + Paralelo. Para:


Actores + Rosca (paralelos de actores incorporados en el sistema.


Los actores tienen la capacidad de mantener todos los componentes del juego en estructuras en lugar de clases. Se trata de hemorroides desde el punto de vista de escribir código, pero en tales condiciones el programa trabaja más con la pila, en lugar de con el montón administrado, lo que afecta significativamente la velocidad de su trabajo.

Actualizar código
  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()); } } 


en componentes de clase

en componentes de estructura

En este caso, obtenemos + 10% a FPS, pero en el ejemplo solo hay dos estructuras de componentes, y no decenas, como debería ser en el producto final. El crecimiento no lineal de FPS es posible aquí ya que los componentes del programa de tipos de referencia se reemplazan por tipos de valor .

Conclusión


  • En todos los casos, el FPS en Actores sin paralelo. Para aumenta en aproximadamente dos veces, y con él, en tres veces en comparación con el secuencial MonoBehavior. Con el aumento de los cálculos matemáticos, estas proporciones permanecen.
  • Para mí, una ventaja adicional de los actores de ECS sobre el secuencial MonoBehaviour es que la paralelización de los cálculos, agregando a la velocidad, se agrega de manera elemental.
  • El uso de Actores + Trabajos + Ráfaga aumenta el FPS en aproximadamente diez veces en comparación con el secuencial MonoBehaviour
  • Es cierto que tal aumento en FPS se debe en gran parte a la explosión. Por supuesto, para su funcionamiento normal, debe usar tipos de datos de Unity.Mathematics (por ejemplo, reemplazar Vector3 con float3)
    Y es muy importante: en mi procesador con 50,000 objetos en la pantalla para aumentar FPS con antes !
    Deben observarse los siguientes puntos:
    1) Si en los cálculos puede prescindir de una biblioteca, es mejor no usarla (marcador rojo - malo, verde - bueno)

    2) No puede usar la biblioteca Mathf, solo matemáticas, de lo contrario, burst no podrá vectorizar y procesar los datos.

  • A juzgar por varias pruebas de terceros, MonoBehavior secuencial con 50,000 objetos muestra los mismos ~ 50 fps en todas partes. Pero trabajar en Actors + Jobs o Threaded es muy diferente.
    Además, cuanto más moderno es el procesador, más útil es dividir el trabajo en varios trabajos "en cola": cálculo de posición, colisionador, movimiento a una posición.
    Puede descargar un programa de prueba y comparar el trabajo de Actores + Trabajos + Ráfaga [un trabajo] con Actores + Trabajos + Ráfaga [cuatro trabajos]. (En mi procesador de cuatro núcleos sin hipercomercio, la primera prueba es -0.2ms más rápido con 50,000 objetos)
  • La efectividad de ECS depende de la cantidad de elementos adicionales (render, física de Unity, etc.).

[1] No sé cuál es el rendimiento en otros marcos ECS, en los sistemas ECS-Unity / DOTS.

Fuente de prueba

Gracias a Oleg Morozov (BenjaminMoore) por editar trabajos, agregar SceneSelector y un nuevo contador de fps.
Gracias a iurii zakipnyi por las instrucciones, revisiones y la prueba adicional Actores + Trabajos + Explosión [cuatro trabajos]

Source: https://habr.com/ru/post/478824/


All Articles