
أنت بالفعل رائع لدرجة أنك تقوم بتدوير coroutines حول جميع المحاور في نفس الوقت ، من لمحة واحدة الخاص بك أداء yield break
والاختباء وراء قماش IDE. الأغلفة البسيطة هي مرحلة طويلة.
أنت تعرف كيفية طبخها جيدًا بحيث يمكنك الحصول على نجمة ميشلان (أو حتى نجمتين) إذا كان لديك مطعم خاص بك. بالطبع! لن يكون أحد غير مبال بتذوق Bouillabaisse مع صلصة كوروتين.
لمدة أسبوع كامل ، لا يسقط الكود الموجود في المنتج! الأغلفة ، callback
، وطرق Start/Stop Coroutine
هي الكثير من العبيد. تحتاج إلى مزيد من السيطرة وحرية العمل. أنت جاهز لتسلق الخطوة التالية (ولكن لا تنسحب ، بالطبع).
إذا كنت تتعرف على نفسك في هذه السطور ، مرحبا بكم في القط.
أغتنم هذه الفرصة لأقول مرحباً بأحد أنماطي المفضلة - القيادة .
مقدمة
بالنسبة لجميع إرشادات yield
المقدمة من Unity ( Coroutine
و AsyncOperation
و WaiForSeconds
وغيرها) ، فإن الفئة الأساسية هي فئة YieldInstruction
( docs ) غير ملحوظة:
[StructLayout (LayoutKind.Sequential)] [UsedByNativeCode] public class YieldInstruction { }
تحت غطاء محرك السيارة ، coroutines هي العداد العادي ( IEnumerator
( مستندات )). يتم استدعاء كل إطار بواسطة الأسلوب MoveNext()
. إذا كانت قيمة الإرجاع true
، فسيتم تأخير التنفيذ حتى بيان yield
التالي ؛ وإذا كان false
، فإنه يتوقف.
للحصول على إرشادات yield
المخصص ، توفر الوحدة فئة CustomYieldInstruction
( docs ) التي تحمل الاسم نفسه.
public abstract class CustomYieldInstruction : IEnumerator { public abstract bool keepWaiting { get; } public object Current => null; public bool MoveNext(); public void Reset(); }
يكفي أن ترث منه وتنفيذ خاصية keepWaiting. لقد ناقشنا بالفعل منطقه أعلاه. يجب أن يعود true
طالما coroutine هو أن تنفذ.
مثال من الوثائق:
using UnityEngine; public class WaitForMouseDown : CustomYieldInstruction { public override bool keepWaiting { get { return !Input.GetMouseButtonDown(1); } } public WaitForMouseDown() { Debug.Log("Waiting for Mouse right button down"); } }
وفقًا لها ، keepWating
تنفيذ keepWating
getter كل إطار بعد Update()
وقبل LateUpdate()
.
إذا كنت تعمل برمز يجب أن تعمل فيه أشياء معينة في لحظة معينة ، ولم تقم بعد بدراسة هذه الصفحة ، فإنني أوصي بشدة بتحديد هذا العمل الفذ.
تعليمات العائد المخصصة

