jueves, 4 de enero de 2018

Unity3D Plugin: Thread Ninja - Multithread Coroutine

Voy a inaugurar una nueva sección en la que comentaré los plugins que mas me han gustado del Asset Store de Unity3D, Así como una breve explicación de como usarlos y, si son gratuitos o no.


En primer lugar, enseñaré uno de los indespensables, al menos para mi, cuando hay acciones muy pesadas dentro de nuestro juego o aplicación creada a través de Unity3D.

Este plugin, llamado Thread Ninja - Multithread Coroutine, como su propio nombre indica, es para que, durante la ejecución, podamos usar corutinas, ya explicadas anteirormente en esta web, en hilos externos a Unity.



¿Para qué sirve?
Las corutinas que ejecutemos en un hilo externo a Unity3D no bajará el rendimiento del hilo principal de Unity3D, siempre y cuando no nos excedamos tanto como para bajar el rendimiento de otros hilos, en cuyo caso, deberíamos plantearnos que algo estamos haciendo mal.

¿Para qué no sirve?
Para llamar a la API de Unity3D, así que solo podremos mejorar el rendimiento de cualquier algoritmo o carga pesada de la que hagamos uso solo de las librerías de C# nativas y algunas excepciones que veremos ahora.

¿Para que sistemas operativos sirven?
Yo personalmente, lo he probado tanto en PC, Mac como Android e iOS y en todos ha funcionado sin problemas y con resultados realmente buenos.

¿Unity3D no iba a sacar su propio sistema llamado Jobs?
Si, pronto deberían sacar su propio sistema, pero de momento no han puesto fecha y lo siguen alargando, aún cuando salga, mi recomendación es esperar al menos un par de parches al menos antes de usar el sistema que lanzarán, para evitar usar algo inestable o aun siendo estable, seguro que saldrán bugs inicialmente.

¿Como se usa?
En primer lugar, si no sabemos usar corutinas, no pasa nada, dejo aquí el enlace a un tutorial previo de como usar corutinas:

http://www.hagamosvideojuegos.com/2014/02/unity-3d-scripting-corutinas.html

Y ahora, una vez tengamos este tutorial aprendido, podemos pasar al plugin del que llevamos hablando tanto rato.

Primero, debemos descargarlo de aquí: https://assetstore.unity.com/packages/tools/thread-ninja-multithread-coroutine-15717

Una vez agregado este plugin a nuestro proyecto, explicaré el uso del ejemplo ya que me parece lo suficientemente completo:

Procedemos a abrir la escena de ejemplo llamada ExampleScene y podremos encontrar en la jerarquía de objetos un GameObject llamado Cube, el cual, contiene el ExampleScript de ejemplo que trae este plugin.

Al principio del ejemplo podemos ver que tenemos las librerías usuales que usa Unity y dos nuevas:

using System;
using UnityEngine;
using System.Collections;
using CielaSpike;
using System.Threading;
 CielaSpike es la librería que acabamos de descargar, tenemos que importarla para darle uso, en cambio System.Threading es una librería interna de C# con la cual podemos acceder a las clases y métodos para usar threading que podremos ver mas adelante en este ejemplo.

Cabe recordar que, como poder, podríamos usar hilos directamente de C#, que ya explicaré en otro tutorial, pero este plugin en particular, me parece interesante por tenerlo muy bien integrado con Unity3D.

Donde empezamos el ejemplo desde una corutina:

void Start()
{
    StartCoroutine(StartExamples());
}

Seguido de un Update en el cual, mientras el hilo principal no se quede colgado, hará rotar el cubo para que veamos que sigue adelante, al ser el hilo principal, hay que recordar que si se queda colgado el hilo principal, nuestro IDE se va a quedar también colgado hasta que el hilo principal vuelva a estar funcional.

void Update()
{
    // rotate cube to see if main thread has been blocked;
    transform.Rotate(Vector3.up, Time.deltaTime * 180);
}
Ahora vamos a ir por partes explicando cada parte del código entrando ya de lleno en la funcionalidad:

IEnumerator StartExamples()
{
    Task task;
    LogExample("Blocking Thread");
    this.StartCoroutineAsync(Blocking(), out task);
    yield return StartCoroutine(task.Wait());
    LogState(task);

    LogExample("Cancellation");
    this.StartCoroutineAsync(Cancellation(), out task);
    yield return new WaitForSeconds(2.0f);
    task.Cancel();
    LogState(task);

    LogExample("Error Handling");
    yield return this.StartCoroutineAsync(ErrorHandling(), out task);
    LogState(task);
}
Las dos primeras líneas podemos omitirlas, en la primera creamos un objeto de la clase Task, que ahora veremos su funcionalmiento, en la segunda, si miramos mas adelante, veremos que solo pondrá un log en la consola de Unity3D.

