¿Cómo no congelar el hilo principal en Unity?

33

Tengo un algoritmo de generación de nivel que es computacionalmente pesado. Como tal, llamarlo siempre provoca la congelación de la pantalla del juego. ¿Cómo puedo colocar la función en un segundo subproceso mientras el juego sigue mostrando una pantalla de carga para indicar que el juego no está congelado?

DarkDestry
fuente
1
Puede usar el sistema de subprocesos para ejecutar otros trabajos en segundo plano ... pero es posible que no invoquen nada en la API de Unity ya que esas cosas no son seguras para subprocesos. Solo comento porque no he hecho esto y no puedo darle con seguridad el código de ejemplo rápidamente.
Almo
Uso Listde acciones para almacenar las funciones que desea llamar en el hilo principal. bloquee y copie la Actionlista en la Updatefunción a la lista temporal, borre la lista original y luego ejecute el Actioncódigo en Listel hilo principal. Vea UnityThread de mi otra publicación sobre cómo hacer esto. Por ejemplo, para llamar a una función en el subproceso principal, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Programador

Respuestas:

48

Actualización: en 2018, Unity está implementando un sistema de trabajo C # como una forma de descargar el trabajo y utilizar múltiples núcleos de CPU.

La respuesta a continuación es anterior a este sistema. Seguirá funcionando, pero puede haber mejores opciones disponibles en Unity moderno, según sus necesidades. En particular, el sistema de trabajo parece resolver algunas de las limitaciones sobre qué hilos creados manualmente pueden acceder de forma segura, como se describe a continuación. Los desarrolladores que experimentan con el informe de vista previa realizan emisiones de rayos y construyen mallas en paralelo , por ejemplo.

Invitaría a los usuarios con experiencia en el uso de este sistema de trabajo a agregar sus propias respuestas que reflejen el estado actual del motor.


He usado subprocesos para tareas pesadas en Unity en el pasado (generalmente procesamiento de imágenes y geometría), y no es drásticamente diferente que usar subprocesos en otras aplicaciones de C #, con dos advertencias:

  1. Debido a que Unity usa un subconjunto algo más antiguo de .NET, hay algunas características y bibliotecas de subprocesos más nuevas que no podemos usar de fábrica, pero los conceptos básicos están ahí.

  2. Como Almo señala en un comentario anterior, muchos tipos de Unity no son seguros para subprocesos y arrojarán excepciones si intenta construirlos, usarlos o incluso compararlos desde el hilo principal. Cosas a tener en cuenta:

    • Un caso común es verificar si una referencia de GameObject o Monobehaviour es nula antes de intentar acceder a sus miembros. myUnityObject == nullllama a un operador sobrecargado para cualquier cosa que descienda de UnityEngine.Object, pero System.Object.ReferenceEquals()trabaja en torno a esto hasta cierto punto; solo recuerde que un GameObject Destroy () ed se compara como igual a nulo usando la sobrecarga, pero aún no es ReferenceEqual a nulo.

    • La lectura de los parámetros de los tipos de Unity generalmente es segura en otro subproceso (en el sentido de que no arrojará una excepción de inmediato siempre y cuando tenga cuidado de verificar los nulos como se indicó anteriormente), pero tenga en cuenta la advertencia de Philipp aquí que el subproceso principal podría estar modificando el estado mientras lo estás leyendo Tendrá que ser disciplinado sobre quién puede modificar qué y cuándo para evitar leer algún estado inconsistente, lo que puede conducir a errores que pueden ser muy difíciles de rastrear porque dependen de tiempos de sub-milisegundos entre hilos que podemos No reproducir a voluntad.

    • Los miembros estáticos aleatorios y de tiempo no están disponibles. Cree una instancia de System.Random por hilo si necesita aleatoriedad, y System.Diagnostics.Stopwatch si necesita información de sincronización.

    • Las estructuras Mathf, Vector, Matrix, Quaternion y Color funcionan bien en todos los subprocesos, por lo que puede hacer la mayoría de sus cálculos por separado

    • Crear GameObjects, adjuntar monobehaviours o crear / actualizar texturas, mallas, materiales, etc., todo debe suceder en el hilo principal. En el pasado, cuando necesitaba trabajar con estos, establecí una cola de productor-consumidor, donde mi hilo de trabajo prepara los datos en bruto (como una gran variedad de vectores / colores para aplicar a una malla o textura), y una actualización o rutina en el hilo principal sondea los datos y los aplica.

Con esas notas fuera del camino, aquí hay un patrón que uso a menudo para el trabajo enhebrado. No garantizo que sea un estilo de mejores prácticas, pero hace el trabajo. (Los comentarios o ediciones para mejorar son bienvenidos; sé que el enhebrado es un tema muy profundo del cual solo conozco los conceptos básicos)

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

