
Ya eres tan genial que giras las rutinas alrededor de todos los ejes al mismo tiempo, desde tu primer vistazo realizan un yield break
y se esconden detrás del lienzo IDE. Las envolturas simples son una etapa pasada.
Usted sabe cómo cocinarlos tan bien que podría obtener una estrella Michelin (o incluso dos) si tuviera su propio restaurante. Por supuesto! Nadie será indiferente al probar su Bouillabaisse con salsa de corutina.
¡Durante toda una semana, el código en el producto no ha estado cayendo! Start/Stop Coroutine
Wrappers, callback
'y Start/Stop Coroutine
son muchos esclavos. Necesita más control y libertad de acción. Estás listo para subir el siguiente paso (pero no abandonar las corutinas, por supuesto).
Si te reconoces en estas líneas, bienvenido a cat.
Aprovecho esta oportunidad para saludar a uno de mis patrones favoritos: Command .
Introduccion
Para todas las instrucciones de yield
proporcionadas por Unity ( Coroutine
, AsyncOperation
, WaiForSeconds
y otras), la clase base es la clase YieldInstruction
( docs ) sin YieldInstruction
:
[StructLayout (LayoutKind.Sequential)] [UsedByNativeCode] public class YieldInstruction { }
Debajo del capó, las corutinas son enumeradores ordinarios ( IEnumerator
( docs )). Cada cuadro se llama mediante el método MoveNext()
. Si el valor de retorno es true
, la ejecución se retrasa hasta la siguiente declaración de yield
; si es false
, se detiene.
Para yield
instrucciones de yield
personalizadas, Unity proporciona la CustomYieldInstruction
( docs ) del mismo nombre.
public abstract class CustomYieldInstruction : IEnumerator { public abstract bool keepWaiting { get; } public object Current => null; public bool MoveNext(); public void Reset(); }
Es suficiente heredar de él e implementar la propiedad keepWaiting. Ya hemos discutido su lógica arriba. Debería volver true
siempre que se ejecute la rutina.
Ejemplo de la documentación:
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"); } }
Según ella, el keepWating
getter keepWating
ejecuta cada cuadro después de Update()
y antes de LateUpdate()
.
Si está trabajando con código en el que ciertas cosas deberían funcionar en un momento particular, y aún no ha estudiado esta página, le recomiendo decidirse por esta hazaña.
Instrucciones de rendimiento personalizadas

