O link de detalhe em detalhe das rotinas detalhadas do Unity3D está morto. Como é mencionado nos comentários e nas respostas, vou postar o conteúdo do artigo aqui. Este conteúdo vem deste espelho .
Rotinas do Unity3D em detalhes
Muitos processos nos jogos ocorrem ao longo de vários quadros. Você tem processos "densos", como a busca de caminhos, que trabalha duro em cada quadro, mas se divide em vários quadros para não afetar muito a taxa de quadros. Você tem processos "esparsos", como gatilhos de jogabilidade, que não fazem mais quadros, mas ocasionalmente são chamados a fazer um trabalho crítico. E você tem processos variados entre os dois.
Sempre que você estiver criando um processo que ocorrerá em vários quadros - sem multithreading -, você precisará encontrar uma maneira de dividir o trabalho em partes que podem ser executadas uma por quadro. Para qualquer algoritmo com um loop central, é bastante óbvio: um descobridor A *, por exemplo, pode ser estruturado de forma a manter suas listas de nós semi-permanentemente, processando apenas alguns nós da lista aberta em cada quadro, em vez de tentar para fazer todo o trabalho de uma só vez. Há algum balanceamento a ser feito para gerenciar a latência - afinal, se você estiver bloqueando a taxa de quadros em 60 ou 30 quadros por segundo, seu processo executará apenas 60 ou 30 etapas por segundo, o que pode fazer com que o processo demore apenas muito longo no geral. Um design elegante pode oferecer a menor unidade de trabalho possível em um nível - por exemplo, processe um único nó A * - e aplique uma camada de agrupamento em partes maiores - por exemplo, continue processando nós A * por X milissegundos. (Algumas pessoas chamam isso de 'timeslicing', embora eu não).
Ainda assim, permitir que o trabalho seja dividido dessa maneira significa que você precisa transferir o estado de um quadro para o outro. Se você estiver quebrando um algoritmo iterativo, precisará preservar todo o estado compartilhado nas iterações, bem como um meio de rastrear qual iteração será executada a seguir. Isso geralmente não é muito ruim - o design de uma 'classe A * pathfinder' é bastante óbvio - mas também há outros casos que são menos agradáveis. Às vezes, você estará enfrentando longos cálculos que estão realizando diferentes tipos de trabalhos de quadro a quadro; o objeto que captura seu estado pode acabar com uma grande confusão de 'locais' semi-úteis, mantidos para passar dados de um quadro para o outro. E se você estiver lidando com um processo esparso, muitas vezes acaba tendo que implementar uma pequena máquina de estado apenas para rastrear quando o trabalho deve ser feito.
Não seria legal se, em vez de rastrear explicitamente todo esse estado em vários quadros, e em vez de multithread e gerenciar a sincronização e o bloqueio e assim por diante, você pudesse escrever sua função como um único pedaço de código e marcar lugares específicos onde a função deve 'pausar' e continuar mais tarde?
A unidade - junto com vários outros ambientes e idiomas - fornece isso na forma de corotinas.
Como eles se parecem? Em "Unityscript" (Javascript):
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
Em c #:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
Como eles funcionam? Deixe-me dizer, rapidamente, que não trabalho para a Unity Technologies. Eu não vi o código fonte do Unity. Eu nunca vi a coragem do mecanismo de rotina da Unity. No entanto, se eles o implementaram de uma maneira radicalmente diferente do que estou prestes a descrever, ficarei surpreso. Se alguém da UT quiser conversar e falar sobre como ele realmente funciona, isso seria ótimo.
As grandes pistas estão na versão C #. Em primeiro lugar, observe que o tipo de retorno para a função é IEnumerator. E segundo, observe que uma das declarações é retorno de rendimento. Isso significa que o rendimento deve ser uma palavra-chave e, como o suporte ao C # do Unity é baunilha C # 3.5, deve ser uma palavra-chave baunilha C # 3.5. De fato, aqui está no MSDN - falando sobre algo chamado 'blocos iteradores'. Então o que está acontecendo?
Em primeiro lugar, há esse tipo de IEnumerator. O tipo IEnumerator atua como um cursor sobre uma sequência, fornecendo dois membros significativos: Current, que é uma propriedade que fornece o elemento sobre o qual o cursor está atualmente, e MoveNext (), uma função que se move para o próximo elemento na sequência. Como o IEnumerator é uma interface, ele não especifica exatamente como esses membros são implementados; MoveNext () poderia apenas adicionar um a Current, ou poderia carregar o novo valor de um arquivo, ou baixar uma imagem da Internet e hash e armazenar o novo hash no Current… ou até mesmo fazer uma coisa pela primeira vez. elemento na sequência e algo totalmente diferente para o segundo. Você pode até usá-lo para gerar uma sequência infinita, se desejar. MoveNext () calcula o próximo valor na sequência (retornando false se não houver mais valores),
Normalmente, se você quiser implementar uma interface, terá que escrever uma classe, implementar os membros e assim por diante. Os blocos do iterador são uma maneira conveniente de implementar o IEnumerator sem todo esse aborrecimento - basta seguir algumas regras e a implementação do IEnumerator é gerada automaticamente pelo compilador.
Um bloco iterador é uma função regular que (a) retorna IEnumerator e (b) usa a palavra-chave yield. Então, o que a palavra-chave yield realmente faz? Ele declara qual é o próximo valor na sequência - ou que não há mais valores. O ponto em que o código encontra um retorno de rendimento X ou quebra de rendimento é o ponto em que IEnumerator.MoveNext () deve parar; um retorno de rendimento X faz com que MoveNext () retorne verdadeiro e atual seja atribuído o valor X, enquanto uma quebra de rendimento faz com que MoveNext () retorne falso.
Agora, aqui está o truque. Não precisa importar quais são os valores reais retornados pela sequência. Você pode chamar MoveNext () repetidamente e ignorar Current; os cálculos ainda serão realizados. Cada vez que MoveNext () é chamado, seu bloco iterador é executado na próxima instrução 'yield', independentemente da expressão que ele produz. Então você pode escrever algo como:
IEnumerator TellMeASecret()
{
PlayAnimation("LeanInConspiratorially");
while(playingAnimation)
yield return null;
Say("I stole the cookie from the cookie jar!");
while(speaking)
yield return null;
PlayAnimation("LeanOutRelieved");
while(playingAnimation)
yield return null;
}
e o que você realmente escreveu é um bloco iterador que gera uma longa sequência de valores nulos, mas o que é significativo são os efeitos colaterais do trabalho que faz para calculá-los. Você pode executar essa rotina usando um loop simples como este:
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
Ou, mais útil, você pode misturá-lo com outro trabalho:
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press 'Escape', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
Está tudo dentro do prazo Como você viu, cada declaração de retorno de rendimento deve fornecer uma expressão (como nula) para que o bloco iterador tenha algo a ser realmente atribuído ao IEnumerator.Current. Uma longa sequência de nulos não é exatamente útil, mas estamos mais interessados nos efeitos colaterais. Não somos?
Há algo útil que podemos fazer com essa expressão, na verdade. E se, em vez de apenas produzir nulo e ignorá-lo, produzimos algo que indicava quando esperamos precisar fazer mais trabalho? Frequentemente, precisamos continuar direto no próximo quadro, com certeza, mas nem sempre: haverá muitas vezes em que queremos continuar após a animação ou som terminar de tocar, ou após um determinado período de tempo. Aqueles while (playingAnimation) produzem retorno nulo; construções são um pouco tediosas, você não acha?
O Unity declara o tipo base YieldInstruction e fornece alguns tipos derivados concretos que indicam tipos específicos de espera. Você tem WaitForSeconds, que retoma a corotina após o tempo designado. Você tem WaitForEndOfFrame, que retoma a corotina em um ponto específico posteriormente no mesmo quadro. Você tem o próprio tipo de corotina que, quando a corotina A produz a corotina B, interrompe a corotina A até que a corotina B termine.
Como isso se parece do ponto de vista do tempo de execução? Como eu disse, não trabalho para o Unity, então nunca vi o código deles; mas eu imagino que possa parecer um pouco com isso:
List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;
foreach(IEnumerator coroutine in unblockedCoroutines)
{
if(!coroutine.MoveNext())
// This coroutine has finished
continue;
if(!coroutine.Current is YieldInstruction)
{
// This coroutine yielded null, or some other value we don't understand; run it next frame.
shouldRunNextFrame.Add(coroutine);
continue;
}
if(coroutine.Current is WaitForSeconds)
{
WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{
shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */
}
unblockedCoroutines = shouldRunNextFrame;
Não é difícil imaginar como mais subtipos YieldInstruction poderiam ser adicionados para lidar com outros casos - o suporte de sinais no nível do mecanismo, por exemplo, poderia ser adicionado, com um WaitForSignal ("SignalName") YieldInstruction suportando-o. Ao adicionar mais YieldInstructions, as próprias corotinas podem se tornar mais expressivas - retornar retorno novo WaitForSignal ("GameOver") é mais agradável de ler do que isso (! Signals.HasFired ("GameOver")) retorna retorno nulo, se você me perguntar, bem diferente de o fato de fazê-lo no mecanismo pode ser mais rápido do que fazê-lo no script.
Algumas ramificações não óbvias Há algumas coisas úteis sobre tudo isso que às vezes as pessoas sentem falta que eu pensei que deveria apontar.
Em primeiro lugar, o retorno do rendimento está apenas produzindo uma expressão - qualquer expressão - e YieldInstruction é um tipo regular. Isso significa que você pode fazer coisas como:
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
As linhas específicas produzem retorno novo WaitForSeconds (), produzem retorno novo WaitForEndOfFrame (), etc, são comuns, mas na verdade não são formulários especiais.
Em segundo lugar, como essas corotinas são apenas blocos de iteradores, você pode iterar sobre elas, se quiser - não precisa que o mecanismo faça isso por você. Eu usei isso para adicionar condições de interrupção a uma corotina antes:
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
Em terceiro lugar, o fato de você poder ceder em outras corotinas pode permitir que você implemente suas próprias YieldInstructions, embora não com o mesmo desempenho que se fossem implementadas pelo mecanismo. Por exemplo:
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
no entanto, eu realmente não recomendaria isso - o custo de iniciar uma Coroutine é um pouco pesado para o meu gosto.
Conclusão Espero que isso esclareça um pouco do que realmente está acontecendo quando você usa uma Coroutine no Unity. Os blocos iteradores do C # são uma construção pequena e divertida, e mesmo se você não estiver usando o Unity, talvez seja útil tirar proveito deles da mesma maneira.
IEnumerator
/IEnumerable
(ou os equivalentes genéricos) e que contêm ayield
palavra - chave. Procure iteradores.