نظام العمل. نظرة عامة من الجانب الآخر

في إصدار الوحدة الجديد لعام 2018 ، أضافوا أخيرًا نظام مكون الكيان الجديد ، أو ECS باختصار ، والذي يسمح لك بالعمل فقط مع بياناتهم بدلاً من العمل المعتاد مع مكونات الكائن.

يوفر لك نظام مهام إضافي استخدام قوة حوسبة متوازية لتحسين أداء التعليمات البرمجية الخاصة بك.

يوفر هذان النظامان الجديدان ( ECS ونظام الوظائف ) معًا مستوى جديدًا من معالجة البيانات.

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

نظام جديد


في البداية ، كانت الوحدة قادرة على استخدام الحوسبة متعددة الخيوط ، ولكن كل هذا كان يجب أن يتم إنشاؤه من قبل المطور بمفرده ، لحل المشاكل بنفسه والتغلب على المزالق. وإذا كان من الضروري قبل ذلك العمل مباشرة مع أشياء مثل إنشاء سلاسل الرسائل ، وإغلاق سلاسل المحادثات ، والتجمعات ، والمزامنة ، فقد سقط كل هذا العمل الآن على أكتاف المحرك ، وكان المطور نفسه بحاجة فقط إلى إنشاء المهام وإكمالها.

المهام


لإجراء أي عمليات حسابية في النظام الجديد ، من الضروري استخدام المهام التي تكون كائنات تتكون من طرق وبيانات للحساب.

مثل أي بيانات أخرى في نظام ECS ، يتم تمثيل المهام في نظام العمل أيضًا كبنى ترث واحدة من ثلاث واجهات.

Ijob


أبسط واجهة مهمة تحتوي على أسلوب تنفيذ واحد لا يأخذ أي شيء في شكل معلمات ولا يُرجع شيئًا.

تبدو المهمة نفسها كما يلي:

Ijob
public struct JobStruct : IJob { public void Execute() {} } 


في طريقة التنفيذ ، يمكنك إجراء العمليات الحسابية اللازمة.

IJobParallelFor


واجهة أخرى بنفس طريقة التنفيذ ، والتي بدورها تقبل بالفعل فهرس المعلمات الرقمية.

IJobParallelFor
 public struct JobStruct : IJobParallelFor { public void Execute(int index) {} } 


تقدم واجهة IJobParallelFor هذه ، على عكس واجهة IJob ، تنفيذ مهمة عدة مرات وليس فقط تنفيذ ، ولكن تقسيم هذا التنفيذ إلى كتل سيتم توزيعها بين سلاسل المحادثات .

غير واضح لا تقلق بشأن هذا ، سأخبرك بالمزيد.

IJobParallelForTransform


والواجهة الأخيرة الخاصة ، والتي ، كما يوحي الاسم ، تم تصميمها للعمل مع هذه التحويلات للكائن. يحتوي أيضًا على أسلوب التنفيذ ، مع فهرس المعلمات الرقمية ومعلمة TransformAccess حيث يوجد موضع التحويل وحجمه وتدويره.

IJobParallelForTransform
 public struct JobStruct : IJobParallelForTransform { public void Execute(int index, TransformAccess transform) {} } 


نظرًا لحقيقة أنه لا يمكنك العمل مع كائنات الوحدة مباشرة في المهمة ، يمكن لهذه الواجهة معالجة تحويل البيانات فقط كبنية TransformAccess منفصلة.

تم الآن ، أنت تعرف كيف يتم إنشاء هياكل المهام ، يمكنك المتابعة إلى الممارسة.

إتمام المهمة


لنقم بإنشاء مهمة بسيطة موروثة من واجهة IJob وإكمالها . لهذا نحن بحاجة إلى أي نص بسيط MonoBehaviour وهيكل المهمة نفسها.

تيستجوب
 public class TestJob : MonoBehaviour { void Start() {} } 


الآن إسقاط هذا البرنامج النصي على كائن ما على المشهد. في نفس البرنامج النصي ( TestJob ) أدناه ، سنكتب بنية المهمة ولا ننسى استيراد المكتبات اللازمة.

