paint-brush
Unity 2023.1 présente la classe Awaitablepar@deniskondratev
7,024 lectures
7,024 lectures

Unity 2023.1 présente la classe Awaitable

par Denis Kondratev10m2023/01/27
Read on Terminal Reader

Trop long; Pour lire

L'article traite de la nouvelle classe Awaitable introduite dans Unity 2023.1, qui offre davantage de possibilités d'écriture de code asynchrone dans le développement de jeux Unity. Il couvre les méthodes d'attente, les propriétés pour arrêter l'exécution asynchrone et l'utilisation avec d'autres outils Unity tels que Coroutines et InvokeRepeating. Il souligne l'importance de comprendre les bases de l'attente asynchrone et d'expérimenter la classe Awaitable pour comprendre ses capacités et ses limites.
featured image - Unity 2023.1 présente la classe Awaitable
Denis Kondratev HackerNoon profile picture
0-item

En mai 2022, Alexandre Mutel et Kristyna Hougaard annonçaient dans leur billet "Unity et .NET, et après ?" que Unity prévoit d'adopter davantage de fonctionnalités de .NET, y compris la commodité d'utiliser async-wait. Et, il semble que Unity tient sa promesse. Dans la version alpha de Unity 2023.1, la classe Awaitable a été introduite, offrant davantage de possibilités d'écriture de code asynchrone.

Méthodes d'attente

Dans cette section, je n'approfondirai pas trop les méthodes qui, à mon avis, sont suffisamment décrites dans la documentation officielle de Unity. Ils sont tous liés à l'attente asynchrone.


Awaitable.WaitForSecondsAsync() vous permet d'attendre un temps de jeu spécifié. Contrairement à Task.Delay(), qui effectue une attente en temps réel. Pour aider à clarifier la différence, je fournirai un petit exemple plus tard dans un bloc de code.


 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."); }


Dans cet exemple, au début de la méthode Start(), le temps de jeu est arrêté à l'aide de Time.timeScale. Pour les besoins de l'expérience, une coroutine sera utilisée pour reprendre son flux après 5 secondes dans la méthode RunGameplay(). Ensuite, nous lançons deux méthodes d'attente d'une seconde. L'un utilisant Awaitable.WaitForSecondsAsync() et l'autre utilisant Task.Delay(). Au bout d'une seconde, nous recevrons un message dans la console "Waiting WaitWithTaskDelay() terminé". Et après 5 secondes, le message "Waiting WaitWithTaskDelay() terminé" apparaîtra.


D'autres méthodes pratiques ont également été ajoutées pour vous donner plus de flexibilité dans la boucle de lecteur de base de Unity. Leur but est clair d'après le nom et correspond à leur analogie lors de l'utilisation de Coroutines :


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


Si vous débutez avec les Coroutines, je vous recommande de les expérimenter par vous-même pour mieux comprendre.


Une méthode, Awaitable.FromAsyncOperation(), a également été ajoutée pour assurer la rétrocompatibilité avec l'ancienne API, AsyncOperation.

Utilisation de la propriété destroyCancellationToken

L'un des avantages de l'utilisation de Coroutines est qu'elles s'arrêtent automatiquement si le composant est supprimé ou désactivé. Dans Unity 2022.2, la propriété destroyCancellationToken a été ajoutée à MonoBehaviour, vous permettant d'arrêter l'exécution asynchrone au moment de la suppression de l'objet. Il est important de se rappeler que l'arrêt de la tâche via l'annulation de CancellationToken lève l'OperationCanceledException. Si la méthode appelante ne renvoie pas Task ou Awaitable, cette exception doit être interceptée.


 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); }


Dans cet exemple, l'objet est immédiatement détruit dans Start(), mais avant cela, Awake() parvient à lancer l'exécution de DoAwaitAsync(). La commande Awaitable.WaitForSecondsAsync(1, destroyCancellationToken) attend 1 seconde, puis doit afficher le message "Ce message ne sera pas enregistré". Étant donné que l'objet est immédiatement supprimé, le destroyCancellationToken arrête l'exécution de toute la chaîne en levant l'OperationCanceledException. De cette manière, destroyCancellationToken nous évite d'avoir à créer un CancellationToken manuellement.


Mais nous pouvons toujours le faire, par exemple, pour arrêter l'exécution au moment de la désactivation de l'objet. Je vais donner un exemple.


 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."); } } }


Dans ce formulaire, le message "Ce message est enregistré toutes les secondes" sera envoyé tant que l'objet sur lequel se bloque ce MonoBehaviour est activé. L'objet peut être éteint et rallumé.


Ce code peut sembler redondant. Unity contient déjà de nombreux outils pratiques tels que Coroutines et InvokeRepeating() qui vous permettent d'effectuer des tâches similaires beaucoup plus facilement. Mais ce n'est qu'un exemple d'utilisation. Ici, nous n'avons affaire qu'à Awaitable.


Utilisation de la propriété Application.exitCancellationToken

Dans Unity, l'exécution de la méthode asynchrone ne s'arrête pas d'elle-même même après avoir quitté le mode de lecture dans l'éditeur. Ajoutons un script similaire au projet.


 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); } } }


Dans cet exemple, après être passé en mode lecture, le message "Ce message est enregistré toutes les secondes" sera envoyé à la console. Il continue d'être émis même après que le bouton Play est relâché. Dans cet exemple, Task.Delay() est utilisé à la place de Awaitable.WaitForSecondsAsync(), car ici, pour afficher l'action, un délai est nécessaire non pas en temps de jeu mais en temps réel.


