→
Qu'est-ce que ECS→
Qu'est-ce que les acteursJ'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
- 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é.
- 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;
- 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; }
- 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;
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)- Séquentiel MonoBehaviour:

- Acteurs séquentiels:

- Acteurs + Emplois + Rafale:

- Acteurs + Parallèle Pour:

5000 objets

- Séquentiel MonoBehaviour:

- Acteurs séquentiels:

- Acteurs + Emplois + Rafale:

- Acteurs + Parallèle Pour:

50 000 objets

- Séquentiel MonoBehaviour:

- Acteurs séquentiels:

- Acteurs + Emplois + Rafale:

- 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.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]