
Vous êtes déjà tellement cool que vous faites pivoter les coroutines autour de tous les axes en même temps, d'un seul coup d'œil, elles effectuent yield break
et se cachent derrière le canevas IDE. Les emballages simples sont une étape révolue depuis longtemps.
Vous savez si bien les cuisiner que vous pourriez obtenir une étoile Michelin (voire deux) si vous aviez votre propre restaurant. Bien sûr! Personne ne sera indifférent en dégustant votre Bouillabaisse à la sauce coroutine.
Pendant toute une semaine, le code dans le prod ne tombe pas! Wrappers, callback
'et les Start/Stop Coroutine
sont le lot des esclaves. Vous avez besoin de plus de contrôle et de liberté d'action. Vous êtes prêt à monter la prochaine étape (mais pas à quitter les coroutines, bien sûr).
Si vous vous reconnaissez dans ces lignes, bienvenue au chat.
J'en profite pour dire bonjour à l'un de mes modèles préférés - Command .
Présentation
Pour toutes les instructions de yield
fournies par Unity ( Coroutine
, AsyncOperation
, WaiForSeconds
et autres), la classe de base est la classe YieldInstruction
( docs ) sans YieldInstruction
:
[StructLayout (LayoutKind.Sequential)] [UsedByNativeCode] public class YieldInstruction { }
Sous le capot, les coroutines sont des énumérateurs ordinaires ( IEnumerator
( docs )). Chaque image est appelée par la méthode MoveNext()
. Si la valeur de retour est true
, l'exécution est retardée jusqu'à la prochaine instruction yield
; si elle est false
, elle s'arrête.
Pour les instructions de yield
personnalisées, Unity fournit la CustomYieldInstruction
( docs ) du même nom.
public abstract class CustomYieldInstruction : IEnumerator { public abstract bool keepWaiting { get; } public object Current => null; public bool MoveNext(); public void Reset(); }
Il suffit d'en hériter et d'implémenter la propriété keepWaiting. Nous avons déjà discuté de sa logique ci-dessus. Elle doit retourner true
tant que la coroutine doit être exécutée.
Exemple tiré de la documentation:
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"); } }
Selon elle, le getter keepWating
exécuté chaque image après Update()
et avant LateUpdate()
.
Si vous travaillez avec du code dans lequel certaines choses devraient fonctionner à un moment donné, et que vous n'avez toujours pas étudié cette page, je vous recommande fortement de décider de cet exploit.
Instructions de rendement personnalisées