Simplejob
 using Unity.Jobs; public struct SimpleJob : IJob { public void Execute() { Debug.Log("Hello parallel world!"); } } 


في طريقة التنفيذ ، على سبيل المثال ، قم بطباعة سطر بسيط إلى وحدة التحكم.

الآن دعنا ننتقل إلى طريقة البدء في البرنامج النصي TestJob ، حيث سننشئ مثيلًا للمهمة ثم ننفذها .

تيستجوب
 public class TestJob : MonoBehaviour { void Start() { SimpleJob job = new SimpleJob(); job.Schedule().Complete(); } } 


إذا فعلت كل شيء كما في المثال ، فعند بدء اللعبة ستحصل على رسالة بسيطة لوحدة التحكم كما في الصورة.

الصورة

ما يحدث هنا: بعد استدعاء طريقة الجدول ، يضع المجدول المهمة في المقبض ويمكن الآن إكمالها عن طريق استدعاء الطريقة كاملة .

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

البيانات في المهمة


كما هو الحال في نظام ECS ، في المهام لا يوجد وصول إلى كائنات الوحدة ، لا يمكنك إدخال GameObject في المهمة وتغيير اسمها هناك. كل ما يمكنك فعله هو نقل بعض معلمات الكائن المنفصلة إلى المهمة ، وتغيير هذه المعلمات ، وبعد إكمال المهمة ، قم بتطبيق هذه التغييرات مرة أخرى على الكائن.

هناك العديد من القيود على البيانات في المهمة نفسها: أولاً ، يجب أن تكون هياكل ، وثانيًا ، يجب ألا تكون أنواع بيانات قابلة للتحويل ، أي أنه لا يمكنك تمرير نفس القيمة المنطقية أو السلسلة إلى المهمة.

Simplejob
 public struct SimpleJob : IJob { public float a, b; public void Execute() { float result = a + b; Debug.Log(result); } } 


والشرط الرئيسي: لا يمكن الوصول إلى البيانات غير المرفقة في الحاوية إلا داخل المهمة!

حاويات


عند العمل مع الحوسبة متعددة الخيوط ، هناك حاجة لتبادل البيانات بطريقة أو بأخرى بين سلاسل المحادثات. من أجل أن تكون قادرًا على نقل البيانات إليها وقراءتها مرة أخرى في نظام المهام ، فهناك حاويات لهذه الأغراض. يتم تقديم هذه الحاويات في شكل هياكل عادية وأعمل على مبدأ الجسر الذي يتم من خلاله مزامنة البيانات الأولية بين التدفقات.

هناك عدة أنواع من الحاويات:
NativeArray . يتم تقديم أبسط أنواع الحاويات وأكثرها استخدامًا كمصفوفة بسيطة ذات حجم ثابت.
شريحة أصلية . حاوية أخرى - صفيف ، كما هو واضح من الترجمة ، مصمم لقص NativeArray إلى قطع.

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

NativeList . إنها قائمة بيانات منتظمة.
NativeHashMap . تمثيلي لقاموس بمفتاح وقيمة.
NativeMultiHashMap . نفس NativeHashMap مع قيم قليلة فقط تحت مفتاح واحد.
قائمة الانتظار الأصلية قائمة قوائم البيانات.

نظرًا لأننا نعمل دون توصيل نظام ECS ، فإن NativeArray و NativeSlice فقط متاحان لنا .

قبل الانتقال إلى الجزء العملي ، من الضروري تحليل النقطة الأكثر أهمية - إنشاء الحالات.

إنشاء حاويات


كما قلت من قبل ، تمثل هذه الحاويات جسرًا تتم فيه مزامنة البيانات بين الخيوط. يفتح نظام المهام هذا الجسر قبل بدء العمل ويغلقه بعد اكتماله. تسمى عملية الافتتاح " التخصيص " ( التخصيص ) أو "تخصيص الذاكرة" ، وتسمى عملية الإغلاق " تحرير الموارد " ( التخلص ).

التخصيص هو الذي يحدد المدة التي يمكن للمهمة فيها استخدام البيانات الموجودة في الحاوية - بعبارة أخرى ، إلى متى سيتم فتح الجسر.

من أجل فهم هاتين العمليتين بشكل أفضل ، دعنا نلقي نظرة على الصورة أدناه.

