
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 RussischUm 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:
StartCoroutine(IEnumerator routine)
Methode StartCoroutine(IEnumerator routine)
:
StartCoroutine(new ConcreteInstruction());
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 };
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);
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.