لكننا لم نأت إلى هنا CustomYieldInstruction
عادية مثل CustomYieldInstruction
. لهذا ، يمكنك البقاء دون ميشلان.
لذلك ، فإننا ندخن الوثائق بشكلٍ تقريبي ، وفي أسفل الصفحة نفسها تقريبًا نجد الفقرة الأكثر أهمية.
للحصول على مزيد من التحكم وتنفيذ إرشادات العائد المعقدة ، يمكنك أن ترث مباشرة من فئة System.Collections.IEnumerator
. في هذه الحالة ، قم بتطبيق الأسلوب MoveNext()
بنفس الطريقة التي keepWaiting
خاصية keepWaiting
. بالإضافة إلى ذلك ، يمكنك أيضًا إرجاع كائن في الخاصية Current
، والتي ستتم معالجتها بواسطة برنامج جدولة coroutine للوحدة بعد تنفيذ الأسلوب MoveNext MoveNext()
. لذلك على سبيل المثال ، إذا كان Current
أعاد كائنًا آخر وراثيًا من IEnumerator
، فسيتم تعليق العداد الحالي حتى يكتمل العائد.
بالروسيةللحصول على مزيد من التحكم وتنفيذ تعليمات yield
الأكثر تعقيدًا ، يمكنك أن ترث مباشرة من واجهة System.Collections.IEnumerator
. في هذه الحالة ، قم بتطبيق الأسلوب MoveNext()
بنفس الطريقة مثل خاصية keepWaiting
. بالإضافة إلى ذلك ، يمكنك استخدام الكائن في الخاصية Current
، والتي ستتم معالجتها بواسطة جدولة coroutine Unity بعد تنفيذ الأسلوب MoveNext()
. على سبيل المثال ، إذا كانت الخاصية Current
تُرجع كائنًا آخر يقوم بتنفيذ واجهة IEnumerator
، فسيتم تأخير تنفيذ العداد الحالي حتى الانتهاء من الجديد.
المحار المقدسة! هذه هي الضوابط وحرية العمل التي أردت الكثير. حسنًا ، كل ما عليك فعله الآن هو لفّ corutins بحيث لا يكون قفل Gimbal مخيفًا. الشيء الرئيسي ، من السعادة ، همز من vertukha لا فرقعة.
واجهة
حسنًا ، سيكون من الجيد أن نبدأ بتحديد واجهة التفاعل مع تعليماتنا التي نريدها. بعض مجموعة الحد الأدنى.
أقترح الخيار التالي
public interface IInstruction { bool IsExecuting { get; } bool IsPaused { get; } Instruction Execute(); void Pause(); void Resume(); void Terminate(); event Action<Instruction> Started; event Action<Instruction> Paused; event Action<Instruction> Cancelled; event Action<Instruction> Done; }
أريد أن IsExecuting
انتباهكم إلى حقيقة أن IsExecuting
و IsPaused
ليست العكس هنا. إذا تم إيقاف تنفيذ coroutine ، فإنه لا يزال قيد التنفيذ.
سوليتير ومجان
كما تقول الوثائق ، تحتاج إلى تطبيق واجهة IEnumerator
. في بعض الأماكن ، في الوقت الحالي ، دعنا نترك الدعارة ، حيث أن تنفيذها يعتمد بشكل مباشر على الوظيفة التي نريد وضعها فيها.
using UnityEngine; using IEnumerator = System.Collections.IEnumerator; public abstract class Instruction : IEnumerator { private Instruction current; object IEnumerator.Current => current; void IEnumerator.Reset() { } bool IEnumerator.MoveNext() { } }
تجدر الإشارة إلى أن هناك طريقتين على الأقل يمكن من خلالها إطلاق تعليماتنا:
طريقة StartCoroutine(IEnumerator routine)
:
StartCoroutine(new ConcreteInstruction());
yield
العائد:
private IEnumerator SomeRoutine() { yield return new ConcreteInstruction(); }
طريقة Execute
، التي وصفناها أعلاه في واجهة IInstruction
، ستستخدم الطريقة الأولى. لذلك ، نضيف بعض الحقول التي ستساعد في إدارة التعليمات في هذه الحالة.
private object routine; public MonoBehaviour Parent { get; private set; }
الآن خصائص وأحداث IInstruction
.
using UnityEngine; using System; using IEnumerator = System.Collections.IEnumerator; public abstract class Instruction : IEnumerator, IInstruction { private Instruction current; object IEnumerator.Current => current; private object routine; public MonoBehaviour Parent { get; private; } public bool IsExecuting { get; private set; } public bool IsPaused { get; private set; } private bool IsStopped { get; set; } public event Action<Instruction> Started; public event Action<Instruction> Paused; public event Action<Instruction> Terminated; public event Action<Instruciton> Done; void IEnumerator.Reset() { } bool IEnumerator.MoveNext() { } Instruction(MonoBehaviour parent) => Parent = parent; }
أيضًا ، طرق التعامل مع الأحداث في الفصول الفرعية:
protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { }
سيتم استدعاؤهم يدويًا قبل الأحداث ذات الصلة مباشرة لإعطاء الطبقات التابعة أولوية المعالجة.
الآن الأساليب المتبقية.
public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public bool Resume() { if (IsExecuting) { IsPaused = false; OnResumed(); } }
كل شيء بسيط هنا. لا يمكن إيقاف التعليمات إلا إذا تم تنفيذها وإذا لم يتم تعليقها ، وتستمر عملية التنفيذ فقط إذا تم تعليقها.
public void Terminate() { if (Stop()) { OnTerminated(); Terminate?.Invoke(this); } } private bool Stop() { if (IsExecuting) { if (routine is Coroutine) Parent.StopCoroutine(routine as Coroutine); (this as IEnumerator).Reset(); return IsStopped = true; } return false; }
تم نقل المنطق الأساسي لتعليمات الإيقاف إلى طريقة Stop
. يعد ذلك ضروريًا حتى تكون قادرًا على تنفيذ توقف صامت (بدون تشغيل الأحداث).
من الضروري التحقق مما if (routine is Coroutine)
ضروريًا ، كما كتبت أعلاه ، يمكن بدء التعليمات من خلال yield
(أي دون استدعاء StartCoroutine
) ، مما يعني أنه قد لا يكون هناك رابط لمثيل محدد من Coroutine
. في هذه الحالة ، سيكون routine
كائن روتين فقط.
public Instruction Execute(MonoBehaviour parent) { if (current != null) { Debug.LogWarning($"Instruction { GetType().Name} is currently waiting for another one and can't be stared right now."); return this; } if (!IsExecuting) { IsExecuting = true; routine = (Parent = parent).StartCoroutine(this); return this; } Debug.LogWarning($"Instruction { GetType().Name} is already executing."); return this; }
طريقة الإطلاق الرئيسية بسيطة للغاية أيضًا - لن يتم تنفيذ الإطلاق إلا إذا لم يتم تشغيل التعليمات بعد.
يبقى إكمال تطبيق IEnumerator
، لأننا تركنا مساحات فارغة في بعض الطرق.
IEnumerator.Reset() { IsExecuting = false; IsPaused = false; IsStopped = false; routine = null; }
والأكثر إثارة للاهتمام ، ولكن ليس أكثر تعقيدا ، هو MoveNext
:
bool IEnumerator.MoveNext() { if (IsStopped) { (this as IEnumerator).Reset(); return false; } if (!IsExecuting) { IsExecuting = true; routine = new object(); OnStarted(); Started?.Invoke(this); } if (current != null) return true; if (IsPaused) return true; if (!Update()) { OnDone(); Done?.Invoke(this); IsStopped = true; return false; } return true; }
if (!IsExecuting)
- إذا لم يتم تشغيل التعليمة من خلال StartCoroutine
، وتم تنفيذ هذا السطر من التعليمات البرمجية ، فإن yield
أطلقته. نكتب كعب في أحداث routine
والنار.
if (current != null)
- يستخدم current
لتعليمات الطفل. إذا ظهر هذا فجأة ، فإننا ننتظر نهايته. يرجى ملاحظة أنني سأفتقد عملية إضافة دعم لتعليمات الطفل في هذه المقالة حتى لا تضخيمها أكثر. لذلك ، إذا كنت غير مهتم بإضافة المزيد من هذه الوظيفة ، يمكنك ببساطة إزالة هذه الخطوط.
if (!Update())
- ستعمل طريقة Update
في إرشاداتنا تمامًا مثل keepWaiting
في CustomYieldInstruction
ويجب تنفيذها في فصل تابع. Instruction
هو مجرد وسيلة مجردة.
protected abstract bool Update();
رمز التعليمات بالكامل using UnityEngine; using System; using IEnumerator = System.Collections.IEnumerator; public abstract class Instruction : IEnumerator, IInstruction { private Instruction current; object IEnumerator.Current => current; private object routine; public MonoBehaviour Parent { get; private set; } public bool IsExecuting { get; private set; } public bool IsPaused { get; private set; } private bool IsStopped { get; set; } public event Action<Instruction> Started; public event Action<Instruction> Paused; public event Action<Instruction> Terminated; public event Action<Instruction> Done; void IEnumerator.Reset() { IsPaused = false; IsStopped = false; routine = null; } bool IEnumerator.MoveNext() { if (IsStopped) { (this as IEnumerator).Reset(); return false; } if (!IsExecuting) { IsExecuting = true; routine = new object(); OnStarted(); Started?.Invoke(this); } if (current != null) return true; if (IsPaused) return true; if (!Update()) { OnDone(); Done?.Invoke(this); IsStopped = true; return false; } return true; } protected Instruction(MonoBehaviour parent) => Parent = parent; public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public void Resume() { IsPaused = false; OnResumed(); } public void Terminate() { if (Stop()) { OnTerminated(); Terminated?.Invoke(this); } } private bool Stop() { if (IsExecuting) { if (routine is Coroutine) Parent.StopCoroutine(routine as Coroutine); (this as IEnumerator).Reset(); return IsStopped = true; } return false; } public Instruction Execute() { if (current != null) { Debug.LogWarning($"Instruction { GetType().Name} is currently waiting for another one and can't be stared right now."); return this; } if (!IsExecuting) { IsExecuting = true; routine = Parent.StartCoroutine(this); return this; } Debug.LogWarning($"Instruction { GetType().Name} is already executing."); return this; } public Instruction Execute(MonoBehaviour parent) { if (current != null) { Debug.LogWarning($"Instruction { GetType().Name} is currently waiting for another one and can't be stared right now."); return this; } if (!IsExecuting) { IsExecuting = true; routine = (Parent = parent).StartCoroutine(this); return this; } Debug.LogWarning($"Instruction { GetType().Name} is already executing."); return this; } public void Reset() { Terminate(); Started = null; Paused = null; Terminated = null; Done = null; } protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { } protected abstract bool Update(); }
أمثلة
الطبقة الأساسية جاهزة. لإنشاء أي تعليمات ، يكفي أن ترثه وتنفيذ الأعضاء الضروريين. أقترح نظرة على بعض الأمثلة.
الانتقال إلى نقطة
public sealed class MoveToPoint : Instruction { public Transform Transform { get; set; } public Vector3 Target { get; set; } public float Speed { get; set; } public float Threshold { get; set; } public MoveToPoint(MonoBehaviour parent) : base(parent) { } protected override bool Update() { Transform.position = Vector3.Lerp(Transform.position, Target, Time.deltaTime * Speed); return Vector3.Distance(Transform.position, Target) >= Threshold; } }
خيارات التشغيل private void Method() { var move = new MoveToPoint(this) { Actor = transform, Target = target, Speed = 1.0F, Threshold = 0.05F }; move.Execute(); }
private void Method() { var move = new MoveToPoint(this); move.Execute(transform, target, 1.0F, 0.05F); }
private IEnumerator Method() { yield return new MoveToPoint(this) { Actor = transform, Target = target, Speed = 1.0F, Threshold = 0.05F }; }
private IEnumerator Method() { var move = new MoveToPoint(this) { Actor = transform, Target = target, Speed = 1.0F, Threshold = 0.05F }; yield return move; }
private IEnumerator Method() { var move = new MoveToPoint(this); yield return move.Execute(transform, target, 1.0F, 0.05F); }
معالجة المدخلات
public sealed class WaitForKeyDown : Instruction { public KeyCode Key { get; set; } protected override bool Update() { return !Input.GetKeyDown(Key); } public WaitForKeyDown(MonoBehaviour parent) : base(parent) { } public Instruction Execute(KeyCode key) { Key = key; return base.Execute(); } }
إطلاق أمثلة private void Method() { car wait = new WaitForKeyDown(this); wait.Execute(KeyCode.Space).Done += (i) => DoSomething(); }
private IEnumerator Method() { yield return new WaitForKeyDown(this) { Key = KeyCode.Space };
انتظر وقفة
public sealed class Wait : Instruction { public float Delay { get; set; } private float startTime; protected override bool Update() { return Time.time - startTime < Delay; } public Wait(MonoBehaviour parent) : base(parent) { } public Wait(float delay, MonoBehaviour parent) : base(parent) { Delay = delay; } protected override void OnStarted() { startTime = Time.time; } public Instruction Execute(float delay) { Delay = delay; return base.Execute(); } }
هنا ، يبدو أن كل شيء بسيط قدر الإمكان. أولاً ، سجلنا وقت البدء ، ثم تحققنا من الفرق بين الوقت الحالي و StartTime
. ولكن هناك فارق بسيط واحد لا يمكنك ملاحظته على الفور. إذا تم إيقافها مؤقتًا أثناء تنفيذ التعليمات (باستخدام طريقة الإيقاف المؤقت) وانتظرت ، ثم بعد استئنافها ( Resume
) سيتم تنفيذها على الفور. وكل هذا لأن هذه التعليمات الآن لا تأخذ في الاعتبار أنه يمكنني إيقافها مؤقتًا.
دعنا نحاول إصلاح هذا الظلم:
protected override void OnPaused() { Delay -= Time.time - startTime; } protected override void OnResumed() { startTime = Time.time; }
تم الآن ، بعد نهاية الإيقاف المؤقت ، سيستمر تنفيذ التعليمات بنفس الوقت المتبقي قبل أن تبدأ.
إطلاق أمثلة private void Method() { var wait = new Wait(this); wait.Execute(5.0F).Done += (i) => DoSomething; }
private IEnumerator Method() { yield return new Wait(this, 5.0F);
يؤدي
تتيح لك Coroutines in Unity القيام بأشياء مذهلة وعملية. لكن مدى تعقيدها وما إذا كنت بحاجة إليها على الإطلاق هو الخيار الشخصي للجميع.
تمديد وظيفة كوروتين ليست المهمة الأكثر صعوبة. كل ما عليك فعله هو تحديد الواجهة التي تريد التفاعل معها وما هي خوارزمية عملهم.
شكرا على وقتك. اترك أسئلتك / تعليقاتك / إضافاتك في التعليقات. سأكون سعيدا للتواصل.
ملحوظة: إذا ، فجأة ، كنت قد أحرقت عن استخدام corutin ، وقراءة هذا التعليق وشرب شيء بارد.