Por que não estou ficando preso no loop


8

Eu sou novo no Unity. Eu estava aprendendo corotinas e escrevi isso.

private void Fire()
{
    if(Input.GetButtonDown("Fire1"))
    {
        StartCoroutine(FireContinuously());
    }
    if(Input.GetButtonUp("Fire1"))
    {
        StopAllCoroutines();
    }
}

IEnumerator FireContinuously()
{
    while(true)
    {
        GameObject laser = Instantiate(LaserPrefab, transform.position, Quaternion.identity) as GameObject;
        laser.GetComponent<Rigidbody2D>().velocity = new Vector2(0, 10f);
        yield return new WaitForSeconds(firetime);
    }
}

Quando o botão é pressionado, a corotina é chamada e entra no loop 'while'. Quando deixo o botão, ele interrompe a corotina. Não deveria ficar preso no loop 'while', pois é um loop infinito? Por quê?


Acabei de voltar recentemente para o Unity, notei que os métodos de entrada estão usando uma string "Fire1", é algo que você pode configurar no mecanismo para permitir remapeamentos importantes em vez de digitar Keycode.Foo?
Mkalafut 6/08/19

11
Pode ser yieldútil perceber que é efetivamente a abreviação de "Rendimento do controle para o chamador até que o próximo item no Enumerável seja solicitado".
3Dave

@Mkalafut parece algo a ser perguntado em uma nova pergunta, se você não conseguir encontrar a resposta nas páginas de documentação , nos tutoriais ou em suas próprias experiências da documentação do Unity .
DMGregory

Eu não recomendo StopAllCoroutines()neste caso. Tudo bem quando você está apenas usando uma corotina, mas se você planeja ter mais de uma, isso terá efeitos indesejados. Em vez disso, você deve usar StopCoroutine()e parar o que é relevante em vez de todos eles. ( StopAllCoroutines()Seria útil, por exemplo, quando terminar o nível ou o carregamento de uma nova área, etc., mas não para o material específico, como "Eu não estou disparando mais.")
Darrel Hoffman

Respostas:


14

O motivo é a palavra-chaveyield que possui um significado específico em C #.

Ao encontrar as palavras, yield returnuma função em C # retorna, como seria de esperar.

Usar yield para definir um iterador elimina a necessidade de uma classe extra explícita

[...]

Quando uma instrução de retorno de rendimento é alcançada no método iterador, a expressão é retornada e o local atual no código é mantido. A execução é reiniciada a partir desse local na próxima vez que a função iteradora for chamada.

Portanto, não há loop infinito. Existe uma função / iterador que pode ser chamado um número infinito de vezes.

A função Unity StartCoroutine()faz com que a estrutura do Unity chame a função / iterador uma vez por quadro.

A função Unity StopAllCoroutinesfaz com que a estrutura do Unity pare de chamar a função / iterador.

E retornar WaitForSeconds(time)do iterador faz com que a estrutura do Unity suspenda a chamada da função / iterador time.


Um comentário confuso e um voto positivo igualmente confuso sobre esse comentário me incentivaram a aprofundar o que a palavra-chave yieldfaz e não faz.

Se você escrever isso:

IEnumerable<int> Count()
{
   int i = 0;
   yield return i++;
}

Você também pode escrever o seguinte:

IEnumerator<int> Count() {
    return new CountEnumerator ();
}
class CountEnumerator : IEnumerator<int> {
    int i = 0;
    bool IEnumerator<int>.MoveNext() { i++; return true; }
    int IEnumerator<int>.Current { get { return i; }
    void IEnumerator<int>.Reset() { throw new NotSupportedException(); }
}

Daqui resulta que a palavra yield- chave não está relacionada com multiencadeamento e absolutamente não é chamada System.Threading.Thread.Yield().


11
" On encountering the words yield return a function in C# returns" Não, não tem. O texto que você cita explica, assim como a Wikipedia - " In computer science, yield is an action that occurs in a computer program during multithreading, of forcing a processor to relinquish control of the current running thread, and sending it to the end of the running queue, of the same scheduling priority.". Basicamente, "` por favor, faça uma pausa onde estou e deixe outra pessoa correr por um tempo ".
Mawg diz que restabelece Monica em 07/08/19

2
Adicionei uma segunda parte à resposta para resolver sua preocupação.
Peter

Muito obrigado por esclarecer (votado). Eu certamente aprendi algo novo hoje :-)
Mawg diz que restabelece Monica em 8/08/19

