paint-brush
Unity 2023.1 presenta Awaitable Classpor@deniskondratev
7,136 lecturas
7,136 lecturas

Unity 2023.1 presenta Awaitable Class

por Denis Kondratev10m2023/01/27
Read on Terminal Reader

Demasiado Largo; Para Leer

El artículo analiza la nueva clase Awaitable introducida en Unity 2023.1, que permite más oportunidades para escribir código asíncrono en el desarrollo de juegos de Unity. Cubre los métodos de espera, las propiedades para detener la ejecución asincrónica y el uso con otras herramientas de Unity, como Coroutines e InvokeRepeating. Enfatiza la importancia de comprender los conceptos básicos de async-await y experimentar con la clase Awaitable para comprender sus capacidades y limitaciones.
featured image - Unity 2023.1 presenta Awaitable Class
Denis Kondratev HackerNoon profile picture
0-item

En mayo de 2022, Alexandre Mutel y Kristyna Hougaard anunciaron en su publicación "Unity y .NET, ¿qué sigue?" que Unity planea adoptar más características de .NET, incluida la conveniencia de usar async-await. Y parece que Unity está cumpliendo su promesa. En la versión alfa de Unity 2023.1, se introdujo la clase Awaitable, que brinda más oportunidades para escribir código asíncrono.

Métodos de espera

En esta sección, no profundizaré demasiado en métodos que, en mi opinión, tienen suficiente descripción en la documentación oficial de Unity. Todos ellos están relacionados con la espera asincrónica.


Awaitable.WaitForSecondsAsync() le permite esperar una cantidad específica de tiempo de juego. A diferencia de Task.Delay(), que realiza una espera en tiempo real. Para ayudar a aclarar la diferencia, proporcionaré un pequeño ejemplo más adelante en un bloque de código.


 private void Start() { Time.timeScale = 0; StartCoroutine(RunGameplay()); Task.WhenAll( WaitWithWaitForSecondsAsync(), WaitWithTaskDelay()); } private IEnumerator RunGameplay() { yield return new WaitForSecondsRealtime(5); Time.timeScale = 1; } private async Task WaitWithWaitForSecondsAsync() { await Awaitable.WaitForSecondsAsync(1); Debug.Log("Waiting WithWaitForSecondsAsync() ended."); } private async Task WaitWithTaskDelay() { await Task.Delay(1); Debug.Log("Waiting WaitWithTaskDelay() ended."); }


En este ejemplo, al comienzo del método Start(), el tiempo del juego se detiene usando Time.timeScale. Por el bien del experimento, se utilizará Coroutine para reanudar su flujo después de 5 segundos en el método RunGameplay(). Luego, lanzamos dos métodos de espera de un segundo. Uno que usa Awaitable.WaitForSecondsAsync() y el otro que usa Task.Delay(). Después de un segundo, recibiremos un mensaje en la consola "Waiting WaitWithTaskDelay () finalizó". Y después de 5 segundos, aparecerá el mensaje "Waiting WaitWithTaskDelay () finalizado".


También se han agregado otros métodos convenientes para brindarle más flexibilidad en el Player Loop básico de Unity. Su propósito es claro por el nombre y corresponde a su analogía al usar Coroutines:


  • EndOfFrameAsync()
  • FixedUpdateAsync()
  • NextFrameAsync()


Si es nuevo en el trabajo con Coroutines, le recomiendo experimentar con ellos por su cuenta para obtener una mejor comprensión.


También se agregó un método, Awaitable.FromAsyncOperation(), para compatibilidad con versiones anteriores de la API anterior, AsyncOperation.

Usando la propiedad destroyCancellationToken

Una de las ventajas de utilizar Coroutines es que se detienen automáticamente si se elimina o desactiva el componente. En Unity 2022.2, la propiedad destroyCancellationToken se agregó a MonoBehaviour, lo que le permite detener la ejecución asíncrona en el momento de la eliminación del objeto. Es importante recordar que detener la tarea mediante la cancelación de CancellationToken arroja OperationCanceledException. Si el método de llamada no devuelve Task o Awaitable, se debe capturar esta excepción.


 private async void Awake() { try { await DoAwaitAsync(); } catch (OperationCanceledException) { } } private async Awaitable DoAwaitAsync() { await Awaitable.WaitForSecondsAsync(1, destroyCancellationToken); Debug.Log("That message won't be logged."); } private void Start() { Destroy(this); }