Mais nous ne sommes pas venus ici avec des wrappers réguliers comme CustomYieldInstruction
hériter. Pour cela, vous pouvez rester sans Michelin.
Par conséquent, nous fumons plus loin la documentation et presque au bas de la même page, nous trouvons le paragraphe le plus important.
Pour avoir plus de contrôle et implémenter des instructions de rendement plus complexes, vous pouvez hériter directement de la classe System.Collections.IEnumerator
. Dans ce cas, implémentez la méthode MoveNext()
la même manière que vous implémenteriez la propriété keepWaiting
. En plus de cela, vous pouvez également retourner un objet dans la propriété Current
, qui sera traité par le planificateur de coroutine d'Unity après avoir exécuté la méthode MoveNext MoveNext()
. Ainsi, par exemple, si Current
retourne un autre objet héritant d' IEnumerator
, l'énumérateur actuel est suspendu jusqu'à ce que celui renvoyé soit terminé.
En russePour obtenir plus de contrôle et implémenter des instructions de yield
plus complexes , vous pouvez hériter directement de l'interface System.Collections.IEnumerator
. Dans ce cas, implémentez la méthode MoveNext()
de la même manière que la propriété keepWaiting
. De plus, vous pouvez utiliser l'objet dans la propriété Current
, qui sera traité par le planificateur de coroutine Unity après l'exécution de la méthode MoveNext()
. Par exemple, si la propriété Current
renvoie un autre objet qui implémente l'interface IEnumerator
, l'exécution de l'énumérateur actuel sera retardée jusqu'à la fin du nouveau.
Saintes palourdes! Ce sont les contrôles et la liberté d'action que je voulais tant. Eh bien c'est ça, maintenant vous allez envelopper de corutines de telle sorte qu'aucun Gimbal Lock ne fait peur. L'essentiel, du bonheur, pousse le vertukha à ne pas cogner.
Interface
Eh bien, ce serait bien de commencer par décider quelle interface d'interaction avec notre instruction nous voulons avoir. Un ensemble minimal.
Je suggère l'option suivante
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; }
Je veux attirer votre attention sur le fait que IsExecuting
et IsPaused
ne IsPaused
pas le contraire ici. Si l'exécution de coroutine est suspendue, elle est toujours en cours d'exécution.
Solitaire et courtisane
Comme l'indique la documentation, vous devez implémenter l'interface IEnumerator
. Dans certains endroits, pour l'instant, laissons les talons, car son implémentation dépend directement de la fonctionnalité que nous voulons y mettre.
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() { } }
Il convient de considérer qu'il existe au moins deux façons de lancer notre instruction:
Méthode StartCoroutine(IEnumerator routine)
:
StartCoroutine(new ConcreteInstruction());
Déclaration de rendement:
private IEnumerator SomeRoutine() { yield return new ConcreteInstruction(); }
La méthode Execute
, que nous avons décrite ci-dessus dans l'interface IInstruction
, utilisera la première méthode. Par conséquent, nous ajoutons quelques champs qui aideront à gérer l'instruction dans ce cas.
private object routine; public MonoBehaviour Parent { get; private set; }
Maintenant propriétés et événements pour 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; }
En outre, les méthodes de gestion des événements dans les classes enfants:
protected virtual void OnStarted() { } protected virtual void OnPaused() { } protected virtual void OnResumed() { } protected virtual void OnTerminated() { } protected virtual void OnDone() { }
Ils seront appelés manuellement immédiatement avant les événements correspondants pour donner aux classes enfants la priorité de leur traitement.
Maintenant, les méthodes restantes.
public void Pause() { if (IsExecuting && !IsPaused) { IsPaused = true; OnPaused(); Paused?.Invoke(this); } } public bool Resume() { if (IsExecuting) { IsPaused = false; OnResumed(); } }
Ici, tout est simple. L'instruction ne peut être suspendue que si elle est en cours d'exécution et si elle n'a pas été suspendue, et poursuivre son exécution uniquement si elle est suspendue.
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 logique de base pour arrêter les instructions a été déplacée vers la méthode Stop
. Cela est nécessaire pour pouvoir effectuer un arrêt silencieux (sans déclencher d'événements).
Vérifier if (routine is Coroutine)
nécessaire car, comme je l'ai écrit ci-dessus, l'instruction peut être lancée par l' yield
(c'est-à-dire sans appeler StartCoroutine
), ce qui signifie qu'il peut n'y avoir aucun lien vers une instance spécifique de Coroutine
. Dans ce cas, la routine
n'aura qu'un objet stub.
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; }
La méthode de lancement principale est également extrêmement simple - le lancement ne sera effectué que si l'instruction n'a pas encore été lancée.
Il reste à terminer l'implémentation de IEnumerator
, car nous avons laissé des espaces vides dans certaines méthodes.
IEnumerator.Reset() { IsExecuting = false; IsPaused = false; IsStopped = false; routine = null; }
Et le plus intéressant, mais pas plus compliqué, est 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 l'instruction n'a pas été lancée via StartCoroutine
et que cette ligne de code est exécutée, alors l' yield
lancée. Nous écrivons un talon dans les événements de routine
et d'incendie.
if (current != null)
- current
utilisé pour les instructions enfants. Si tout à coup cela est apparu, nous attendons sa fin. Veuillez noter que je vais manquer le processus d'ajout de la prise en charge des instructions enfant dans cet article afin de ne pas le gonfler encore plus. Par conséquent, si vous n'êtes pas intéressé à ajouter davantage cette fonctionnalité, vous pouvez simplement supprimer ces lignes.
if (!Update())
- la méthode Update
de nos instructions fonctionnera comme keepWaiting
dans CustomYieldInstruction
et devrait être implémentée dans une classe enfant. Instruction
n'est qu'une méthode abstraite.
protected abstract bool Update();
Code d'instruction complet 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(); }
Des exemples
La classe de base est prête. Pour créer des instructions, il suffit d'en hériter et d'implémenter les membres nécessaires. Je suggère un regard sur quelques exemples.
Déplacer vers le point
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; } }
Options de lancement 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); }
Traitement d'entrée
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(); } }
Exemples de lancement 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 };
Attendez et faites une pause
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(); } }
Ici, semble-t-il, tout est aussi simple que possible. Tout d'abord, nous avons enregistré l'heure de début, puis vérifions la différence entre l'heure actuelle et StartTime
. Mais il y a une nuance que vous ne pouvez pas remarquer immédiatement. Si pendant l'exécution d'une instruction vous la mettez en pause (en utilisant la méthode Pause
) et attendez, alors après la reprise ( Resume
), elle sera exécutée instantanément. Et tout cela parce que maintenant cette instruction ne tient pas compte du fait que je peux la suspendre.
Essayons de réparer cette injustice:
protected override void OnPaused() { Delay -= Time.time - startTime; } protected override void OnResumed() { startTime = Time.time; }
C'est fait. Maintenant, après la fin de la pause, l'instruction continuera d'être exécutée autant de temps qu'il en restait avant de commencer.
Exemples de lancement 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);
Résumé
Coroutines in Unity vous permet de faire des choses incroyables et fonctionnelles. Mais combien ils seront complexes et si vous en avez besoin, c'est le choix personnel de chacun.
L'extension des fonctionnalités de la corutine n'est pas la tâche la plus difficile. Tout ce que vous avez à faire est de décider quelle interface vous souhaitez interagir avec eux et quel sera l'algorithme de leur travail.
Merci pour votre temps. Laissez vos questions / commentaires / ajouts dans les commentaires. Je serai ravi de communiquer.
PS Si, soudainement, vous avez été brûlé d'utiliser la corutine, lisez ce commentaire et buvez quelque chose de cool.