Bien, en primer lugar, veremos como nos crea una corutina asíncrona, es decir, un hilo, para la corutina Blocking, y el out task es para que, ese hilo que nos cree con la corutina Blocking, la tengamos en task, de manera que, como veremos mas adelante, podremos interactuar desde fuera con ciertos métodos.

IEnumerator Blocking()
{
    LogAsync("Thread.Sleep(5000); -> See if cube rotates.");
    Thread.Sleep(5000);
    LogAsync("Jump to main thread.");
    yield return Ninja.JumpToUnity;
    LogSync("Thread.Sleep(5000); -> See if cube rotates.");
    yield return new WaitForSeconds(0.1f);
    Thread.Sleep(5000);
    LogSync("Jump to background.");
    yield return Ninja.JumpBack;
    LogAsync("Yield WaitForSeconds on background.");
    yield return new WaitForSeconds(3.0f);
}

Bien, vamos por partes, en primer lugar tenemos una funcionalidad nativa de C# para cualquier hilo, que sería equivalente en una corutina a WaitForSeconds pero el número sería en milisegundos, y lo mas importante,  parará el hilo donde se esté ejecutando totalmente durante ese tiempo, como podremos observar, lo para durante cincomil milisegundos, o lo que es lo mismo, cinco segundos, pero deberíamos de ver el cubo rotar esos cinco segundos aun habiendo bloqueado el hilo.

¿Por qué? Porque es un hilo externo.

IEnumerator Blocking()
{
    LogAsync("Thread.Sleep(5000); -> See if cube rotates.");
    Thread.Sleep(5000);
    LogAsync("Jump to main thread.");
    yield return Ninja.JumpToUnity;
    LogSync("Thread.Sleep(5000); -> See if cube rotates.");
    yield return new WaitForSeconds(0.1f);
    Thread.Sleep(5000);
    LogSync("Jump to background.");
    yield return Ninja.JumpBack;
    LogAsync("Yield WaitForSeconds on background.");
    yield return new WaitForSeconds(3.0f);
}
Vamos a quedarnos con esta línea:

yield return Ninja.JumpToUnity;
A partir de esta línea, volvemos a estar en el hilo principal de Unity, todo lo que se haga de aquí en adelante, se ejecutará en Unity3D, esto nos vendrá bien si por ejemplo para proseguir, debemos usar la API de Unity3D, como por ejemplo Resources.Load() o cualquier otra funcionalidad que esté dentro de UnityEngine, la clase Debug es una excepción, pero no todos los accesos estáticos son válidos, los de la clase Application por ejemplo, no pueden usarse en un hilo externo tampoco, así que tendremos que usar esto para poder hacerlo dentro de Unity, te invito a hacer las pruebas y comprobarlo e incluso ver si hay alguna otra funcionalidad permitida en otro hilo.

IEnumerator Blocking()
{
    LogAsync("Thread.Sleep(5000); -> See if cube rotates.");
    Thread.Sleep(5000);
    LogAsync("Jump to main thread.");
    yield return Ninja.JumpToUnity;
    LogSync("Thread.Sleep(5000); -> See if cube rotates.");
    yield return new WaitForSeconds(0.1f);
    Thread.Sleep(5000);
    LogSync("Jump to background.");
    yield return Ninja.JumpBack;
    LogAsync("Yield WaitForSeconds on background.");
    yield return new WaitForSeconds(3.0f);
}
Aquí podremos ver que, repitiendo el mismo proceso de antes, bloqueará el hilo, esta vez el hilo principal, así que Unity3D se quedará colgado y no podremos hacer nada durante esos cinco segundos.

¿Por qué? porque como explicamos antes, volvimos al hilo principal, el de Unity3D, si pausamos ese hilo, pausamos todo.

IEnumerator Blocking()
{
    LogAsync("Thread.Sleep(5000); -> See if cube rotates.");
    Thread.Sleep(5000);
    LogAsync("Jump to main thread.");
    yield return Ninja.JumpToUnity;
    LogSync("Thread.Sleep(5000); -> See if cube rotates.");
    yield return new WaitForSeconds(0.1f);
    Thread.Sleep(5000);
    LogSync("Jump to background.");
    yield return Ninja.JumpBack;
    LogAsync("Yield WaitForSeconds on background.");
    yield return new WaitForSeconds(3.0f);
}
Como expliqué antes, este sería el proceso inverso:

