Como não congelar o thread principal no Unity?


33

Eu tenho um algoritmo de geração de nível que é computacionalmente pesado. Como tal, chamá-lo sempre resulta no congelamento da tela do jogo. Como colocar a função em um segundo segmento enquanto o jogo ainda continua renderizando uma tela de carregamento para indicar que o jogo não está congelado?


1
Você pode usar o sistema de encadeamento para executar outros trabalhos em segundo plano ... mas eles podem não chamar nada na API do Unity, pois esse material não é seguro para threads. Apenas comentando porque eu não fiz isso e não posso dar um exemplo de código com confiança rapidamente.
Almo

Uso Listde Ações para armazenar as funções que você deseja chamar no thread principal. bloqueie e copie a Actionlista na Updatefunção para a lista temporária, limpe a lista original e execute o Actioncódigo na Listlista principal. Veja UnityThread do meu outro post sobre como fazer isso. Por exemplo, para chamar uma função no Thread principal, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Programador

Respostas:


48

Atualização: em 2018, o Unity está lançando um C # Job System como uma maneira de descarregar o trabalho e fazer uso de vários núcleos da CPU.

A resposta abaixo é anterior a este sistema. Ainda funcionará, mas pode haver melhores opções disponíveis no Unity moderno, dependendo de suas necessidades. Em particular, o sistema de tarefas parece resolver algumas das limitações sobre o que os threads criados manualmente podem acessar com segurança, descritos abaixo. Desenvolvedores que experimentam o relatório de visualização realizando raycasts e construindo malhas em paralelo , por exemplo.

Convido usuários com experiência no uso desse sistema de tarefas a adicionar suas próprias respostas, refletindo o estado atual do mecanismo.


Eu usei threading para tarefas pesadas no Unity no passado (geralmente processamento de imagem e geometria), e não é drasticamente diferente do que usar threads em outros aplicativos C #, com duas ressalvas:

  1. Como o Unity usa um subconjunto um pouco mais antigo do .NET, existem alguns recursos e bibliotecas de encadeamento mais recentes que não podemos usar imediatamente, mas o básico está lá.

  2. Como Almo observa em um comentário acima, muitos tipos de Unity não são seguros para threads e lançarão exceções se você tentar construí-las, usá-las ou mesmo compará-las na thread principal. Coisas para manter em mente:

    • Um caso comum é verificar se uma referência GameObject ou Monobehaviour é nula antes de tentar acessar seus membros. myUnityObject == nullchama um operador sobrecarregado para qualquer coisa descendente de UnityEngine.Object, mas System.Object.ReferenceEquals()trabalha em torno disso até certo ponto - lembre-se de que um GameObject de Destroy () ed se compara como igual a nulo usando a sobrecarga, mas ainda não é ReferenceEqual como nulo.

    • A leitura de parâmetros dos tipos Unity geralmente é segura em outro segmento (pois não gera uma exceção imediatamente, desde que você tenha cuidado em verificar nulos como acima), mas observe o aviso de Philipp aqui de que o segmento principal pode estar modificando o estado enquanto você está lendo. Você precisará ser disciplinado sobre quem tem permissão para modificar o quê e quando, a fim de evitar a leitura de um estado inconsistente, o que pode levar a erros que podem ser incrivelmente difíceis de rastrear, porque dependem de tempos de menos de um milissegundo entre os threads que podemos reproduza à vontade.

    • Membros estáticos Random e Time não estão disponíveis. Crie uma instância de System.Random por thread, se você precisar de aleatoriedade, e System.Diagnostics.Stopwatch, se precisar de informações de tempo.

    • As funções Mathf, Vector, Matrix, Quaternion e Color structs funcionam bem em threads, para que você possa executar a maioria dos seus cálculos separadamente

    • Criar GameObjects, anexar Monobehaviours ou criar / atualizar Texturas, Malhas, Materiais, etc., tudo precisa acontecer no thread principal. No passado, quando eu precisava trabalhar com isso, configurei uma fila produtor-consumidor, na qual meu thread de trabalho prepara os dados brutos (como uma grande variedade de vetores / cores para aplicar a uma malha ou textura), e uma Atualização ou Coroutine no thread principal pesquisa dados e os aplica.

Com essas anotações fora do caminho, eis um padrão que frequentemente uso para trabalhos encadeados. Não garanto que seja um estilo de práticas recomendadas, mas ele faz o trabalho. (Comentários ou edições para melhorar são bem-vindos - eu sei que o encadeamento é um tópico muito profundo, do qual eu só sei o básico)

using UnityEngine;
using System.Threading; 

public class MyThreadedBehaviour : MonoBehaviour
{

    bool _threadRunning;
    Thread _thread;

    void Start()
    {
        // Begin our heavy work on a new thread.
        _thread = new Thread(ThreadedWork);
        _thread.Start();
    }


    void ThreadedWork()
    {
        _threadRunning = true;
        bool workDone = false;

        // This pattern lets us interrupt the work at a safe point if neeeded.
        while(_threadRunning && !workDone)
        {
            // Do Work...
        }
        _threadRunning = false;
    }

