Corotinas personalizadas em união com preferência e cortesãs


Você já é tão legal que roda rotinas em torno de todos os eixos ao mesmo tempo; de uma só vez, elas executam yield break e se escondem atrás da tela do IDE. Invólucros simples são um estágio distante.


Você sabe como cozinhá-los tão bem que poderia conseguir uma estrela Michelin (ou até duas) se tivesse seu próprio restaurante. Claro! Ninguém ficará indiferente ao provar seu Bouillabaisse com molho de corotina.


Durante uma semana inteira, o código no prod não cai! Wrappers, callback 'e Start/Stop Coroutine são muitos escravos. Você precisa de mais controle e liberdade de ação. Você está pronto para subir o próximo passo (mas não saia das corotinas, é claro).


Se você se reconhece nessas linhas, bem-vindo ao gato.


Aproveito a oportunidade para dizer olá a um dos meus padrões favoritos - Comando .


1. Introdução


Para todas as instruções de yield fornecidas pelo Unity ( Coroutine , AsyncOperation , WaiForSeconds e outros), a classe base é a classe YieldInstruction ( docs ) não digna de YieldInstruction :


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

Sob o capô, as corotinas são enumeradores comuns ( IEnumerator ( docs )). Cada quadro é chamado pelo método MoveNext() . Se o valor de retorno for true , a execução será atrasada até a próxima instrução de yield ; se false , será interrompida.


Para yield instruções de yield personalizadas, o Unity fornece a CustomYieldInstruction ( docs ) com o mesmo nome.


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

Basta herdar e implementar a propriedade keepWaiting. Já discutimos sua lógica acima. Ele deve retornar true enquanto a corotina for executada.


Exemplo da documentação:


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

Segundo ela, o keepWating executado todos os quadros após Update() e antes de LateUpdate() .


Se você estiver trabalhando com código no qual certas coisas devem funcionar em um determinado momento e ainda não tiver estudado esta página, recomendo que você decida esse feito.


Instruções de rendimento personalizadas



Mas não viemos aqui com invólucros regulares, como o CustomYieldInstruction herdar. Para isso, você pode ficar sem Michelin.


Portanto, fumamos ainda mais a documentação e quase na parte inferior da mesma página encontramos o parágrafo mais importante.


Para ter mais controle e implementar instruções de rendimento mais complexas, você pode herdar diretamente da classe System.Collections.IEnumerator . Nesse caso, implemente o método MoveNext() da mesma maneira que implementaria a propriedade keepWaiting . Além disso, você também pode retornar um objeto na propriedade Current , que será processado pelo agendador de rotinas de rotina da Unity após executar o método MoveNext MoveNext() . Por exemplo, se Current retornasse outro objeto herdado de IEnumerator , o enumerador atual seria suspenso até que o retornado fosse concluído.


Em russo

Para obter mais controle e implementar instruções de yield mais complexas , você pode herdar diretamente da interface System.Collections.IEnumerator . Nesse caso, implemente o método MoveNext() da mesma maneira que a propriedade keepWaiting . Além disso, você pode usar o objeto na propriedade Current , que será processada pelo agendador de rotinas de rotina Unity após executar o método MoveNext() . Por exemplo, se a propriedade Current retornar outro objeto que implemente a interface IEnumerator , a execução do enumerador atual será atrasada até a conclusão do novo.


Amêijoas sagradas! Esses são os controles e a liberdade de ação que eu tanto queria. Bem, é isso, agora você embrulha tão corutins que nenhum Gimbal Lock é assustador. A principal coisa, da felicidade, incita a vertukha a não bater.


Interface


Bem, seria bom começar decidindo qual interface de interação com nossa instrução queremos ter. Algum conjunto mínimo.


Eu sugiro a seguinte opção


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

Quero chamar sua atenção para o fato de IsExecuting e IsPaused não IsPaused o oposto aqui. Se a execução da corotina estiver em pausa, ela ainda está sendo executada.


Paciência e cortesã