En este ejemplo, el objeto se destruye inmediatamente en Start(), pero antes de eso, Awake() logra iniciar la ejecución de DoAwaitAsync(). El comando Awaitable.WaitForSecondsAsync(1, destroyCancellationToken) espera 1 segundo y luego debe generar el mensaje "Ese mensaje no se registrará". Debido a que el objeto se elimina de inmediato, destroyCancellationToken detiene la ejecución de toda la cadena al generar OperationCanceledException. De esta forma, destroyCancellationToken nos libera de la necesidad de crear un CancellationToken manualmente.


Pero aún podemos hacer esto, por ejemplo, para detener la ejecución en el momento de la desactivación del objeto. Daré un ejemplo.


 using System; using System.Threading; using UnityEngine; public class Example : MonoBehaviour { private CancellationTokenSource _tokenSource; private async void OnEnable() { _tokenSource = new CancellationTokenSource(); try { await DoAwaitAsync(_tokenSource.Token); } catch (OperationCanceledException) { } } private void OnDisable() { _tokenSource.Cancel(); _tokenSource.Dispose(); } private static async Awaitable DoAwaitAsync(CancellationToken token) { while (!token.IsCancellationRequested) { await Awaitable.WaitForSecondsAsync(1, token); Debug.Log("This message is logged every second."); } } }


De esta forma, se enviará el mensaje "Este mensaje se registra cada segundo" siempre que el objeto en el que cuelga este MonoBehaviour esté encendido. El objeto se puede apagar y encender de nuevo.


Este código puede parecer redundante. Unity ya contiene muchas herramientas convenientes como Coroutines e InvokeRepeating() que te permiten realizar tareas similares mucho más fácilmente. Pero esto es solo un ejemplo de uso. Aquí solo estamos tratando con Awaitable.


Uso de la propiedad Application.exitCancellationToken

En Unity, la ejecución del método asíncrono no se detiene por sí sola incluso después de salir del modo de reproducción en el editor. Agreguemos un script similar al proyecto.


 using System.Threading.Tasks; using UnityEngine; public static class Boot { [RuntimeInitializeOnLoadMethod] public static async Awaitable LogAsync() { while (true) { Debug.Log("This message is logged every second."); await Task.Delay(1000); } } }


En este ejemplo, después de cambiar al modo de reproducción, el mensaje "Este mensaje se registra cada segundo" aparecerá en la consola. Continúa emitiéndose incluso después de soltar el botón Reproducir. En este ejemplo, se usa Task.Delay() en lugar de Awaitable.WaitForSecondsAsync(), porque aquí, para mostrar la acción, se necesita un retraso no en el tiempo del juego sino en tiempo real.


Análogamente a destroyCancellationToken, podemos usar Application.exitCancellationToken, que interrumpe la ejecución de métodos asincrónicos al salir del modo de reproducción. Arreglemos el guión.


 using System.Threading.Tasks; using UnityEngine; public static class Boot { [RuntimeInitializeOnLoadMethod] public static async Awaitable LogAsync() { var cancellationToken = Application.exitCancellationToken; while (!cancellationToken.IsCancellationRequested) { Debug.Log("This message is logged every second."); await Task.Delay(1000, cancellationToken); } } }


Ahora el script se ejecutará según lo previsto.

Uso con funciones de eventos

En Unity, algunas funciones de eventos pueden ser corrutinas, por ejemplo, Start, OnCollisionEnter o OnCollisionExit. Pero a partir de Unity 2023.1, todos pueden estar disponibles, incluidos Update(), LateUpdate e incluso OnDestroy().


Deben usarse con precaución, ya que no hay que esperar a su ejecución asincrónica. Por ejemplo, para el siguiente código:


 private async Awaitable Awake() { Debug.Log("Awake() started"); await Awaitable.NextFrameAsync(); Debug.Log("Awake() finished"); } private void OnEnable() { Debug.Log("OnEnable()"); } private void Start() { Debug.Log("Start()"); }


En la consola obtendremos el siguiente resultado:


 Awake() started OnEnable() Start() Awake() finished


También vale la pena recordar que el propio MonoBehaviour o incluso el objeto del juego pueden dejar de existir mientras el código asincrónico aún se está ejecutando. En tal situación:


 private async Awaitable Awake() { Debug.Log(this != null); await Awaitable.NextFrameAsync(); Debug.Log(this != null); } private void Start() { Destroy(this); }


En el siguiente cuadro, el MonoBehaviour se considera eliminado. En la consola obtendremos el siguiente resultado:


 True Flase


