Cada desenvolvedor encontra suas vantagens e desvantagens do uso da corotina no Unity. E ele decide em quais cenários aplicá-los e em que dar preferência a alternativas.
Na prática cotidiana, costumo usar corotinas para vários tipos de tarefas. Certa vez, percebi que era eu quem aborrecia e rejeitava muitos recém-chegados.
Interface pesadelo
O mecanismo fornece apenas alguns métodos e várias de suas sobrecargas para trabalhar com corotinas:
Iniciar ( docs )
- StartCoroutine(string name, object value = null)
- StartCoroutine(IEnumerator routine)
Parar ( documentos )
- StopCoroutine(string methodName)
- StopCoroutine(IEnumerator routine)
- StopCoroutine(Coroutine routine)
Sobrecargas com parâmetros de string (apesar de sua conveniência enganosa) podem imediatamente enviar para o lixo esqueça por pelo menos três razões.
- O uso explícito de nomes de métodos de string complicará a análise futura de códigos, depuração e muito mais.
- De acordo com a documentação, as sobrecargas de string demoram mais e permitem que apenas um parâmetro adicional seja passado.
- Na minha prática, muitas vezes acontecia que nada acontecia quando uma sobrecarga de string StopCoroutineeraStopCoroutine. Corutin continuou a ser executado.
Por um lado, os métodos fornecidos são suficientes para atender às necessidades básicas. Mas com o tempo, comecei a perceber que, com o uso ativo, tenho que escrever uma grande quantidade de código padrão - isso é cansativo e prejudica sua legibilidade.
Mais perto do ponto
Neste artigo, quero descrever um pequeno invólucro que utilizo há muito tempo. Graças a ela, com pensamentos sobre as corotinas, fragmentos do código do modelo não aparecem mais na minha cabeça, com os quais tive que dançar ao redor deles. Além disso, toda a equipe ficou mais fácil de ler e entender os componentes onde as corotinas são usadas.
Suponha que tenhamos a seguinte tarefa - escrever um componente que permita mover um objeto para um determinado ponto.
Nesse momento, não importa qual método será usado para mover e em quais coordenadas. Somente uma das muitas opções será selecionada por mim - estas são interpolação e coordenadas globais.
Observe que é altamente recomendável não mover um objeto alterando suas coordenadas “na testa”, ou seja, o construto transform.position = newPosition se o componente RigidBody for usado com ele (especialmente no método Update ) ( docs ).
Implementação padrão
Proponho a seguinte opção de implementação para o componente necessário:
 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; public void Move() { if (moveRoutine == null) StartCoroutine(MoveRoutine()); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 
Um pouco sobre códigoNo método Move , é muito importante executar a corotina apenas quando ainda não estiver em execução. Caso contrário, eles podem ser lançados quantos forem desejados e cada um deles moverá o objeto.
threshold - tolerância. Em outras palavras, a distância até o ponto, aproximando-se da qual assumiremos que alcançamos a meta.
Para que serve?
Dado que todos os componentes ( x , y , z ) da estrutura Vector3 são do tipo float , é uma má idéia usar o resultado da verificação da igualdade de distância do alvo e da tolerância como condição do loop.
Verificamos a distância do alvo para mais / menos, o que nos permite evitar esse problema.
Além disso, se desejar, você pode usar o Mathf.Approximately ( docs ) para uma verificação aproximada da igualdade. Vale a pena notar que, com alguns métodos de movimento, a velocidade pode ser grande o suficiente para que o objeto “salte” o alvo em um quadro. Então o ciclo nunca terminará. Por exemplo, se você usar o método Vector3.MoveTowards .
Até Vector3 eu sei, no mecanismo do Unity para a estrutura Vector3 , o operador Vector3 já está redefinido de tal forma que Mathf.Approximately é chamado para verificar a igualdade entre os componentes.
 Por enquanto é tudo, nosso componente é bastante simples. E no momento não há problemas. Mas, qual é esse componente que permite mover um objeto para um ponto, mas não oferece uma oportunidade para interrompê-lo. Vamos consertar essa injustiça.
Como você e eu decidimos não passar para o lado ruim, e não usar sobrecargas com parâmetros de string, agora precisamos salvar em algum lugar um link para a corotina em execução. Caso contrário, como então impedi-la?
Adicione um campo:
 private Coroutine moveRoutine; 
Fix Move :
 public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); } 