Como a documentação diz, você precisa implementar a interface IEnumerator . Em alguns lugares, por enquanto, vamos deixar os stubs, já que sua implementação depende diretamente da funcionalidade que queremos colocar nele.


 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 a pena considerar que existem pelo menos duas maneiras pelas quais nossas instruções podem ser lançadas:


  1. Método StartCoroutine(IEnumerator routine) :


     StartCoroutine(new ConcreteInstruction()); 

  2. yield rendimento:


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


O método Execute , que descrevemos acima na interface IInstruction , usará o primeiro método. Portanto, adicionamos alguns campos que ajudarão a gerenciar as instruções nesse caso.


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

Agora propriedades e 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; } 

Além disso, métodos para manipular eventos em classes filho:


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

Eles serão chamados manualmente imediatamente antes dos eventos correspondentes para dar às classes filhas a prioridade de seu processamento.


Agora os métodos restantes.


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

Tudo é simples aqui. A instrução pode ser pausada apenas se estiver sendo executada e se não tiver sido suspensa, e continuar a execução somente se estiver suspensa.


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

A lógica principal para parar as instruções é feita no método Stop . Isso é necessário para poder executar uma parada silenciosa (sem disparar eventos).
Verificando if (routine is Coroutine) necessário porque, como escrevi acima, a instrução pode ser iniciada pela yield (ou seja, sem chamar StartCoroutine ), o que significa que pode não haver link para uma instância específica da Coroutine . Nesse caso, a routine terá apenas um objeto de 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; } 

O método principal de inicialização também é extremamente simples - a inicialização será realizada apenas se a instrução ainda não tiver sido lançada.


Resta concluir a implementação do IEnumerator , porque deixamos espaços vazios em alguns métodos.


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

E o mais interessante, mas não mais complicado, é o 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) - se a instrução não foi iniciada por meio do StartCoroutine e essa linha de código for executada, a yield a StartCoroutine . Escrevemos um esboço na routine e nos eventos de incêndio.


if (current != null) - current usado para instruções filho. Se de repente isso apareceu, estamos aguardando o seu fim. Observe que sentirei falta do processo de adicionar suporte para instruções para crianças neste artigo para não aumentar ainda mais. Portanto, se você não estiver interessado em adicionar mais essa funcionalidade, basta remover essas linhas.


if (!Update()) - o método Update em nossas instruções funcionará exatamente como keepWaiting em CustomYieldInstruction e deve ser implementado em uma classe filho. Instruction é apenas um método abstrato.


 protected abstract bool Update(); 

Código de instrução 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(); } 

Exemplos


A classe base está pronta. Para criar qualquer instrução, basta herdá-la e implementar os membros necessários. Sugiro dar uma olhada em alguns exemplos.


Mover para o ponto


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

Opções de inicialização
 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); } 

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

Exemplos de Lançamento
 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; } 

Aguarde e faça uma 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(); } } 

Aqui, ao que parece, tudo é o mais simples possível. Primeiro, registramos a hora de início e depois verificamos a diferença entre a hora atual e o StartTime . Mas há uma nuance que você não pode perceber imediatamente. Se durante a execução de uma instrução você a pausar (usando o método Pause ) e aguardar, depois de reiniciar ( Resume ), ela será executada instantaneamente. E tudo porque agora esta instrução não leva em conta que eu posso pausá-la.


Vamos tentar consertar essa injustiça:


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

Feito. Agora, após o final da pausa, a instrução continuará sendo executada pelo tempo que faltava antes de começar.


Exemplos de Lançamento
 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; } 

Sumário


As corotinas no Unity permitem fazer coisas surpreendentes e funcionais. Mas quão complexas elas serão e se você precisa delas é a escolha pessoal de todos.


Estender a funcionalidade do corutin não é a tarefa mais difícil. Tudo que você precisa fazer é decidir qual interface você deseja interagir com eles e qual será o algoritmo do trabalho deles.


Obrigado pelo seu tempo. Deixe suas perguntas / comentários / acréscimos nos comentários. Ficarei feliz em me comunicar.




PS Se, de repente, você se cansou de usar corutin, leia este comentário e beba algo legal.

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


All Articles