Esto también se aplica al método OnDestroy(). Si hace que el método sea asíncrono, debe tener en cuenta que después de la declaración de espera, el MonoBehaviour ya se considera eliminado. Cuando se elimina el objeto en sí, es posible que el trabajo de muchos MonoBehaviours ubicados en él no funcione correctamente en este punto.


Vale la pena señalar que cuando se trabaja con funciones de eventos, es importante tener en cuenta el orden de ejecución. Es posible que el código asíncrono no se ejecute en el orden esperado, y es esencial tener esto en cuenta al diseñar sus scripts.

Las funciones de eventos en espera capturan todos los tipos de excepciones

Vale la pena señalar que las funciones de eventos en espera detectan todo tipo de excepciones, que pueden ser inesperadas. Esperaba que capturaran solo OperationCanceledExceptions, lo que habría tenido más sentido. Pero la captura de todos los tipos de excepciones hace que no sean adecuadas para su uso en este momento. En su lugar, puede ejecutar métodos asincrónicos y capturar manualmente los mensajes necesarios, como se muestra en el ejemplo anterior.


 private async void Awake() { try { await DoAwaitAsync(); } catch (OperationCanceledException) { } } private async Awaitable DoAwaitAsync() { await Awaitable.WaitForSecondsAsync(1, destroyCancellationToken); Debug.Log("That message won't be logged"); } private void Start() { Destroy(this); }


Debido a que el componente se elimina inmediatamente al inicio, la ejecución de DoAwaitAsync() se interrumpirá. El mensaje "Ese mensaje no se registrará" no aparecerá en la consola. Solo se detecta OperationCanceledException(), todas las demás excepciones se pueden lanzar.


Espero que este enfoque se corrija en el futuro. Por el momento, el uso de Awaitable Event Functions no es seguro.

Libre movimiento a través de hilos

Como se sabe, todas las operaciones con objetos del juego y MonoBehaviours solo están permitidas en el hilo principal. A veces es necesario hacer cálculos masivos que pueden llevar a congelar el juego. Es mejor realizarlos fuera del hilo principal. Awaitable ofrece dos métodos, BackgroundThreadAsync() y MainThreadAsync(), que permiten alejarse del hilo principal y volver a él. Daré un ejemplo.


 private async Awaitable DoAwaitAsync(CancellationToken token) { await Awaitable.BackgroundThreadAsync(); Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(10000); await Awaitable.MainThreadAsync(); if (token.IsCancellationRequested) { return; } Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); gameObject.SetActive(false); await Awaitable.BackgroundThreadAsync(); Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); }


Aquí, cuando se inicia el método, cambiará a un subproceso adicional. Aquí envío la identificación de este hilo adicional a la consola. No será igual a 1, porque 1 es el hilo principal.


Luego, el hilo se congela durante 10 segundos (Thread.Sleep(10000)), simulando cálculos masivos. Si haces esto en el hilo principal, el juego parecerá congelarse mientras dure su ejecución. Pero en esta situación, todo sigue funcionando de manera estable. También puede usar un token de cancelación en estos cálculos para detener una operación innecesaria.


Después de eso, volvemos al hilo principal. Y ahora todas las funciones de Unity están disponibles nuevamente para nosotros. Por ejemplo, como en este caso, deshabilitar un objeto del juego, que no era posible prescindir del hilo principal.

Conclusión

En conclusión, la nueva clase Awaitable presentada en Unity 2023.1 brinda a los desarrolladores más oportunidades para escribir código asíncrono, lo que facilita la creación de juegos con capacidad de respuesta y rendimiento. La clase Awaitable incluye una variedad de métodos de espera, como WaitForSecondsAsync(), EndOfFrameAsync(), FixedUpdateAsync() y NextFrameAsync(), que permiten una mayor flexibilidad en el Player Loop básico de Unity. Las propiedades destroyCancellationToken y Application.exitCancellationToken también brindan una manera conveniente de detener la ejecución asíncrona en el momento de eliminar el objeto o salir del modo de reproducción.


Es importante tener en cuenta que, si bien la clase Awaitable proporciona una nueva forma de escribir código asíncrono en Unity, debe usarse junto con otras herramientas de Unity, como Coroutines e InvokeRepeating, para lograr los mejores resultados. Además, es importante comprender los conceptos básicos de async-await y los beneficios que puede aportar al proceso de desarrollo de juegos, como mejorar el rendimiento y la capacidad de respuesta.


En resumen, la clase Awaitable es una herramienta poderosa para los desarrolladores de Unity, pero debe usarse con cuidado y en conjunto con otras herramientas y conceptos de Unity para lograr los mejores resultados. Es importante experimentar con él para comprender mejor sus capacidades y limitaciones.