كيفية جعل coroutines في الوحدة أكثر قليلا مريحة

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


في الممارسة اليومية ، كثيراً ما أستخدم coroutines لأنواع مختلفة من المهام. ذات مرة ، أدركت أنني كنت من أزعج ورفض العديد من القادمين الجدد.


واجهة كابوس


يوفر المحرك فقط عدة طرق والعديد من الأحمال الزائدة للعمل مع coroutines:


التشغيل ( المستندات )


  • StartCoroutine(string name, object value = null)
  • StartCoroutine(IEnumerator routine)

توقف ( مستندات )


  • StopCoroutine(string methodName)
  • StopCoroutine(IEnumerator routine)
  • StopCoroutine(Coroutine routine)

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


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

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


أقرب إلى هذه النقطة


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


افترض أن لدينا المهمة التالية - كتابة مكون يسمح لك بنقل كائن إلى نقطة معينة.


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


يرجى ملاحظة أنه يوصى بشدة بعدم نقل كائن عن طريق تغيير إحداثياته ​​"على الجبهة" ، أي بناء transform.position = newPosition إذا تم استخدام مكون transform.position = newPosition معه (خاصة في طريقة Update ) ( المستندات ).


التنفيذ القياسي


أقترح خيار التنفيذ التالي للمكون المطلوب:


 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; public void Move() { if (moveRoutine == null) StartCoroutine(MoveRoutine()); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 

قليلا عن الرمز

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


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


ما هذا؟


بالنظر إلى أن جميع المكونات ( x ، y ، z ) من بنية Vector3 هي من النوع float ، فإن استخدام نتيجة التحقق من المساواة في المسافة بين الهدف والتسامح كشرط حلقة هي فكرة سيئة .


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


وأيضًا ، إذا كنت ترغب في ذلك ، يمكنك استخدام Mathf.Approximately ( docs ) للفحص التقريبي للمساواة. تجدر الإشارة إلى أنه في بعض طرق الحركة ، قد تتحول السرعة إلى حجم كبير بحيث "يقفز" الهدف في إطار واحد. ثم لن تنتهي الدورة أبدًا. على سبيل المثال ، إذا كنت تستخدم أسلوب Vector3.MoveTowards .


على Vector3 علمي ، في مشغل Vector3 لبنية Vector3 ، تم بالفعل إعادة تعريف عامل Vector3 بالفعل بحيث يتم استدعاء Mathf.Approximately للتحقق من المساواة بين المكونات.


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


نظرًا لأننا ، وأنا قررت عدم الانتقال إلى الجانب الشرير ، وعدم استخدام الأحمال الزائدة مع معلمات السلسلة ، نحن الآن بحاجة إلى حفظ مكان ما إلى coroutine قيد التشغيل. خلاف ذلك ، فكيف لوقفها؟


إضافة حقل:


 private Coroutine moveRoutine; 

إصلاح Move :


 public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); } 

أضف طريقة توقف الحركة:


 public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); } 

كود كامل
 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 

مسألة مختلفة تماما! على الأقل تنطبق على الجرح.


هكذا. لدينا مكون صغير يؤدي المهمة. ما هو سخطي؟


المشاكل وحلها


مع مرور الوقت ، ينمو المشروع ، ومعه عدد المكونات ، بما في ذلك تلك التي تستخدم coroutines. وفي كل مرة ، هذه الأشياء تطاردني أكثر وأكثر:


  • مكالمات ساندويتش الجارية

 StartCoroutine(MoveRoutine()); 

 StopCoroutine(moveRoutine); 

مجرد النظر إليهم يجعل نشل عيني ، وقراءة مثل هذا الكود متعة مشكوك فيها (أوافق ، يمكن أن تكون أسوأ). ولكن سيكون من الجميل أن يكون لديك شيء مثل هذا:


 moveRoutine.Start(); 

 moveRoutine.Stop(); 

  • في كل مرة تتصل فيها بـ StartCoroutine يجب أن تتذكر حفظ القيمة المرجعة:

 moveRoutine = StartCoroutine(MoveRoutine()); 

خلاف ذلك ، بسبب عدم وجود إشارة إلى coroutine ، لا يمكنك ببساطة إيقافه.


  • الشيكات المستمرة:

 if (moveRoutine == null) 

 if (moveRoutine != null) 

  • وشيء شرير آخر يجب أن تتذكره دائمًا (والذي أنا عليه نسيت مرة أخرى غاب خصيصا). في نهاية coroutine وقبل كل خروج منه (على سبيل المثال ، استخدام yield break ) ، من الضروري إعادة تعيين قيمة الحقل.
     moveRoutine = null; 

إذا نسيت ، سوف تتلقى coroutine لمرة واحدة. بعد التشغيل الأول في moveRoutine ، moveRoutine الرابط إلى coroutine ، ولن يعمل المدى الجديد.