الصورة

يعرض الجزء السفلي دورة حياة الخيط الرئيسي (الخيط الرئيسي ) ، والذي يتم حسابه في عدد الإطارات ؛ في الإطار الأول ، نقوم بإنشاء خيط مواز آخر ( خيط جديد) موجود لعدد معين من الإطارات ثم يغلق بأمان.
في نفس الموضوع الجديد تصل المهمة مع الحاوية.

الآن نلقي نظرة على الجزء العلوي من الصورة.

الصورة

يوضح الشريط الأبيض تخصيص عمر الحاوية. في الإطار الأول ، يتم تخصيص الحاوية - يتم فتح الجسر ، حتى هذه اللحظة لم تكن الحاوية موجودة ، بعد اكتمال جميع العمليات الحسابية في المهمة ، يتم تحرير الحاوية من الذاكرة وفي الإطار التاسع يتم إغلاق الجسر.

يوجد أيضًا في هذا الشريط ( التخصيص ) قطاعات زمنية ( Temp و TempJob و Presistent ) ، يوضح كل جزء هذا العمر المقدر للحاوية.

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

إذا كان لا يزال غير واضح ، فسأقوم بتحليل كل نوع من التخصيص باستخدام مثال.

الآن يمكننا أن ننتقل إلى الجزء العملي من إنشاء الحاويات ، لذلك نعود إلى طريقة البدء في البرنامج النصي TestJob وننشئ نسخة جديدة من حاوية NativeArray ولا ننسى ربط المكتبات الضرورية.

درجة الحرارة


تيستجوب
 using Unity.Jobs; using Unity.Collections; public class TestJob : MonoBehaviour { void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); } } 


لإنشاء مثيل حاوية جديد ، يجب تحديد حجم ونوع التخصيص في المُنشئ الخاص به. يستخدم هذا المثال نوع Temp ، حيث سيتم تنفيذ المهمة فقط في طريقة البدء .

قم الآن بتهيئة متغير المصفوفة نفسه بالضبط في بنية مهمة SimpleJob .

Simplejob
 public struct SimpleJob : IJob { public NativeArray<int> array; public void Execute() {} } 


تم. الآن يمكنك إنشاء المهمة نفسها وتمرير نسخة مصفوفة إليها.

ابدأ
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; } 


لتشغيل المهمة هذه المرة ، سنستخدم مقبض JobHandle الخاص بها للحصول عليها عن طريق استدعاء نفس طريقة الجدول .

ابدأ
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); } 


الآن يمكنك استدعاء الأسلوب Complete على مقبضها والتحقق مما إذا كانت المهمة مكتملة لعرض النص في وحدة التحكم.

ابدأ
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); handle.Complete(); if (handle.IsCompleted) print(" "); } 


إذا قمت بتشغيل المهمة في هذا النموذج ، فسوف تحصل بعد بدء اللعبة على خطأ أحمر فاتن قائلًا أنك لم تطلق حاوية الصفيف من الموارد بعد اكتمال المهمة.

شيء من هذا القبيل.

الصورة

لتجنب ذلك ، قم باستدعاء طريقة التخلص في الحاوية بعد إكمال المهمة.

ابدأ
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); handle.Complete(); if (handle.IsCompleted) print("Complete"); array.Dispose(); } 


ثم يمكنك إعادة تشغيله بأمان.
لكن المهمة لا تفعل شيئًا! - ثم أضف إليها بعض الإجراءات.

Simplejob
 public struct SimpleJob : IJob { public NativeArray<int> array; public void Execute() { for(int i = 0; i < array.Length; i++) { array[i] = i * i; } } } 


في أسلوب التنفيذ ، أقوم بضرب فهرس كل عنصر من عناصر الصفيف بنفسي وأكتبه مرة أخرى إلى صفيف الصفيف لطباعة النتيجة على وحدة التحكم في طريقة البدء .

ابدأ
 void Start() { NativeArray<int> array = new NativeArray<int>(10, Allocator.Temp); SimpleJob job = new SimpleJob(); job.array = array; JobHandle handle = job.Schedule(); handle.Complete(); if (handle.IsCompleted) print(job.array[job.array.Length - 1]); array.Dispose(); } 