yield return Ninja.JumpBack;
Con esto, volveremos al hilo secundario para poder volver a hacer otros procesos fuera del hilo principal.

IEnumerator Blocking()
{
    LogAsync("Thread.Sleep(5000); -> See if cube rotates.");
    Thread.Sleep(5000);
    LogAsync("Jump to main thread.");
    yield return Ninja.JumpToUnity;
    LogSync("Thread.Sleep(5000); -> See if cube rotates.");
    yield return new WaitForSeconds(0.1f);
    Thread.Sleep(5000);
    LogSync("Jump to background.");
    yield return Ninja.JumpBack;
    LogAsync("Yield WaitForSeconds on background.");
    yield return new WaitForSeconds(3.0f);
}
por último podemos ver que también podemos usar cualquier CustomYieldInstruction o las que trae por defecto Unity3D como WaitForSeconds.

Antes de continuar, hacer incapié en el uso de esta línea, que hace que espere a que el hilo termine de ejecutarse correctamente:

yield return StartCoroutine(task.Wait());
Dicho esto, pasemos a la segunda parte del ejemplo:

IEnumerator StartExamples()
{
    Task task;
    LogExample("Blocking Thread");
    this.StartCoroutineAsync(Blocking(), out task);
    yield return StartCoroutine(task.Wait());
    LogState(task);

    LogExample("Cancellation");
    this.StartCoroutineAsync(Cancellation(), out task);
    yield return new WaitForSeconds(2.0f);
    task.Cancel();
    LogState(task);

    LogExample("Error Handling");
    yield return this.StartCoroutineAsync(ErrorHandling(), out task);
    LogState(task);
}
Si nos fijamos, esta vez, dos segundos después de empezar la rutina, cancelará la rutina como está marcado en:

task.Cancel();
Este método cancelará el hilo esté como esté, veamos que hace ese hilo:

IEnumerator Cancellation()
{
    LogAsync("Running heavy task...");
    for (int i = 0; i < int.MaxValue; i++)
    {
        // do some heavy ops;
        // ...
    }

    yield break;
}
Aquí tenemos un ejemplo de una operación pesada y que tardaría mucho en ejecutarse, un contador desde 0 al máximo valor de un entero, que puedo adelantarte que es 2147483647 un número de iteraciones en ese for bastante grande, pero en caso de que nuestro ordenador no sea lo suficientemente potente como para procesar todas esas operaciones en menos de dos segundos, como bien vimos justo antes, si no terminó cuando usamos esta cancelación, parará de golpe, podría ser útil por ejemplo si queremos cancelar una descarga de algún archivo remoto.

por ultimo veamos el tercer ejemplo:

IEnumerator StartExamples()
{
    Task task;
    LogExample("Blocking Thread");
    this.StartCoroutineAsync(Blocking(), out task);
    yield return StartCoroutine(task.Wait());
    LogState(task);

    LogExample("Cancellation");
    this.StartCoroutineAsync(Cancellation(), out task);
    yield return new WaitForSeconds(2.0f);
    task.Cancel();
    LogState(task);

    LogExample("Error Handling");
    yield return this.StartCoroutineAsync(ErrorHandling(), out task);
    LogState(task);
}
En este ejemplo parece que nos van a enseñar un ejemplo de error, veamos que contiene y quedémonos con la parte importante:

IEnumerator ErrorHandling()
{
    LogAsync("Running heavy task...");
    for (int i = 0; i < int.MaxValue; i++)
    {
        if (i > int.MaxValue / 2)
            throw new Exception("Some error from background thread...");
    }

    yield break;
}
Aquí, en la parte señalada podemos ver que proboca un error, no es un error real si no uno controlado por el usuario, aunque para el ejemplo es mas que suficiente, un error real al final es exáctamente lo mismo, un throw, del que mas adelante podría hacer un tutorial también para explicarlos en profundidad.

Al llegar a esta parte, podremos ver que pasa si tenemos un error en el hilo externo, podremos comprobar que en Unity nos dará el error como cualquier otro error, del cual podremos ver en que línea se provocó el error.

Y hasta aquí los ejemplos de uso mas importantes que tiene este fantástico plugin, si quieres que explique como usar cualquier otro plugin, siempre que pueda disponer de el, haré lo posible por explicar su uso.

No hay comentarios:

Publicar un comentario