بنفس الطريقة ، عليك القيام به في حالة التوقف القسري:


 public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } } 

رمز مع كل التغييرات
 public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null; } } 

في مرحلة ما ، أريد حقًا أن أخرج هذه المهزلة بالكامل مرة واحدة في مكان ما ، وأترك ​​لنفسي فقط الطرق الضرورية: Start ، Stop واثنين من الأحداث والخصائص الأخرى.


دعونا نفعل ذلك في النهاية!


 using System.Collections; using System; using UnityEngine; public sealed class CoroutineObject { public MonoBehaviour Owner { get; private set; } public Coroutine Coroutine { get; private set; } public Func<IEnumerator> Routine { get; private set; } public bool IsProcessing => Coroutine != null; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if (IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 

استخلاص المعلومات

Owner - رابط إلى مثيل MonoBehaviour الذي سيتم إرفاق coroutine به. كما تعلمون ، يجب أن يتم تنفيذه في سياق مكون معين ، نظرًا لأن طرق StopCoroutine و StopCoroutine . وفقًا لذلك ، نحتاج إلى رابط للمكون الذي سيكون مالك coroutine.


Coroutine - التناظرية لحقل moveRoutine في مكون MoveToPoint ، يحتوي على رابط إلى coroutine الحالي.


Routine - المندوب الذي سيتم التواصل معه بالطريقة التي تعمل ككوروتيني.


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


IsProcessing - يسمح لك بمعرفة ما إذا كان coroutine يعمل حاليًا.


وبالتالي ، نتخلص من قدر كبير من الصداع ، ومكوننا يأخذ نظرة مختلفة تمامًا:


 using IEnumerator = System.Collections; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private CoroutineObject moveRoutine; private void Awake() { moveRoutine = new CoroutineObject(this, MoveRoutine); } public void Move() => moveRoutine.Start(); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 

كل ما تبقى هو coroutine نفسها وخطوط قليلة من التعليمات البرمجية للعمل معها. أفضل بكثير.


افترض أن مهمة جديدة قد حان - تحتاج إلى إضافة القدرة على تنفيذ أي رمز بعد أن يصل الكائن إلى هدفه.


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


 private IEnumerator MoveRoutine(System.Action callback) { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null callback?.Invoke(); } 

وندعو على النحو التالي:


 moveRoutine = StartCoroutine(moveRoutine(CallbackHandler)); private void CallbackHandler() { // do something } 

وإذا كان هناك بعض lambda كمعالج ، فإنه يبدو أسوأ.


مع غلافنا ، يكفي إضافة هذا الحدث مرة واحدة فقط.


 public Action Finish; 

 private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finish?.Invoke(); } 

ثم ، إذا لزم الأمر ، اشترك.


 moveRoutine.Finished += OnFinish; private void OnFinish() { // do something } 

أعتقد أنك لاحظت بالفعل أن الإصدار الحالي من المجمع يوفر القدرة على العمل فقط مع coroutines بدون معلمات. لذلك ، يمكننا كتابة مجمع معمم ل coroutine مع معلمة واحدة. تتم الباقي عن طريق القياس.


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


نزيلها:


  • خصائص Owner ، Coroutine ، IsProcessing
  • حدث Finished

 using Action = System.Action; using UnityEngine; public abstract class CoroutineObjectBase { public MonoBehaviour Owner { get; protected set; } public Coroutine Coroutine { get; protected set; } public bool IsProcessing => Coroutine != null; public abstract event Action Finished; } 

المجمع بدون معلمات بعد إعادة البناء
 using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject : CoroutineObjectBase { public Func<IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finished?.Invoke(); } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 

والآن ، في الواقع ، غلاف ل coroutine مع معلمة واحدة:


 using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject<T> : CoroutineObjectBase { public Func<T, IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<T, IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process(T arg) { yield return Routine.Invoke(arg); Coroutine = null; Finished?.Invoke(); } public void Start(T arg) { Stop(); Coroutine = Owner.StartCoroutine(Process(arg)); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 

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


لنفترض أنه طُلب منا تحديث مكون MoveToPoint بحيث لا يمكن تعيين النقطة من خلال نافذة Inspector في المحرر ، ولكن بواسطة الكود عندما تم استدعاء طريقة Move .


 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public float speed; public float threshold; private CoroutineObject<Vector3> moveRoutine; public bool IsMoving => moveRoutine.IsProcessing; private void Awake() { moveRoutine = new CoroutineObject<Vector3>(this, MoveRoutine); } public void Move(Vector3 target) => moveRoutine.Start(target); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine(Vector3 target) { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed); yield return null; } } } 

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


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


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


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


All Articles