Benutzerdefinierte Coroutinen in Einheit mit Vorlieben und Kurtisanen


Sie sind bereits so cool, dass Sie Coroutinen gleichzeitig um alle Achsen drehen. Auf einen Blick führen sie eine yield break und verstecken sich hinter der IDE-Leinwand. Einfache Wrapper sind längst vorbei.


Sie wissen, wie man sie so gut kocht, dass Sie einen Michelin- Stern (oder sogar zwei) bekommen könnten, wenn Sie ein eigenes Restaurant hätten. Natürlich! Niemand wird gleichgültig sein, wenn Sie Ihre Bouillabaisse mit Coroutinensauce probieren.


Für eine ganze Woche fällt der Code im Produkt nicht! Wrapper, callback 'und Start/Stop Coroutine sind die Menge der Slaves. Sie brauchen mehr Kontrolle und Handlungsfreiheit. Sie sind bereit, den nächsten Schritt zu erklimmen (aber natürlich keine Coroutinen zu beenden).


Wenn Sie sich in diesen Zeilen wiedererkennen, sind Sie bei cat willkommen.


Ich nutze diese Gelegenheit, um eines meiner Lieblingsmuster zu begrüßen - Command .


Einführung


Für alle von Unity Coroutine AsyncOperation ( Coroutine , AsyncOperation , WaiForSeconds und andere) ist die Basisklasse die unauffällige YieldInstruction ( docs ) -Klasse:


 [StructLayout (LayoutKind.Sequential)] [UsedByNativeCode] public class YieldInstruction { } 

Unter der Haube sind Coroutinen gewöhnliche Enumeratoren ( IEnumerator ( docs )). Jeder Frame wird von der MoveNext() -Methode aufgerufen. Wenn der Rückgabewert true , wird die Ausführung bis zur nächsten yield Anweisung verzögert. Wenn false , wird sie gestoppt.


Für benutzerdefinierte yield stellt Unity die gleichnamige CustomYieldInstruction Klasse ( docs ) CustomYieldInstruction .


 public abstract class CustomYieldInstruction : IEnumerator { public abstract bool keepWaiting { get; } public object Current => null; public bool MoveNext(); public void Reset(); } 

Es reicht aus, davon zu erben und die keepWaiting-Eigenschaft zu implementieren. Wir haben seine Logik bereits oben diskutiert. Es sollte true , solange Coroutine ausgeführt werden soll.


Beispiel aus der Dokumentation:


 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"); } } 

Ihr zufolge wird der keepWating Getter jeden Frame nach Update() und vor LateUpdate() .


Wenn Sie mit Code arbeiten, in dem bestimmte Dinge zu einem bestimmten Zeitpunkt funktionieren sollten, und diese Seite noch nicht studiert haben , empfehle ich dringend, sich für dieses Kunststück zu entscheiden.


Benutzerdefinierte Ertragsanweisungen



Wir sind jedoch nicht mit regulären Wrappern wie der CustomYieldInstruction um sie CustomYieldInstruction erben. Dafür können Sie ohne Michelin bleiben.


Deshalb rauchen wir die Dokumentation weiter und fast am Ende derselben Seite finden wir den wichtigsten Absatz.


Um mehr Kontrolle zu haben und komplexere Ertragsanweisungen zu implementieren, können Sie direkt von der System.Collections.IEnumerator Klasse erben . Implementieren MoveNext() in diesem Fall die MoveNext() -Methode genauso, wie Sie die keepWaiting Eigenschaft implementieren keepWaiting . Darüber hinaus können Sie in der Eigenschaft Current ein Objekt zurückgeben, das nach Ausführung der MoveNext MoveNext() -Methode vom Coroutine-Scheduler von Unity verarbeitet wird . Wenn Current beispielsweise ein anderes Objekt IEnumerator , das von IEnumerator , wird der aktuelle Enumerator angehalten, bis der zurückgegebene abgeschlossen ist.


