يجد كل مطور مزايا وعيوب استخدام 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() {
وإذا كان هناك بعض lambda كمعالج ، فإنه يبدو أسوأ.
مع غلافنا ، يكفي إضافة هذا الحدث مرة واحدة فقط.
public Action Finish;
private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finish?.Invoke(); }
ثم ، إذا لزم الأمر ، اشترك.
moveRoutine.Finished += OnFinish; private void OnFinish() {
أعتقد أنك لاحظت بالفعل أن الإصدار الحالي من المجمع يوفر القدرة على العمل فقط مع 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 ، وأكثر من ذلك. لكنني اقترح التوقف في هذه المرحلة.
الغرض من هذه المقالة هو الرغبة في مشاركة المشكلات الملحة التي واجهتها واقتراح حل لها ، وليس لتغطية الاحتياجات المحتملة لجميع المطورين.
آمل أن يستفيد كل من المبتدئين والرفاق ذوي الخبرة من تجربتي. ربما سيشاركون تعليقاتهم أو يشيرون إلى الأخطاء التي قد تكون ارتكبت.