ماذا ستكون النتيجة في وحدة التحكم إذا قمنا بطباعة العنصر الأخير من مربع المصفوفة؟

هذه هي الطريقة التي يمكنك من خلالها إنشاء حاويات ، ووضعها في المهام وتنفيذ الإجراءات عليها.

كان هذا مثالًا باستخدام نوع تخصيص Temp ، مما يعني ضمناً إكمال مهمة ضمن إطار واحد. يُفضل استخدام هذا النوع عندما تحتاج إلى إجراء عمليات حسابية بسرعة دون تحميل سلسلة المحادثات الرئيسية ، ولكن يجب أن تكون حذرًا إذا كانت المهمة معقدة للغاية أو إذا كان هناك الكثير منها ، يمكن أن يحدث ترهل ، وفي هذه الحالة من الأفضل استخدام نوع TempJob ، والذي سأقوم بتحليله لاحقًا.

Tempjob


في هذا المثال ، سوف أقوم بتعديل بنية مهمة SimpleJob قليلاً وأرثها من واجهة IJobParallelFor أخرى.

Simplejob
 public struct SimpleJob : IJobParallelFor { public NativeArray<Vector2> array; public void Execute(int index) {} } 


أيضًا ، نظرًا لأن المهمة ستعمل لفترة أطول من إطار واحد ، فسنقوم بتنفيذ وجمع نتائج المهمة في طرق الاستيقاظ والبدء المختلفة المقدمة في شكل coroutine. للقيام بذلك ، قم بتغيير مظهر فئة TestJob قليلاً .

تيستجوب
 public class TestJob : MonoBehaviour { private NativeArray<Vector2> array; private JobHandle handle; void Awake() {} IEnumerator Start() {} } 


في طريقة Awake ، سننشئ مهمة وحاوية من المتجهات ، وفي طريقة البدء ، نقوم بإخراج البيانات المستلمة وموارد التحرير.

استيقظ
 void Awake() { this.array = new NativeArray<Vector2>(100, Allocator.TempJob); SimpleJob job = new SimpleJob(); job.array = this.array; } 


هنا مرة أخرى ، يتم إنشاء حاوية صفيف بنوع التخصيص TempJob ، وبعد ذلك نقوم بإنشاء مهمة والحصول على معالجتها عن طريق استدعاء طريقة الجدول مع تغييرات طفيفة.

استيقظ
 void Awake() { this.array = new NativeArray<Vector2>(100, Allocator.TempJob); SimpleJob job = new SimpleJob(); job.array = this.array; this.handle = job.Schedule(100, 5) } 


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

ما كتل أخرى؟
سابقًا ، من أجل إكمال مهمة ، هناك سلسلة رسائل تسمى ببساطة طريقة التنفيذ مرة واحدة ، والآن من الضروري استدعاء هذه الطريقة 100 مرة ، لذلك يقوم المجدول بتقسيم هذه المرات 100 مرة من التكرارات إلى كتل يتم توزيعها بين سلاسل الرسائل حتى لا يتم تحميل أي سلسلة رسائل منفصلة. في المثال ، سيتم تقسيم مائة تكرار إلى 5 كتل من 20 تكرارًا ، أي أنه من المفترض أن يقوم المجدول بتوزيع هذه الكتل الخمسة إلى 5 سلاسل ، حيث سيستدعي كل خيط طريقة التنفيذ 20 مرة. من الناحية العملية ، بالطبع ، ليس حقيقة أن المجدول سيفعل ذلك بالضبط ، كل هذا يتوقف على عبء عمل النظام ، لذلك ربما يحدث كل 100 تكرار في سلسلة واحدة.

الآن يمكنك استدعاء الأسلوب Complete على مقبض المهام.

استيقظ
 void Awake() { this.array = new NativeArray<Vector2>(100, Allocator.TempJob); SimpleJob job = new SimpleJob(); job.array = this.array; this.handle = job.Schedule(100, 5); this.handle.Complete(); } 


في Corotine Start ، سوف نتحقق من تنفيذ المهمة ثم نقوم بتنظيف الحاوية.