Pero no vinimos aquí con envoltorios regulares como CustomYieldInstruction
heredar. Para esto, puedes quedarte sin Michelin.
Por lo tanto, fumamos más la documentación y casi al final de la misma página encontramos el párrafo más importante.
Para tener más control e implementar instrucciones de rendimiento más complejas, puede heredar directamente de la clase System.Collections.IEnumerator
. En este caso, implemente el método MoveNext()
la misma manera que implementaría la propiedad keepWaiting
. Además de eso, también puede devolver un objeto en la propiedad Current
, que será procesado por el programador de rutina de Unity después de ejecutar el método MoveNext MoveNext()
. Entonces, por ejemplo, si Current
devolvió otro objeto heredado de IEnumerator
, entonces el enumerador actual se suspendería hasta que se complete el devuelto.
En rusoPara obtener más control e implementar instrucciones de yield
más complejas , puede heredar directamente de la interfaz System.Collections.IEnumerator
. En este caso, implemente el método MoveNext()
de la misma manera que la propiedad keepWaiting
. Además, puede usar el objeto en la propiedad Current
, que será procesada por el planificador de rutina Unity después de ejecutar el método MoveNext()
. Por ejemplo, si la propiedad Current
devuelve otro objeto que implementa la interfaz IEnumerator
, la ejecución del enumerador actual se retrasará hasta que se complete el nuevo.
Almejas santas! Estos son los controles y la libertad de acción que tanto deseaba. Bueno, eso es todo, ahora envolverás tantas corutinas que ningún Gimbal Lock da miedo. Lo principal, desde la felicidad, desde el vertukha no se golpea.
Interfaz
Bueno, sería bueno comenzar decidiendo qué interfaz de interacción con nuestra instrucción queremos tener. Un conjunto mínimo.
Sugiero la siguiente opción
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; }
Quiero llamar su atención sobre el hecho de que IsExecuting
e IsPaused
no IsPaused
lo opuesto aquí. Si la ejecución de la rutina se pausa, todavía se está ejecutando.
Solitario y cortesana
Como dice la documentación, debe implementar la interfaz IEnumerator
. En algunos lugares, por ahora, dejemos los apéndices, ya que su implementación depende directamente de la funcionalidad que queramos poner en él.
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() { } }
Vale la pena considerar que hay al menos dos formas en que nuestra instrucción puede iniciarse:
Método StartCoroutine(IEnumerator routine)
:
StartCoroutine(new ConcreteInstruction());
yield
rendimiento:
private IEnumerator SomeRoutine() { yield return new ConcreteInstruction(); }
El método Execute
, que describimos anteriormente en la interfaz IInstruction
, utilizará el primer método. Por lo tanto, agregamos algunos campos que ayudarán a administrar la instrucción en este caso.
private object routine; public MonoBehaviour Parent { get; private set; }
Ahora propiedades y eventos para 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; }
Además, los métodos para manejar eventos en clases secundarias:
protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { }
Se llamarán manualmente inmediatamente antes de los eventos correspondientes para dar a las clases secundarias la prioridad de su procesamiento.
Ahora los métodos restantes.
public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public bool Resume() { if (IsExecuting) { IsPaused = false; OnResumed(); } }
Todo es simple aquí. La instrucción puede pausarse solo si se está ejecutando y si no se ha suspendido, y continuar la ejecución solo si está suspendida.
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; }
La lógica básica para detener las instrucciones se ha trasladado al método Stop
. Esto es necesario para poder realizar una parada silenciosa (sin desencadenar eventos).
Comprobar if (routine is Coroutine)
necesario porque, como escribí anteriormente, la instrucción puede iniciarse mediante la yield
(es decir, sin llamar a StartCoroutine
), lo que significa que puede no haber un enlace a una instancia específica de Coroutine
. En este caso, la routine
solo tendrá un objeto auxiliar.
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; }
El método de inicio principal también es extremadamente simple: el inicio se realizará solo si la instrucción aún no se ha iniciado.
Queda por completar la implementación de IEnumerator
, porque dejamos espacios vacíos en algunos métodos.
IEnumerator.Reset() { IsExecuting = false; IsPaused = false; IsStopped = false; routine = null; }
Y lo más interesante, pero no más complicado, es 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)
: si la instrucción no se inició a través de StartCoroutine
y se ejecuta esta línea de código, la yield
lanzó. Escribimos un trozo en la routine
y los eventos de incendio.
if (current != null)
- current
usa para instrucciones secundarias. Si de repente esto ha aparecido, estamos esperando su final. Tenga en cuenta que extrañaré el proceso de agregar soporte para instrucciones secundarias en este artículo para no inflarlo aún más. Por lo tanto, si no está interesado en agregar más esta funcionalidad, simplemente puede eliminar estas líneas.
if (!Update())
: el método de Update
en nuestras instrucciones funcionará igual que keepWaiting
en CustomYieldInstruction
y debería implementarse en una clase secundaria. Instruction
es solo un método abstracto.
protected abstract bool Update();
Código de instrucciones completo 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(); }
Ejemplos
La clase base está lista. Para crear cualquier instrucción, es suficiente heredarla e implementar los miembros necesarios. Sugiero echar un vistazo a algunos ejemplos.
Mover al punto
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; } }
Opciones de lanzamiento 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); }
Procesamiento de entrada
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(); } }
Ejemplos de lanzamiento 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 };
Espera y pausa
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(); } }
Aquí, al parecer, todo es lo más simple posible. Primero, registramos la hora de inicio y luego verificamos la diferencia entre la hora actual y StartTime
. Pero hay un matiz que no puedes notar de inmediato. Si durante la ejecución de una instrucción la pausa (usando el método Pause
) y espera, luego de reanudar ( Resume
) se ejecutará instantáneamente. Y todo porque ahora esta instrucción no tiene en cuenta que puedo pausarla.
Tratemos de arreglar esta injusticia:
protected override void OnPaused() { Delay -= Time.time - startTime; } protected override void OnResumed() { startTime = Time.time; }
Listo Ahora, después del final de la pausa, la instrucción continuará ejecutándose tanto tiempo como le quedaba antes de comenzar.
Ejemplos de lanzamiento 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);
Resumen
Coroutines in Unity te permite hacer cosas increíbles y funcionales. Pero lo complejos que serán y si los necesita es una elección personal de todos.
Ampliar la funcionalidad de la corutina no es la tarea más difícil. Todo lo que necesita hacer es decidir qué interfaz desea interactuar con ellos y cuál será el algoritmo de su trabajo.
Gracias por su tiempo Deje sus preguntas / comentarios / adiciones en los comentarios. Estaré encantado de comunicarme.
PD Si, de repente, te quemaste por usar corutina, lee este comentario y bebe algo genial.