Auf Russisch

Um mehr Kontrolle zu erhalten und komplexere yield implementieren , können Sie direkt von der System.Collections.IEnumerator Schnittstelle erben . Implementieren MoveNext() in diesem Fall die MoveNext() -Methode auf dieselbe Weise wie die keepWaiting Eigenschaft . Darüber hinaus können Sie das Objekt in der Current Eigenschaft verwenden , die vom Unity-Coroutine-Scheduler nach Ausführung der MoveNext() -Methode verarbeitet wird . Wenn die Current Eigenschaft beispielsweise ein anderes Objekt zurückgibt, das die IEnumerator Schnittstelle implementiert , wird die Ausführung des aktuellen Enumerators bis zum Abschluss des neuen verzögert.


Heilige Muscheln! Dies sind die Kontrollen und die Handlungsfreiheit, die ich so sehr wollte. Nun, das ist es, jetzt wickelst du so viele Corutins ein, dass kein Gimbal Lock beängstigend ist. Die Hauptsache, aus Glück, stupste von der Vertukha, nicht zu schlagen.


Schnittstelle


Nun, es wäre schön, zunächst zu entscheiden, welche Schnittstelle der Interaktion mit unserer Anweisung wir haben möchten. Einige minimale Menge.


Ich schlage die folgende Option vor


 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; } 

Ich möchte Ihre Aufmerksamkeit auf die Tatsache IsExecuting , dass IsExecuting und IsPaused nicht das Gegenteil sind. Wenn die Ausführung von Coroutine angehalten wird, wird sie noch ausgeführt.


Solitaire und Kurtisane


Wie in der Dokumentation angegeben, müssen Sie die IEnumerator Schnittstelle implementieren. Lassen Sie uns an einigen Stellen vorerst die Stubs verlassen, da ihre Implementierung direkt davon abhängt, welche Funktionen wir in sie einfügen möchten.


 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() { } } 

Es ist zu bedenken, dass es mindestens zwei Möglichkeiten gibt, wie unser Unterricht gestartet werden kann:


  1. StartCoroutine(IEnumerator routine) Methode StartCoroutine(IEnumerator routine) :


     StartCoroutine(new ConcreteInstruction()); 

  2. Ertragserklärung:


     private IEnumerator SomeRoutine() { yield return new ConcreteInstruction(); } 


Die Execute Methode, die wir oben in der IInstruction Schnittstelle beschrieben haben, verwendet die erste Methode. Daher fügen wir einige Felder hinzu, die in diesem Fall die Verwaltung der Anweisung erleichtern.


 private object routine; public MonoBehaviour Parent { get; private set; } 

Jetzt Eigenschaften und Ereignisse für 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; } 

Methoden zum Behandeln von Ereignissen in untergeordneten Klassen:


 protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { } 

Sie werden unmittelbar vor den entsprechenden Ereignissen manuell aufgerufen, um den untergeordneten Klassen die Priorität ihrer Verarbeitung zu geben.


Nun die restlichen Methoden.


 public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public bool Resume() { if (IsExecuting) { IsPaused = false; OnResumed(); } } 

Hier ist alles einfach. Der Befehl kann nur angehalten werden, wenn er ausgeführt wird und nicht angehalten wurde, und die Ausführung nur fortsetzen, wenn er angehalten ist.


 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; } 

Die Grundlogik zum Stoppen von Anweisungen wurde in die Stop Methode verschoben. Dies ist notwendig, um einen stillen Stopp durchführen zu können (ohne Ereignisse auszulösen).
Überprüfen Sie, if (routine is Coroutine) erforderlich if (routine is Coroutine) , da die Anweisung, wie oben beschrieben, über die yield gestartet werden kann ( StartCoroutine ohne StartCoroutine ). StartCoroutine bedeutet, dass möglicherweise keine Verknüpfung zu einer bestimmten Instanz von Coroutine . In diesem Fall hat die routine nur ein Stub-Objekt.


 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; } 