ابدأ
 IEnumerator Start() { while(this.handle.isCompleted == false){ yield return new WaitForEndOfFrame(); } this.array.Dispose(); } 


الآن دعنا ننتقل إلى الإجراءات في المهمة نفسها.

Simplejob
 public struct SimpleJob : IJobParallelFor { public NativeArray<Vector2> array; public void Execute(int index) { float x = index; float y = index; Vector2 vector = new Vector2(x * x, y * y / (y * 2)); this.array[index] = vector; } } 


بعد الانتهاء من المهمة في طريقة البدء ، قم بعرض جميع عناصر المصفوفة في وحدة التحكم.

ابدأ
 IEnumerator Start() { while(this.handle.IsCompleted == false){ yield return new WaitForEndOfFrame(); } foreach(Vector2 vector in this.array) { print(vector); } this.array.Dispose(); } 


تم ، يمكنك تشغيل وإلقاء نظرة على النتيجة.

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

الصورة

يقدم IJobParallelFor ليس فقط لتنفيذ مهمة في سلسلة رسائل واحدة عدة مرات ، ولكن أيضًا لتوزيع هذه التكرارات بين سلاسل الرسائل الأخرى.

الصورة

بشكل عام ، يعتبر نوع التخصيص TempJob مثاليًا لمعظم المهام التي يتم إجراؤها عبر عدة إطارات.

ولكن ماذا لو كنت بحاجة إلى تخزين البيانات حتى بعد الانتهاء من المهمة ، وماذا لو لم تكن بحاجة إلى تدميرها على الفور بعد تلقي النتيجة. لهذا ، من الضروري استخدام نوع التخصيص المستمر ، مما يعني ضمناً الإفراج عن الموارد ثم " عند الضرورة!" .

مثابر


دعنا نعود إلى فصل TestJob ونغيره . الآن سنقوم بإنشاء المهام في أسلوب OnEnable ، والتحقق من تنفيذها في طريقة التحديث وتنظيف الموارد في طريقة OnDisable .
في المثال ، سنقوم بتحريك الكائن في طريقة التحديث ، لحساب المسار ، سنستخدم حاويتين متجهتين - inputArray حيث سنضع الموضع الحالي و outputArray من حيث سنتلقى النتائج.

تيستجوب
 public class TestJob : MonoBehaviour { private NativeArray<Vector2> inputArray; private NativeArray<Vector2> outputArray; private JobHandle handle; void OnEnable() {} void Update() {} void OnDisable() {} } 


سنقوم أيضًا بتعديل بنية مهمة SimpleJob قليلاً عن طريق ترثها من واجهة IJob لتنفيذها مرة واحدة.

Simplejob
 public struct SimpleJob : IJob { public void Execute() {} } 


في المهمة نفسها ، سنخون أيضًا حاويتين متجهتين ، ناقل موقف واحد ودلتا عددية ، والتي ستنقل الكائن إلى الهدف.

Simplejob
 public struct SimpleJob : IJob { [ReadOnly] public NativeArray<Vector2> inputArray; [WriteOnly] public NativeArray<Vector2> outputArray; public Vector2 position; public float delta; public void Execute() {} } 


تُظهر السمتان ReadOnly و WriteOnly قيود التدفق على الإجراءات المرتبطة بالبيانات داخل الحاويات. تقدم ReadOnly الدفق فقط لقراءة البيانات من الحاوية ، والسمة WriteOnly ، على العكس ، تسمح للدفق بكتابة البيانات فقط إلى الحاوية. إذا كنت بحاجة إلى تنفيذ هذين الإجراءين في وقت واحد باستخدام حاوية واحدة ، فلن تحتاج إلى وضع علامة عليها بسمة على الإطلاق.

دعنا ننتقل إلى أسلوب OnEnable لفئة TestJob حيث سيتم تهيئة الحاويات.

لا يمكن إنكاره
 void OnEnable() { this.inputArray = new NativeArray<Vector2>(1, Allocator.Persistent); this.outputArray = new NativeArray<Vector2>(1, Allocator.Persistent); } 


ستكون أبعاد الحاويات مفردة حيث أنه من الضروري إرسال واستقبال المعلمات مرة واحدة فقط. سيكون نوع التخصيص ثابتًا .
في طريقة OnDisable ، سنقوم بتحرير موارد الحاويات.

