الوحدة ، ECS ، الممثلون: كيفية رفع FPS في لعبتك عشر مرات ، عندما لا يكون هناك شيء لتحسين [مع التعديلات]

ما هو ECS
ما هي الجهات الفاعلة

لقد سمعت غالبًا عن مدى جودة قالب ECS ، وأن Jobs and Burst من مكتبة Unity هما الحل لجميع مشكلات الأداء. من أجل عدم إضافة كلمة "ربما" و "ربما" في كل مرة ، ومناقشة سرعة الكود ، قررت فحص كل شيء شخصيًا.

كان هدفي هو جعل عقلي منفتحًا حول مدى سرعة استخدام أداة التطوير هذه وما إذا كان يجب استخدام التوازي لإجراء العمليات الحسابية. وإذا كان الأمر كذلك ، هل من الأفضل استخدام Unity.Jobs أو System.Threading ؟ في الوقت نفسه ، اكتشفت ما هو استخدام ECS في المهام الحقيقية.


شروط الاختبار (قريبة من مهام اللعبة الحقيقية):

  • المعالج i5 2500 (4 مراكز بدون تداول مفرط) و Unity2019.3.0f1
  • كل GameObject كل إطار ...

    A) يتحرك على طول منحنى Bezier التربيعي لمدة 10 دقائق من نقطة البداية إلى النهاية.

    B) يحسب مصادمه المربع (المربع 10 f10f) ، والذي يستخدم math.sincos و math.asin و math.sqrt (نفس العمليات الحسابية المعقدة جدًا لجميع الاختبارات).
  • يتم تعيين الكائنات قبل قياسات FPS في مواضع عشوائية داخل منطقة 720fx1280f والانتقال إلى نقطة عشوائية في هذه المنطقة.
  • يتم اختبار كل شيء في الإصدار في IL2CPP على جهاز الكمبيوتر
  • يتم تسجيل الاختبارات بعد ثوانٍ قليلة من بدء التشغيل ، بحيث لا تؤثر جميع عمليات بدء الحسابات الأولية وإدراج أنظمة الوحدة على 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. الجهات الفاعلة متتابعة على فئات المكون دون موازاة.

    تحديث الرمز
     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.
    فحوصات السلامة -
    إرفاق محرر - قبالة
    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. الممثلون + الموازي

    بدلاً من المعتاد بالنسبة للحلقة من خلال مجموعة من الكيانات المتحركة ، يتم استخدام Parallel.For من مكتبة System.Threading.Tasks. وتحسب الموقف الجديد والمصادم في التدفقات المتوازية. يتم نقل كائن في مجموعة مجاورة.

    تحديث الرمز
      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. الممثلون + الموازي.



50000 الكائنات



  1. MonoBehaviour متتابعة:

  2. الجهات الفاعلة متتابعة:

  3. ممثلون + وظائف + انفجار:

  4. الممثلون + الموازي.


الفاعلون + الخيوط (مضمنة في التوازي مع الممثلين على System.Threading)


يتمتع الممثلون بالقدرة على الاحتفاظ بجميع مكونات اللعبة في الهياكل بدلاً من الفصول الدراسية. هذا هو البواسير من وجهة نظر كتابة التعليمات البرمجية ، ولكن في ظل هذه الظروف ، يعمل البرنامج بشكل أكبر مع المكدس ، بدلاً من كومة الذاكرة المؤقتة التي تتم إدارتها ، مما يؤثر بشكل كبير على سرعة عمله.

تحديث الرمز
  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()); } } 


على مكونات الطبقة

على مكونات الهيكل

في هذه الحالة ، نحصل على + 10٪ إلى FPS ، ولكن في المثال ، هناك هيكلان فقط للمكونات ، وليس العشرات ، كما يجب أن يكون في المنتج النهائي. يمكن تحقيق نمو غير خطي لـ FPS هنا حيث يتم استبدال مكونات برنامج الأنواع المرجعية بأنواع القيم .

استنتاج


  • في جميع الحالات ، يزداد معدل FPS في الفاعلين بدون Parallel.For بحوالي مرتين ، ومعه - بمقدار ثلاث مرات مقارنة بالتسلسل MonoBehavior. مع الزيادة في الحسابات الرياضية ، تبقى هذه النسب.
  • بالنسبة لي ، هناك ميزة إضافية لـ ECS Actors على تسلسل MonoBehaviour وهي إضافة موازنة الحسابات ، إضافة إلى السرعة ، بشكل أساسي.
  • يؤدي استخدام Actors + Jobs + Burst إلى زيادة FPS بحوالي عشرة أضعاف مقارنة بتسلسل MonoBehaviour
  • من المسلم به أن هذه الزيادة في FPS ترجع إلى حد كبير إلى الاندفاع. بالطبع ، لتشغيلها العادي ، تحتاج إلى استخدام أنواع البيانات من Unity.Mathematics (على سبيل المثال ، استبدل Vector3 بـ float3)
    وهذا مهم للغاية: على المعالج الخاص بي مع 50000 كائن على الشاشة لرفع FPS مع إلى !
    يجب مراعاة النقاط التالية:
    1) إذا كان في الحسابات يمكنك الاستغناء عن المكتبة ، فمن الأفضل عدم استخدامها (علامة حمراء - سيئة ، خضراء - جيدة)

    2) لا يمكنك استخدام مكتبة Mathf - فالرياضيات فقط ، وإلا فإن الاندفاع لن يكون قادرًا على توجيه البيانات ومعالجتها.

  • إذا حكمنا من خلال العديد من اختبارات الجهات الخارجية ، فإن تسلسل MonoBehavior الذي يحتوي على 50000 كائن يظهر في حدود 50 إطارًا في الثانية في كل مكان. لكن العمل على Actors + Jobs أو Threaded مختلف تمامًا.
    أيضًا ، كلما كان المعالج أكثر حداثة ، كلما كان من المفيد تقسيم العمل إلى عدة وظائف "قائمة الانتظار": حساب الموضع ، المصادم ، الانتقال إلى الموضع.
    يمكنك تنزيل برنامج اختبار ومقارنة عمل Actors + Jobs + Burst [one job] مع Actors + Jobs + Burst [four Job]. (على المعالج رباعي النواة بدون تداول مفرط ، يكون الاختبار الأول أسرع -0.2 مللي ثانية مع 50000 كائن)
  • تعتمد فعالية ECS على عدد العناصر الإضافية (التجسيد ، فيزياء الوحدة ، وما إلى ذلك).

[1] لا أعرف ما هو الأداء في أطر ECS الأخرى ، في أنظمة ECS-Unity / DOTS.

مصدر الاختبار

بفضل Oleg Morozov (BenjaminMoore) لتحريره على الوظائف ، وإضافة SceneSelector وعداد fps جديد.
بفضل iurii zakipnyi للحصول على التعليمات والمراجعات والاختبار الإضافي Actors + Jobs + Burst [four Job]

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


All Articles