Die Hauptstartmethode ist ebenfalls äußerst einfach: Der Start wird nur ausgeführt, wenn die Anweisung noch nicht gestartet wurde.


Die Implementierung von IEnumerator , da wir in einigen Methoden Leerzeichen gelassen haben.


 IEnumerator.Reset() { IsExecuting = false; IsPaused = false; IsStopped = false; routine = null; } 

Und das interessanteste, aber nicht kompliziertere ist 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) - Wenn die Anweisung nicht über StartCoroutine gestartet wurde und diese Codezeile ausgeführt wird, wurde sie von der yield gestartet. Wir schreiben einen Stich in die routine und Feuerereignisse.


if (current != null) - current für untergeordnete Anweisungen verwendet. Wenn dies plötzlich aufgetaucht ist, warten wir auf sein Ende. Bitte beachten Sie, dass ich den Prozess des Hinzufügens von Unterstützung für untergeordnete Anweisungen in diesem Artikel verpassen werde, um ihn nicht noch mehr aufzublasen. Wenn Sie diese Funktionalität nicht weiter hinzufügen möchten, können Sie diese Zeilen einfach entfernen.


if (!Update()) - Die Update Methode in unseren Anweisungen funktioniert genauso wie keepWaiting in CustomYieldInstruction und sollte in einer CustomYieldInstruction Klasse implementiert werden. Instruction ist nur eine abstrakte Methode.


 protected abstract bool Update(); 

Gesamter Anweisungscode
 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(); } 

Beispiele


Die Basisklasse ist fertig. Um Anweisungen zu erstellen, reicht es aus, sie zu erben und die erforderlichen Mitglieder zu implementieren. Ich schlage vor, einige Beispiele anzuschauen.


Bewegen Sie sich zum Punkt


 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; } } 

Startoptionen
 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); } 

Eingabeverarbeitung


 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(); } } 

Beispiele starten
 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 }; // do something; } 

Warten Sie und halten Sie inne


 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(); } } 

Hier scheint alles so einfach wie möglich zu sein. Zuerst haben wir die Startzeit aufgezeichnet und dann die Differenz zwischen der aktuellen Zeit und der StartTime . Aber es gibt eine Nuance, die Sie nicht sofort bemerken können. Wenn Sie während der Ausführung eines Befehls ihn anhalten (mithilfe der Pause Methode) und warten, wird er nach dem Fortsetzen ( Resume ) sofort ausgeführt. Und das alles, weil diese Anweisung jetzt nicht berücksichtigt, dass ich sie anhalten kann.


Versuchen wir, diese Ungerechtigkeit zu beheben:


 protected override void OnPaused() { Delay -= Time.time - startTime; } protected override void OnResumed() { startTime = Time.time; } 

Fertig. Jetzt, nach dem Ende der Pause, wird die Anweisung so lange ausgeführt, wie sie vor Beginn noch übrig war.


Beispiele starten
 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); // do something; } 

Zusammenfassung


Mit Coroutines in Unity können Sie erstaunliche und funktionale Dinge tun. Aber wie komplex sie sein werden und ob Sie sie überhaupt brauchen, ist jedermanns persönliche Entscheidung.


Die Funktionalität von Corutin zu erweitern ist nicht die schwierigste Aufgabe. Alles, was Sie tun müssen, ist zu entscheiden, welche Schnittstelle Sie mit ihnen interagieren möchten und wie der Algorithmus ihrer Arbeit aussehen wird.


Danke für deine Zeit. Hinterlassen Sie Ihre Fragen / Kommentare / Ergänzungen in den Kommentaren. Ich werde gerne kommunizieren.




PS Wenn Sie sich plötzlich wegen der Verwendung von Corutin verbrannt haben, lesen Sie diesen Kommentar und trinken Sie etwas Cooles.

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


All Articles