    void OnDisable()
    {
        // If the thread is still running, we should shut it down,
        // otherwise it can prevent the game from exiting correctly.
        if(_threadRunning)
        {
            // This forces the while loop in the ThreadedWork function to abort.
            _threadRunning = false;

            // This waits until the thread exits,
            // ensuring any cleanup we do after this is safe. 
            _thread.Join();
        }

        // Thread is guaranteed no longer running. Do other cleanup tasks.
    }
}

Se você não precisa estritamente dividir o trabalho entre threads para obter velocidade, e está apenas procurando uma maneira de torná-lo sem bloqueio, para que o resto do seu jogo continue correndo, uma solução mais leve no Unity é a Coroutines . Essas são funções que podem fazer algum trabalho e, em seguida, devolvem o controle ao mecanismo para continuar o que está fazendo e retomar sem interrupções posteriormente.

using UnityEngine;
using System.Collections;

public class MyYieldingBehaviour : MonoBehaviour
{ 

    void Start()
    {
        // Begin our heavy work in a coroutine.
        StartCoroutine(YieldingWork());
    }    

    IEnumerator YieldingWork()
    {
        bool workDone = false;

        while(!workDone)
        {
            // Let the engine run for a frame.
            yield return null;

            // Do Work...
        }
    }
}

Isso não precisa de considerações especiais de limpeza, pois o mecanismo (até onde eu sei) se livra das corotinas dos objetos destruídos para você.

Todo o estado local do método é preservado quando ele produz e continua, portanto, para muitos propósitos, é como se estivesse sendo executado ininterruptamente em outro encadeamento (mas você tem todas as conveniências de executar no encadeamento principal). Você só precisa garantir que cada iteração seja curta o suficiente para que não diminua seu encadeamento principal de maneira irracional.

Ao garantir que operações importantes não sejam separadas por um rendimento, você pode obter a consistência do comportamento de thread único - sabendo que nenhum outro script ou sistema no thread principal pode modificar os dados nos quais você está trabalhando.

A linha de retorno de rendimento oferece algumas opções. Você pode...

  • yield return null para retomar após a atualização do próximo quadro ()
  • yield return new WaitForFixedUpdate() para retomar após o próximo FixedUpdate ()
  • yield return new WaitForSeconds(delay) para retomar após um certo tempo decorrido
  • yield return new WaitForEndOfFrame() para retomar após a GUI terminar a renderização
  • yield return myRequestonde myRequesté uma instância da WWW , para continuar assim que os dados solicitados terminarem o carregamento da Web ou do disco.
  • yield return otherCoroutineonde otherCoroutineé uma instância da Coroutine , para retomar após a otherCoroutineconclusão. Isso geralmente é usado no formato yield return StartCoroutine(OtherCoroutineMethod())para encadear a execução a uma nova corotina que pode render quando ela deseja.

    • Experimentalmente, pular o segundo StartCoroutinee simplesmente escrever yield return OtherCoroutineMethod()realiza o mesmo objetivo se você deseja encadear a execução no mesmo contexto.

      O agrupamento dentro de a StartCoroutineainda pode ser útil se você desejar executar a corotina aninhada em associação com um segundo objeto, comoyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... dependendo de quando você deseja que a corotina faça a próxima curva.

Ou yield break;para interromper a corotina antes que ela chegue ao fim, da maneira que você pode usar return;para sair mais cedo de um método convencional.


Existe uma maneira de usar corotinas para produzir somente após a iteração levar mais de 20 ms?
DarkDestry

@DarkDestry Você pode usar uma instância de cronômetro para cronometrar quanto tempo está gastando no loop interno e interromper um loop externo no qual você produz antes de redefinir o cronômetro e retomar o loop interno.
DMGregory

1
Boa resposta! Eu só quero acrescentar que mais um motivo para usar corotinas é que, se você precisar construir para o webgl, isso funcionará, o que não acontecerá se você usar threads. Sentado com essa dor de cabeça em um grande projeto agora: P
Mikael Högström 05/05

Boa resposta e obrigado. Apenas me pergunto e se eu precisar parar e reiniciar o thread várias vezes, tento abortar e iniciar, obtém um erro
flankechen

@flankechen A inicialização e desmontagem de encadeamentos são operações relativamente caras, portanto, muitas vezes preferimos manter o encadeamento disponível, mas inativo - usando itens como semáforos ou monitores para sinalizar quando temos um novo trabalho para fazer. Deseja postar uma nova pergunta descrevendo seu caso de uso em detalhes, e as pessoas podem sugerir maneiras eficientes de alcançá-lo?
DMGregory

0

Você pode colocar seu cálculo pesado em outro thread, mas a API do Unity não é segura para threads, você deve executá-los no thread principal.

Bem, você pode experimentar este pacote na Asset Store para ajudá-lo a usar a segmentação mais facilmente. http://u3d.as/wQg Você pode simplesmente usar apenas uma linha de código para iniciar um thread e executar a API do Unity com segurança.



0

@DMGregory explicou muito bem.

É possível usar rosqueamento e corotinas. Anterior para descarregar o encadeamento principal e posteriormente para retornar o controle para o encadeamento principal. Pressionar o trabalho pesado para separar a linha parece mais razoável. Portanto, as filas de tarefas podem ser o que você procura.

Há realmente um bom exemplo de script do JobQueue no Unity Wiki.

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.