De manière analogue à destroyCancellationToken, nous pouvons utiliser Application.exitCancellationToken, qui interrompt l'exécution des méthodes asynchrones à la sortie du mode de lecture. Corrigeons le script.


 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); } } }


Maintenant, le script s'exécutera comme prévu.

Utilisation avec les fonctions d'événement

Dans Unity, certaines fonctions d'événement peuvent être des coroutines, par exemple, Start, OnCollisionEnter ou OnCollisionExit. Mais à partir de Unity 2023.1, tous peuvent être Awaitable, y compris Update(), LateUpdate et même OnDestroy().


Ils doivent être utilisés avec prudence, car il n'y a pas d'attente pour leur exécution asynchrone. Par exemple, pour le code suivant :


 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()"); }


Dans la console, nous obtiendrons le résultat suivant :


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


Il convient également de rappeler que le MonoBehaviour lui-même ou même l'objet de jeu peut cesser d'exister pendant que le code asynchrone est toujours en cours d'exécution. Dans une telle situation:


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


Dans la trame suivante, le MonoBehaviour est considéré comme supprimé. Dans la console, nous obtiendrons le résultat suivant :


 True Flase


Cela s'applique également à la méthode OnDestroy(). Si vous rendez la méthode asynchrone, vous devez tenir compte du fait qu'après l'instruction await, le MonoBehaviour est déjà considéré comme supprimé. Lorsque l'objet lui-même est supprimé, le travail de nombreux MonoBehaviours qui s'y trouvent peut ne pas fonctionner correctement à ce stade.


Il convient de noter que lorsque vous travaillez avec des fonctions d'événement, il est important de connaître l'ordre d'exécution. Le code asynchrone peut ne pas s'exécuter dans l'ordre que vous attendez, et il est essentiel de garder cela à l'esprit lors de la conception de vos scripts.

Les fonctions d'événement en attente interceptent tous les types d'exceptions

Il convient de noter que les fonctions d'événement en attente interceptent tous les types d'exceptions, qui peuvent être inattendues. Je m'attendais à ce qu'ils n'attrapent que OperationCanceledExceptions, ce qui aurait été plus logique. Mais la capture de tous les types d'exceptions les rend inutilisables pour le moment. Au lieu de cela, vous pouvez exécuter des méthodes asynchrones et intercepter manuellement les messages nécessaires, comme indiqué dans l'exemple précédent.


 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); }


Comme le composant est supprimé immédiatement au démarrage, l'exécution de DoAwaitAsync() sera interrompue. Le message "Ce message ne sera pas enregistré" n'apparaîtra pas dans la console. Seule OperationCanceledException() est interceptée, toutes les autres exceptions peuvent être levées.


J'espère que cette approche sera corrigée à l'avenir. Pour le moment, l'utilisation des fonctions d'événement en attente n'est pas sûre.

Mouvement libre à travers les threads

Comme on le sait, toutes les opérations avec des objets de jeu et des MonoBehaviours ne sont autorisées que dans le thread principal. Parfois, il est nécessaire de faire des calculs massifs qui peuvent conduire à un blocage du jeu. Il est préférable de les exécuter en dehors du thread principal. Awaitable propose deux méthodes, BackgroundThreadAsync() et MainThreadAsync(), qui permettent de s'éloigner du thread principal et d'y revenir. Je vais donner un exemple.


 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}"); }


Ici, lorsque la méthode démarre, elle passe à un thread supplémentaire. Ici, j'affiche l'identifiant de ce thread supplémentaire sur la console. Il ne sera pas égal à 1, car 1 est le thread principal.


Ensuite, le thread est gelé pendant 10 secondes (Thread.Sleep(10000)), simulant des calculs massifs. Si vous faites cela dans le thread principal, le jeu semblera se figer pendant la durée de son exécution. Mais dans cette situation, tout continue de fonctionner de manière stable. Vous pouvez également utiliser un CancellationToken dans ces calculs pour arrêter une opération inutile.


Après cela, nous revenons au fil principal. Et maintenant, toutes les fonctions Unity sont à nouveau disponibles. Par exemple, comme dans ce cas, désactiver un objet de jeu, ce qui n'était pas possible sans le thread principal.

Conclusion

En conclusion, la nouvelle classe Awaitable introduite dans Unity 2023.1 offre aux développeurs davantage de possibilités d'écrire du code asynchrone, ce qui facilite la création de jeux réactifs et performants. La classe Awaitable comprend une variété de méthodes d'attente, telles que WaitForSecondsAsync(), EndOfFrameAsync(), FixedUpdateAsync() et NextFrameAsync(), qui permettent une plus grande flexibilité dans la boucle de lecteur de base de Unity. Les propriétés destroyCancellationToken et Application.exitCancellationToken fournissent également un moyen pratique d'arrêter l'exécution asynchrone au moment de la suppression d'un objet ou de la sortie du mode de lecture.


Il est important de noter que si la classe Awaitable fournit une nouvelle façon d'écrire du code asynchrone dans Unity, elle doit être utilisée conjointement avec d'autres outils Unity tels que Coroutines et InvokeRepeating pour obtenir les meilleurs résultats. De plus, il est important de comprendre les bases de l'attente asynchrone et les avantages qu'elle peut apporter au processus de développement de jeux, comme l'amélioration des performances et de la réactivité.


En résumé, la classe Awaitable est un outil puissant pour les développeurs Unity, mais elle doit être utilisée avec précaution et en conjonction avec d'autres outils et concepts Unity pour obtenir les meilleurs résultats. Il est important de l'expérimenter pour mieux comprendre ses capacités et ses limites.