قابل للتعطيل
 void OnDisable() { this.inputArray.Dispose(); this.outputArray.Dispose(); } 


دعونا ننشئ طريقة CreateJob منفصلة حيث سننشئ مهمة بمقبضها وهناك سنملأها بالبيانات.

CreateJob
 void CreateJob() { SimpleJob job = new SimpleJob(); job.delta = Time.deltaTime; Vector2 position = this.transform.position; job.position = position; Vector2 newPosition = position + Vector2.right; this.inputArray[0] = newPosition; job.inputArray = this.inputArray; job.outputArray = this.outputArray; this.handle = job.Schedule(); this.handle.Complete(); } 


في الواقع ، ليست هناك حاجة في الإدخالArray حقًا هنا ، لأنه من الممكن نقل متجه الاتجاه فقط إلى المهمة ، ولكن أعتقد أنه سيكون من الأفضل أن نفهم سبب الحاجة إلى هذه السمات ReadOnly و WriteOnly على الإطلاق.

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

تحديث
 void Update() { if (this.handle.IsCompleted) { Vector2 newPosition = this.outputArray[0]; this.transform.position = newPosition; CreateJob(); } } 


قبل البدء ، سنقوم بتعديل أسلوب OnEnable قليلاً حتى يتم إنشاء المهمة فورًا بعد تهيئة الحاويات.

لا يمكن إنكاره
 void OnEnable() { this.inputArray = new NativeArray<Vector2>(1, Allocator.Persistent); this.outputArray = new NativeArray<Vector2>(1, Allocator.Persistent); CreateJob(); } 


تم الآن يمكنك الذهاب إلى المهمة نفسها وإجراء العمليات الحسابية اللازمة في طريقة التنفيذ .

تنفيذ
 public void Execute() { Vector2 newPosition = this.inputArray[0]; newPosition = Vector2.Lerp(this.position, newPosition, this.delta); this.outputArray[0] = newPosition; } 


لرؤية نتيجة العمل ، يمكنك رمي نص TestJob على بعض الأشياء وتشغيل اللعبة.

على سبيل المثال ، يتحول نقشتي المتحركة تدريجيًا إلى اليمين.

الرسوم المتحركة
الصورة

بشكل عام ، يعد نوع التخصيص المستمر مناسبًا للحاويات القابلة لإعادة الاستخدام التي لا تحتاج إلى تدميرها وإعادة إنشائها في كل مرة.

فما نوع لاستخدام!؟
يُفضل استخدام نوع Temp لإجراء العمليات الحسابية بسرعة ، ولكن إذا كانت المهمة معقدة جدًا وكبيرة ، فقد يحدث الركود.
يعد نوع TempJob رائعًا للعمل مع كائنات الوحدة ، بحيث يمكنك تغيير معلمات الكائنات وتطبيقها ، على سبيل المثال ، في الإطار التالي.
يمكن استخدام النوع المستمر عندما لا تكون السرعة مهمة بالنسبة لك ، ولكنك تحتاج فقط إلى حساب نوع ما من البيانات باستمرار على الجانب ، على سبيل المثال ، معالجة البيانات عبر شبكة ، أو عمل الذكاء الاصطناعي.

غير صالح ولا شيء
هناك نوعان آخران من التخصيص غير صالح ولا شيء ، لكنهما بحاجة إلى المزيد من أجل التصحيح ، ولا يشتركان في العمل.


Jobhandle


بشكل منفصل ، من المفيد تحليل إمكانات مقبض المهام ، لأنه بصرف النظر عن التحقق من عملية تنفيذ المهمة ، لا يزال بإمكان هذا المقبض الصغير إنشاء شبكات كاملة من المهام من خلال التبعيات (على الرغم من أنني أفضل أن أدعوهم بقوائم انتظار أكثر).

على سبيل المثال ، إذا كنت بحاجة إلى تنفيذ مهمتين في تسلسل معين ، فعندئذٍ تحتاج فقط إلى إرفاق مقبض إحدى المهام بمقبض آخر.

يبدو شيء من هذا القبيل.