8

Quando o botão de disparo é pressionado, o segundo se a instrução é inserida e o StopAllCoroutines é executado. Isso significa que a Corotina na qual o loop while está sendo finalizado termina, portanto, não há mais loop infinito. A corotina é como um contêiner para o código executar.

Posso recomendar o Manual do Unity e a API de script do Unity para entender melhor o que são as corotinas e o quão poderosas elas podem ser.

Este blog e a postagem no YouTube também foram úteis para eu usar melhor as corotinas.


3

As corotinas são um animal estranho. O retorno de rendimento faz com que o método suspenda a execução até que seja posteriormente escalado. Nos bastidores, pode ser algo como isto:

class FireContinuouslyData {
    int state;
    bool shouldBreak;
}

object FireContinuously(FireContinuouslyData data) {
    switch (data.state) {
        case 0:
            goto State_0;
    }
    while (true) {
        GameObject laser = ...;
        laser.GetComponent...
        //the next three lines handle the yield return
        data.state = 0;
        return new WaitForSeconds(fireTime);
        State_0:
    }
}

E interno ao Unity / C # (como o retorno de rendimento é um recurso nativo de c #), quando você chama StartCoroutine, ele cria um FireContinuouslyDataobjeto e o transmite ao método. Com base no valor de retorno, ele determina quando chamá-lo novamente mais tarde, simplesmente armazenando o objeto FireContinuouslyData para transmiti-lo na próxima vez.

Se você alguma vez quebrasse o rendimento, ele poderia ser definido internamente data.shouldBreak = truee o Unity simplesmente descartaria os dados e não os agendaria novamente.

E se houvesse algum dado que precisasse ser salvo entre as execuções, ele também seria armazenado nos dados para mais tarde.

Um exemplo de como o Unity / C # pode implementar a funcionalidade da rotina:

//Internal to Unity/C#

class Coroutine {
    Action<object> method;
    object data;
}

Coroutine StartCoroutine(IEnumerator enumerator) {
    object data = CreateDataForEnumerator(method); //Very internal to C#
    Action<object> method = GetMethodForEnumerator(enumerator); //Also very internal to C#
    Coroutine coroutine = new Coroutine(method, data);
    RunCoroutine(coroutine);
    return coroutine;
}

//Called whenever this coroutine is scheduled to run
void RunCoroutine(Coroutine coroutine) {
    object yieldInstruction = coroutine.method(coroutine.data);
    if (!data.shouldBreak) {
        //Put this coroutine into a collection of coroutines to run later, by calling RunCoroutine on it again
        ScheduleForLater(yieldInstruction, coroutine);
    }
}

1

Outra resposta menciona que você está interrompendo as co-rotinas quando "Fire1"terminar - isso é completamente correto, pois a corotina não continua instanciando GameObjects após o primeiro pressionamento de "Fire1".

No seu caso, no entanto, esse código não ficará "preso" em um loop infinito, que é o que parece que você está procurando uma resposta - ou seja, o while(true) {}loop, mesmo que você não o tenha parado externamente.

Ele não fica preso, mas a sua rotina não termina (sem ligar StopCoroutine()ou StopAllCoroutines()). Isso ocorre porque as corotinas do Unity cedem controle ao chamador. yielding é diferente de returning:

  • uma returninstrução interromperá a execução de uma função, mesmo se houver mais código após ela
  • uma yieldinstrução pausará a função, iniciando na próxima linha depois de yieldretomada.

Normalmente, as corotinas serão retomadas a cada quadro, mas você também retornará um WaitForSecondsobjeto.

A linha yield return new WaitForSeconds(fireTime)se traduz aproximadamente como "agora me suspenda e não volte até que os fireTimesegundos se passem".

IEnumerator FireContinuously()
{
    // When started, this coroutine enters the below while loop...
    while(true)
    {
        // It does some things... (Infinite coroutine code goes here)

        // Then it yields control back to it's caller and pauses...
        yield return new WaitForSeconds(fireTime);
        // The next time it is called , it resumes here...
        // It finds the end of a loop, so will re-evaluate the loop condition...
        // Which passes, so control is returned to the top of the loop.
    }
}