Adicione um método de stop motion:
 public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); } 
Código inteiro using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { if (moveRoutine == null) moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) StopCoroutine(moveRoutine); } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 
 Uma questão completamente diferente! Pelo menos aplique na ferida.
Então Temos um pequeno componente que executa a tarefa. Qual é a minha indignação?
Problemas e sua solução
Com o tempo, o projeto cresce e, com ele, o número de componentes, incluindo aqueles que utilizam corotinas. E cada vez essas coisas estão me assombrando cada vez mais:
- Chamadas sanduíche em andamento
 StartCoroutine(MoveRoutine()); 
 StopCoroutine(moveRoutine); 
Só de olhar para eles, meus olhos se contraem e ler um código desse tipo é um prazer duvidoso (concordo que pode ser pior). Mas seria muito melhor e mais visual ter algo assim:
 moveRoutine.Start(); 
 moveRoutine.Stop(); 
- Sempre que você chamar StartCoroutinelembre-se de salvar o valor de retorno:
 moveRoutine = StartCoroutine(MoveRoutine()); 
Caso contrário, devido à falta de referência à corotina, você simplesmente não poderá pará-la.
 if (moveRoutine == null) 
 if (moveRoutine != null) 
- E outra coisa má que você deve sempre lembrar (e que eu esqueci de novoespecialmente perdido). No final da corotina e antes de cada saída dela (por exemplo, usandoyield break), o valor do campo deve ser redefinido.
  moveRoutine = null;
 
Se você esquecer, receberá uma rotina única. Após a primeira execução em moveRoutine , o link para a corotina permanecerá e a nova execução não funcionará.
Da mesma maneira, você precisa fazer no caso de uma parada forçada:
 public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } } 
Código com todas as alterações public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private Coroutine moveRoutine; public void Move() { moveRoutine = StartCoroutine(MoveRoutine()); } public void Stop() { if (moveRoutine != null) { StopCoroutine(moveRoutine); moveRoutine = null; } } private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null; } } 
 A certa altura, eu realmente quero tirar toda essa máscara em algum lugar e deixar apenas os métodos necessários: Start , Stop e mais alguns eventos e propriedades.
Vamos finalmente fazer isso!
 using System.Collections; using System; using UnityEngine; public sealed class CoroutineObject { public MonoBehaviour Owner { get; private set; } public Coroutine Coroutine { get; private set; } public Func<IEnumerator> Routine { get; private set; } public bool IsProcessing => Coroutine != null; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if (IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 
DebriefingOwner - um link para a instância MonoBehaviour à qual a corotina será anexada. Como você sabe, ele deve ser executado no contexto de um componente específico, pois é para ele que os métodos StopCoroutine e StopCoroutine . Portanto, precisamos de um link para o componente que será o proprietário da corotina.
Coroutine - um análogo do campo moveRoutine no componente MoveToPoint , contém um link para a corotina atual.
Routine - o delegado com quem o método que atua como a rotina será comunicado.
Process() é um pequeno invólucro sobre o método principal de Routine . É necessário para poder rastrear quando a execução da corotina estiver concluída, redefinir o link para ele e executar outro código naquele momento (se necessário).
IsProcessing - permite descobrir se a corotina está em execução no momento.
 Assim, nos livramos de uma grande quantidade de dor de cabeça e nosso componente assume uma aparência completamente diferente:
 using IEnumerator = System.Collections; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public Vector3 target; [Space] public float speed; public float threshold; private CoroutineObject moveRoutine; private void Awake() { moveRoutine = new CoroutineObject(this, MoveRoutine); } public void Move() => moveRoutine.Start(); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine() { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } } } 
Tudo o que resta é a própria rotina e algumas linhas de código para trabalhar com ela. Significativamente melhor.
Suponha que uma nova tarefa tenha chegado - você precisa adicionar a capacidade de executar qualquer código depois que o objeto atingir seu objetivo.
Na versão original, teríamos que adicionar um parâmetro delegado adicional a cada rotina que pode ser puxada após sua conclusão.
 private IEnumerator MoveRoutine(System.Action callback) { while (Vector3.Distance(transform.position, target) > threshold) { transform.position = Vector3.Lerp(transform.position, target, speed * Time.deltaTime); yield return null; } moveRoutine = null callback?.Invoke(); } 