Si no necesitas dividir estrictamente el trabajo entre hilos para obtener velocidad, y solo estás buscando una manera de que no se bloquee para que el resto del juego siga funcionando, una solución más ligera en Unity es Coroutines . Estas son funciones que pueden hacer algo de trabajo y luego devolver el control al motor para continuar lo que está haciendo y reanudar sin problemas en un momento posterior.

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

Esto no necesita ninguna consideración especial de limpieza, ya que el motor (por lo que puedo decir) elimina las rutinas de los objetos destruidos para usted.

Todo el estado local del método se conserva cuando cede y se reanuda, por lo que para muchos propósitos es como si se estuviera ejecutando sin interrupciones en otro hilo (pero tiene todas las comodidades de ejecutarse en el hilo principal). Solo necesita asegurarse de que cada iteración sea lo suficientemente corta como para que no ralentice su hilo principal sin razón.

Al garantizar que las operaciones importantes no estén separadas por un rendimiento, puede obtener la consistencia del comportamiento de un solo subproceso, sabiendo que ningún otro script o sistema en el hilo principal puede modificar los datos en los que está trabajando.

La línea de retorno de rendimiento le ofrece algunas opciones. Usted puede...

  • yield return null reanudar después de la siguiente actualización del marco ()
  • yield return new WaitForFixedUpdate() reanudar después de la próxima FixedUpdate ()
  • yield return new WaitForSeconds(delay) reanudar después de que transcurra una cierta cantidad de tiempo de juego
  • yield return new WaitForEndOfFrame() reanudar después de que la GUI termine de renderizar
  • yield return myRequestdonde myRequestes una instancia WWW , para reanudar una vez que los datos solicitados terminen de cargarse desde la web o el disco.
  • yield return otherCoroutinedonde otherCoroutinees una instancia de Coroutine , para reanudar una vez otherCoroutinecompletada. Esto a menudo se usa en la forma yield return StartCoroutine(OtherCoroutineMethod())de encadenar la ejecución a una nueva rutina que puede producir cuando lo desee.

    • Experimentalmente, omitir el segundo StartCoroutiney simplemente escribir yield return OtherCoroutineMethod()logra el mismo objetivo si desea encadenar la ejecución en el mismo contexto.

      Ajustar dentro de un StartCoroutinepuede ser útil si desea ejecutar la rutina anidada en asociación con un segundo objeto, comoyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... dependiendo de cuándo quieras que la corutina tome su próximo turno.

O yield break;para detener la corutina antes de que llegue al final, de la forma en que podría utilizarla return;antes de un método convencional.

DMGregory
fuente
¿Hay alguna forma de usar corutinas para producir solo después de que la iteración demore más de 20 ms?
DarkDestry
@DarkDestry Puede usar una instancia de cronómetro para medir el tiempo que pasa en su bucle interno y salir a un bucle externo donde ceda antes de reiniciar el cronómetro y reanudar el bucle interno.
DMGregory
1
¡Buena respuesta! Solo quiero agregar que una razón más para usar corutinas es que si alguna vez necesitas construir para webgl, funcionará, lo que no funcionará si usas hilos. Sentado con ese dolor de cabeza en un gran proyecto en este momento: P
Mikael Högström
Buena respuesta y gracias. Solo me pregunto qué pasa si necesito detener y reiniciar el hilo varias veces, intento abortar y comenzar, me da un error
flankechen
@flankechen El inicio y desmontaje de subprocesos son operaciones relativamente costosas, por lo que a menudo preferimos mantener el subproceso disponible pero inactivo, utilizando cosas como semáforos o monitores para indicar cuándo tenemos trabajo nuevo para hacerlo. ¿Desea publicar una nueva pregunta que describa su caso de uso en detalle, y la gente puede sugerir formas eficientes de lograrlo?
DMGregory
0

Puede poner su cálculo pesado en otro hilo, pero la API de Unity no es segura para hilos, debe ejecutarlos en el hilo principal.

Bueno, puedes probar este paquete en Asset Store que te ayudará a usar el enhebrado más fácilmente. http://u3d.as/wQg Simplemente puede usar una sola línea de código para iniciar un hilo y ejecutar Unity API de forma segura.

Qi Haoyan
fuente
0

@DMGregory lo explicó muy bien.

Se pueden usar tanto hilos como corutinas. Primero para descargar el hilo principal y más tarde para devolver el control al hilo principal. Empujar el trabajo pesado al hilo separado parece más razonable. Por lo tanto, las colas de trabajo pueden ser lo que busca.

Hay un buen script de ejemplo de JobQueue en Unity Wiki.

IndieForger
fuente