A menos que parado, esta é uma rotina que, uma vez iniciada, executará todo o loop a cada fireTimesegundo.


1

Uma explicação simples: sob o capô, o Unity está interagindo sobre uma coleção (de YieldInstruction s ou nulls ou o que você quiser yield return) usando o IEnumeratorque sua função retorna.

Como você usa a yieldpalavra - chave, seu método é um iterador . Não é a coisa do Unity, é um recurso da linguagem C #. Como funciona?

É preguiçoso e não gera toda a coleção de uma só vez (e a coleção pode ser infinita e impossível de ser gerada ao mesmo tempo). Os elementos da coleção são gerados conforme necessário. Sua função retorna um iterador para o Unity trabalhar. Ele chama seu MoveNextmétodo para gerar um novo elemento e Currentpropriedade para acessá-lo.

Portanto, seu loop não é infinito, ele executa algum código, retorna um elemento e retorna o controle de volta ao Unity, para que não fique preso e possa fazer outro trabalho, como manipular sua entrada para interromper a corotina.


0

Pense em como foreachfunciona:

foreach (var number in Enumerable.Range(1, 1000000))
{
  if (number > 10) break;
}

O controle sobre a iteração está no chamador - se você parar a iteração (aqui com break), é isso.

A yieldpalavra-chave é uma maneira simples de tornar um enumerável em C #. O nome sugere isso - yield returnretorna o controle ao chamador (nesse caso, nosso foreach); é o chamador que decide quando continuar para o próximo item. Então você pode criar um método como este:

IEnumerable<int> ToInfinity()
{
  var i = 0;
  while (true) yield return i++;
}

Parece ingênuo que funcionará para sempre; mas, na realidade, depende inteiramente do chamador. Você pode fazer algo assim:

var range = ToInfinity().Take(10).ToArray();

Isso pode ser um pouco confuso se você não estiver acostumado a esse conceito, mas espero que também seja óbvio que essa é uma propriedade muito útil. Era a maneira mais simples de fornecer controle ao seu interlocutor e, quando o interlocutor decide fazer o acompanhamento, ele pode executar o próximo passo (se o Unity fosse feito hoje, provavelmente seria usado em awaitvez de yield; mas awaitnão existia de volta). então).

Tudo o que você precisa para implementar suas próprias rotinas (desnecessário dizer, as rotinas mais simples e estúpidas) é o seguinte:

List<IEnumerable> continuations = new List<IEnumerable>();

void StartCoroutine(IEnumerable coroutine) => continuations.Add(coroutine);

void MainLoop()
{
  while (GameIsRunning)
  {
    foreach (var continuation in continuations.ToArray())
    {
      if (!continuation.MoveNext()) continuations.Remove(continuation);
    }

    foreach (var gameObject in updateableGameObjects)
    {
      gameObject.Update();
    }
  }
}

Para adicionar uma WaitForSecondsimplementação muito simples , você só precisa de algo como isto:

interface IDelayedCoroutine
{
  bool ShouldMove();
}

class Waiter: IDelayedCoroutine
{
  private readonly TimeSpan time;
  private readonly DateTime start;

  public Waiter(TimeSpan time)
  {
    this.start = DateTime.Now;
    this.time = time;
  }

  public bool ShouldMove() => start + time > DateTime.Now;
}

E o código correspondente em nosso loop principal:

foreach (var continuation in continuations.ToArray())
{
  if (continuation.Current is IDelayedCoroutine dc)
  {
    if (!dc.ShouldMove()) continue;
  }

  if (!continuation.MoveNext()) continuations.Remove(continuation);
}

Ta-da - é tudo o que um sistema simples de rotina precisa. E, cedendo controle ao chamador, ele pode decidir sobre qualquer número de coisas; eles podem ter uma tabela de eventos classificados em vez de iterar por todas as corotinas em todos os quadros; eles podem ter prioridades ou dependências. Permite a implementação muito simples de multitarefa cooperativa. E veja como isso é simples, graças a yield:)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.