E ligue da seguinte maneira:
 moveRoutine = StartCoroutine(moveRoutine(CallbackHandler)); private void CallbackHandler() {  
E se houver algum lambda como manipulador, ele parecerá ainda pior.
Com o nosso wrapper, basta adicionar este evento a ele apenas uma vez.
 public Action Finish; 
 private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finish?.Invoke(); } 
E então, se necessário, assine.
 moveRoutine.Finished += OnFinish; private void OnFinish() {  
Acredito que você já tenha notado que a versão atual do wrapper oferece a capacidade de trabalhar apenas com corotinas sem parâmetros. Portanto, podemos escrever um wrapper generalizado para a corotina com um parâmetro. O resto é feito por analogia.
Mas, para o bem, seria bom primeiro colocar o código, que será o mesmo para todos os wrappers, em alguma classe base, para não escrever a mesma coisa. Estamos lutando contra isso.
Nós o removemos:
- Ownerpropriedades,- IsProcessing,- IsProcessing
- Evento Finished
 using Action = System.Action; using UnityEngine; public abstract class CoroutineObjectBase { public MonoBehaviour Owner { get; protected set; } public Coroutine Coroutine { get; protected set; } public bool IsProcessing => Coroutine != null; public abstract event Action Finished; } 
Invólucro sem parâmetros após refatoração using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject : CoroutineObjectBase { public Func<IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process() { yield return Routine.Invoke(); Coroutine = null; Finished?.Invoke(); } public void Start() { Stop(); Coroutine = Owner.StartCoroutine(Process()); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 
 E agora, de fato, um invólucro para corotina com um parâmetro:
 using System; using System.Collections; using UnityEngine; public sealed class CoroutineObject<T> : CoroutineObjectBase { public Func<T, IEnumerator> Routine { get; private set; } public override event Action Finished; public CoroutineObject(MonoBehaviour owner, Func<T, IEnumerator> routine) { Owner = owner; Routine = routine; } private IEnumerator Process(T arg) { yield return Routine.Invoke(arg); Coroutine = null; Finished?.Invoke(); } public void Start(T arg) { Stop(); Coroutine = Owner.StartCoroutine(Process(arg)); } public void Stop() { if(IsProcessing) { Owner.StopCoroutine(Coroutine); Coroutine = null; } } } 
Como você pode ver, o código é quase o mesmo. Somente em alguns lugares foram adicionados fragmentos, dependendo do número de argumentos.
Suponha que nos pedissem para atualizar o componente MoveToPoint para que o ponto pudesse ser definido não pela janela do Inspector no editor, mas por código quando o método Move foi chamado.
 using IEnumerator = System.Collections.IEnumerator; using UnityEngine; public sealed class MoveToPoint : MonoBehaviour { public float speed; public float threshold; private CoroutineObject<Vector3> moveRoutine; public bool IsMoving => moveRoutine.IsProcessing; private void Awake() { moveRoutine = new CoroutineObject<Vector3>(this, MoveRoutine); } public void Move(Vector3 target) => moveRoutine.Start(target); public void Stop() => moveRoutine.Stop(); private IEnumerator MoveRoutine(Vector3 target) { while (Vector3.Distance(transform.position, target) > threshold) { transform.localPosition = Vector3.Lerp(transform.position, target, speed); yield return null; } } } 
Há muitas opções para expandir a funcionalidade desse wrapper, tanto quanto possível: adicione uma inicialização atrasada, eventos com parâmetros, possível rastreamento do progresso da corotina e muito mais. Mas sugiro parar nesta fase.
O objetivo deste artigo é o desejo de compartilhar os problemas prementes que encontrei e propor uma solução para eles, e não cobrir as possíveis necessidades de todos os desenvolvedores.
Espero que iniciantes e camaradas experientes se beneficiem da minha experiência. Talvez eles compartilhem seus comentários ou apontem os erros que eu poderia ter cometido.