الصورة

يحتوي كل مقبض فردي في البداية على مهمته الخاصة ، ولكن عند الجمع ، نحصل على مقبض جديد بمهمتين.

ابدأ
 void Start() { Job jobA = new Job(); JobHandle handleA = jobA.Schedule(); Job jobB = new Job(); JobHandle handleB = jobB.Schedule(); JobHandle result = JobHandle.CombineDependecies(handleA, handleB); result.Complete(); } 


أو هكذا.

ابدأ
 void Start() { JobHandle handle; for(int i = 0; i < 10; i++) { Job job = new Job(); handle = job.Schedule(handle); } handle.Complete(); } 


يتم حفظ تسلسل التنفيذ ولن يبدأ المجدول المهمة التالية حتى يقتنع بالمهمة السابقة ، ولكن من المهم أن تتذكر أن خاصية مقبض IsCompleted ستنتظر اكتمال جميع المهام فيه.

الخلاصة


حاويات


  1. عند العمل مع البيانات في الحاويات ، لا تنس أن هذه هي الهياكل ، لذا فإن أي استبدال للبيانات في الحاوية لا يغيرها ، بل ينشئها مرة أخرى.
  2. ماذا يحدث إذا قمت بتعيين نوع تخصيص درجة الحرارة ولم تقم بمسح الموارد بعد اكتمال المهمة؟ الخطأ.
  3. هل يمكنني إنشاء حاوياتي الخاصة؟ من الممكن أن توصف الوحدات بالتفصيل عملية إنشاء حاويات مخصصة هنا ، ولكن من الأفضل التفكير عدة مرات: هل الأمر يستحق ذلك ، ربما سيكون هناك ما يكفي من الحاويات العادية!؟

الأمان!


بيانات ثابتة.

لا تحاول استخدام البيانات الثابتة في مهمة ( عشوائية وغيرها) ، فإن أي وصول إلى البيانات الثابتة ينتهك أمن النظام. في الواقع ، في الوقت الحالي ، يمكنك الوصول إلى البيانات الثابتة ، ولكن فقط إذا كنت متأكدًا من أنها لا تتغير أثناء العمل - أي أنها ثابتة تمامًا وقراءة فقط.

متى تستخدم نظام المهام؟

كل هذه الأمثلة الواردة هنا في المقالة مشروطة فقط ، وتوضح كيفية العمل مع هذا النظام ، وليس متى تستخدمه. يمكن استخدام نظام المهام بدون ECS ،عليك أن تفهم أن النظام يستهلك أيضًا الموارد في العمل وأنه لأي سبب يكتب المهام على الفور ، فإن إنشاء أكوام من الحاويات أمر لا معنى له ببساطة - كل شيء سيصبح أسوأ. على سبيل المثال ، لن تكون إعادة حساب مصفوفة بحجم 10 آلاف عنصر صحيحة - سيستغرق الأمر أكثر من الوقت حتى يعمل المجدول ، ولكن إعادة حساب جميع مضلعات التضاريس الضخمة أو حتى توليدها هو الحل الصحيح ، يمكنك تقسيم التضاريس إلى مهام ومعالجة كل منها في تيار منفصل.

بشكل عام ، إذا كنت منخرطًا باستمرار في الحسابات المعقدة في المشاريع وتبحث باستمرار عن فرص جديدة لجعل هذه العملية أقل استهلاكًا للموارد ، فعندئذٍ نظام الوظائفهذا هو بالضبط ما تحتاجه. إذا كنت تعمل باستمرار مع حسابات معقدة بشكل لا ينفصل عن الكائنات وتريد أن تعمل شفرتك بشكل أسرع وأن تكون مدعومة في معظم الأنظمة الأساسية ، فإن ECS ستساعدك بالتأكيد في ذلك. إذا قمت بإنشاء مشاريع لـ WebGL فقط ، فهذا ليس مناسبًا لك ، في الوقت الحالي لا يدعم نظام العمل العمل في المتصفحات ، على الرغم من أن هذه ليست مشكلة بالنسبة للوحدات النمطية ، ولكن لمطوري المستعرضات أنفسهم.

المصدر مع جميع الأمثلة

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


All Articles