Unity, ECS, Actors: comment augmenter dix fois le FPS dans votre jeu, quand il n'y a rien à optimiser [avec des modifications]

Qu'est-ce que ECS
Qu'est-ce que les acteurs

J'ai souvent entendu à quel point le modèle ECS est bon, et que Jobs et Burst de la bibliothèque Unity sont la solution à tous les problèmes de performances. Afin de ne pas ajouter le mot «probablement» et «peut-être» à chaque fois, en discutant de la vitesse du code, j'ai décidé de tout vérifier personnellement.

Mon objectif était de me faire une idée de la rapidité de cet outil de développement et de l'opportunité d'utiliser la parallélisation pour les calculs. Et si c'est le cas, est-il préférable d'utiliser Unity.Jobs ou System.Threading ? En même temps, j'ai découvert à quoi sert ECS dans les tâches réelles.


Conditions de test (proches des tâches réelles du jeu):

  • Le processeur i5 2500 (4 cœurs sans hyper trading) et Unity2019.3.0f1
  • Chaque GameObject chaque image ...

    A) se déplace le long d'une courbe de Bézier quadratique pendant 10 minutes du point de départ à la fin.

    B) calcule son collisionneur carré (case 10f10f), qui utilise math.sincos, math.asin, math.sqrt (les mêmes calculs assez compliqués pour tous les tests).
  • Les objets avant les mesures FPS sont placés à des positions aléatoires dans la zone 720fx1280f et se déplacent vers un point aléatoire dans cette zone.
  • Tout est testé en version IL2CPP sur PC
  • Les tests sont enregistrés quelques secondes après le lancement, de sorte que tous les calculs préliminaires de départ et l'inclusion des systèmes Unity n'affectent pas le FPS. Pour les mêmes raisons, seul le code de mise à jour de chaque trame est affiché.
  • Les objets n'ont pas d'affichage visuel dans la version afin que le rendu n'affecte pas le FPS.

Test des positions et mise à jour du code


  1. MonoBehaviour séquentiel (marquage conditionnel).
    Le script MonoBehaviour est "accroché" à l'objet, dans la mise à jour dont la position, le collisionneur est calculé et le soi est déplacé.

    Mettre à jour le 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. Acteurs séquentiels sur les classes de composants sans parallélisation.

    Mettre à jour le 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. Acteurs + Jobs + Burst

    Calcul et déplacement dans les Jobs à partir des bibliothèques Unity.Jobs 0.1.1, Unity.Burst 1.1.2.
    Contrôles de sécurité - désactivés
    Attachement de l'éditeur - désactivé
    JobsDebbuger - désactivé
    Pour un fonctionnement normal de IJobParallelForTransform, tous les objets déplacés ont un «objet parent» (jusqu'à 255 pièces d'objets dans chaque «parent» selon la recommandation pour des performances maximales).
    Mettre à jour le 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. Acteurs + Parallel.Pour

    Au lieu de la boucle For habituelle via un groupe d'entités en mouvement, Parallel.For est utilisé à partir de la bibliothèque System.Threading.Tasks. Il calcule la nouvelle position et le collisionneur en flux parallèles. Le déplacement d'un objet s'effectue dans un groupe voisin.

    Mettre à jour le 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 } } 

Test avec déplacement [1]:


500 objets



(une image de l'éditeur près du texte avec FPS pour montrer ce qui se passe visuellement là-bas)

  1. Séquentiel MonoBehaviour:

  2. Acteurs séquentiels:

  3. Acteurs + Emplois + Rafale:

  4. Acteurs + Parallèle Pour:


5000 objets




  1. Séquentiel MonoBehaviour:

  2. Acteurs séquentiels:

  3. Acteurs + Emplois + Rafale:

  4. Acteurs + Parallèle Pour:



50 000 objets



  1. Séquentiel MonoBehaviour:

  2. Acteurs séquentiels:

  3. Acteurs + Emplois + Rafale:

  4. Acteurs + Parallèle Pour:


Acteurs + Threaded (parallélisation des acteurs intégrée sur System.Threading)


Les acteurs ont la capacité de contenir toutes les composantes du jeu dans des structures au lieu de classes. Il s'agit d'hémorroïdes du point de vue de l'écriture de code, mais dans de telles conditions, le programme fonctionne plus avec la pile plutôt qu'avec le tas géré, ce qui affecte considérablement la vitesse de son travail.

Mettre à jour le 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()); } } 


sur les composants de classe

sur les composants de la structure

Dans ce cas, nous obtenons + 10% de FPS, mais dans l'exemple, il n'y a que deux structures de composants, et non des dizaines, comme cela devrait être dans le produit final. La croissance non linéaire de FPS est possible ici car les composants du programme des types de référence sont remplacés par des types de valeur .

Conclusion


  • Dans tous les cas, le FPS dans Actors without Parallel.For augmente d'environ deux fois, et avec lui - de trois fois par rapport à MonoBehaviour séquentiel. Avec l'augmentation des calculs mathématiques, ces proportions restent.
  • Pour moi, un avantage supplémentaire des ECS Actors par rapport à MonoBehaviour séquentiel est que la parallélisation des calculs, s'ajoutant à la vitesse, est ajoutée de manière élémentaire.
  • Utiliser Actors + Jobs + Burst augmente les FPS d'environ dix fois par rapport à MonoBehaviour séquentiel
  • Certes, une telle augmentation des FPS est largement due à Burst. Bien sûr, pour son fonctionnement normal, vous devez utiliser les types de données d'Unity.Mathematics (par exemple, remplacer Vector3 par float3)
    Et c'est très important: sur mon processeur avec 50 000 objets à l'écran pour augmenter le FPS avec avant !
    Les points suivants doivent être respectés:
    1) Si dans les calculs vous pouvez vous passer d'une bibliothèque, il vaut mieux ne pas l'utiliser (marqueur rouge - mauvais, vert - bon)

    2) Vous ne pouvez pas utiliser la bibliothèque Mathf - uniquement des mathématiques, sinon la rafale ne pourra pas vectoriser et traiter les données.

  • À en juger par plusieurs tests tiers, MonoBehavior séquentiel avec 50 000 objets affiche les mêmes ~ 50fps partout. Mais le travail sur Actors + Jobs ou Threaded est très différent.
    De plus, plus le processeur est moderne, plus il est utile de diviser le travail en plusieurs tâches «en file d'attente»: calcul de position, collisionneur, déplacement vers une position.
    Vous pouvez télécharger un programme de test et comparer le travail d'Acteurs + Travaux + Rafale [un travail] avec Acteurs + Travaux + Rafale [quatre travaux]. (Sur mon processeur à quatre cœurs sans hyper trading, le premier test est -0,2 ms plus rapide avec 50 000 objets)
  • L'efficacité d'ECS dépend du nombre d'éléments supplémentaires (rendu, physique Unity, etc.).

[1] Je ne sais pas quelles sont les performances dans d'autres cadres ECS, dans les systèmes ECS-Unity / DOTS.

Source de test

Merci à Oleg Morozov (BenjaminMoore) pour l'édition des travaux, l'ajout de SceneSelector et un nouveau compteur fps.
Merci à iurii zakipnyi pour les instructions, les révisions et le test supplémentaire Acteurs + Travaux + Rafale [quatre travaux]

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


All Articles