Certaines tâches de développement de jeux ne sont pas synchrones — elles sont asynchrones. Cela signifie qu'ils ne sont pas exécutés de manière linéaire dans le code du jeu. Certaines de ces tâches asynchrones peuvent nécessiter un temps assez long, tandis que d'autres sont associées à des calculs intensifs.
Effectuer des requêtes réseau
Chargement de scènes, de ressources et d'autres ressources
Lire et écrire des fichiers
L'intelligence artificielle pour la prise de décision
Longues séquences d'animation
Traiter de grandes quantités de données
Trouver son chemin
Maintenant, surtout, puisque tout le code Unity s'exécute dans un thread, toute tâche comme l'une de celles mentionnées ci-dessus, si elles étaient effectuées de manière synchrone, conduirait au blocage du thread principal et, par conséquent, à la perte de trames.
Bonjour à tous, je m'appelle Dmitrii Ivashchenko et je suis le chef de l'équipe de développement de MY.GAMES. Dans cet article, nous allons parler d'éviter de tels problèmes. Nous recommanderons des techniques de programmation asynchrone pour effectuer ces tâches dans un thread séparé, laissant ainsi le thread principal libre d'effectuer d'autres tâches. Cela contribuera à garantir un gameplay fluide et réactif et (espérons-le) des joueurs satisfaits.
Tout d'abord, parlons des coroutines. Ils ont été introduits dans Unity en 2011, avant même que async/attend n'apparaisse dans .NET. Dans Unity, les coroutines nous permettent d'exécuter un ensemble d'instructions sur plusieurs images, au lieu de les exécuter toutes en même temps. Ils sont similaires aux threads, mais sont légers et intégrés dans la boucle de mise à jour de Unity, ce qui les rend bien adaptés au développement de jeux.
(Au fait, historiquement parlant, les coroutines ont été le premier moyen d'effectuer des opérations asynchrones dans Unity, donc la plupart des articles sur Internet en parlent.)
Pour créer une coroutine, vous devez déclarer une fonction avec le type de retour IEnumerator
. Cette fonction peut contenir n'importe quelle logique que vous souhaitez que la coroutine exécute.
Pour démarrer une coroutine, vous devez appeler la méthode StartCoroutine
sur une instance MonoBehaviour
et passer la fonction coroutine en argument :
public class Example : MonoBehaviour { void Start() { StartCoroutine(MyCoroutine()); } IEnumerator MyCoroutine() { Debug.Log("Starting coroutine"); yield return null; Debug.Log("Executing coroutine"); yield return null; Debug.Log("Finishing coroutine"); } }
Il existe plusieurs instructions de rendement disponibles dans Unity, telles que WaitForSeconds
, WaitForEndOfFrame
, WaitForFixedUpdate
, WaitForSecondsRealtime
, WaitUntil
ainsi que quelques autres. Il est important de se rappeler que leur utilisation entraîne des allocations, elles doivent donc être réutilisées dans la mesure du possible.
Par exemple, considérez cette méthode de la documentation :
IEnumerator Fade() { Color c = renderer.material.color; for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return new WaitForSeconds(.1f); } }
A chaque itération de la boucle, une nouvelle instance de new WaitForSeconds(.1f)
sera créée. Au lieu de cela, nous pouvons déplacer la création en dehors de la boucle et éviter les allocations :
IEnumerator Fade() { Color c = renderer.material.color; **var waitForSeconds = new WaitForSeconds(0.2f);** for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return **waitForSeconds**; } }
Une autre propriété importante à noter est que yield return
peut être utilisé avec toutes les méthodes Async
fournies par Unity car les AsyncOperation
sont des descendants de YieldInstruction
:
yield return SceneManager.LoadSceneAsync("path/to/scene.unity");
Ceci étant dit, les coroutines ont également quelques inconvénients à noter :
MonoBehaviour
qui la lance. Si le GameObject
est désactivé ou détruit, la coroutine cesse d'être traitée.try-catch-finally
ne peut pas être utilisée en raison de la présence de la syntaxe yield.yield return
avant que le code suivant ne commence à s'exécuter.Les promesses sont un modèle pour organiser et rendre les opérations asynchrones plus lisibles. Ils sont devenus populaires en raison de leur utilisation dans de nombreuses bibliothèques JavaScript tierces et, depuis ES6, ont été implémentés de manière native.
Lors de l'utilisation de Promises, nous renvoyons immédiatement un objet de votre fonction asynchrone. Cela permet à l'appelant d'attendre la résolution (ou une erreur) de l'opération.
Essentiellement, cela fait en sorte que les méthodes asynchrones peuvent renvoyer des valeurs et "agir" comme des méthodes synchrones : au lieu de renvoyer immédiatement la valeur finale, elles donnent une "promesse" qu'elles renverront une valeur dans le futur.
Il existe plusieurs implémentations de Promises pour Unity :
La principale façon d'interagir avec une promesse est via les fonctions de rappel .
Vous pouvez définir une fonction de rappel qui sera appelée lorsqu'une promesse est résolue, et une autre fonction de rappel qui sera appelée si la promesse est rejetée. Ces rappels reçoivent le résultat de l'opération asynchrone sous forme d'arguments, qui peuvent ensuite être utilisés pour effectuer d'autres opérations.
Selon ces spécifications de l' organisation Promises/A+, une promesse peut être dans l'un des trois états suivants :
Pending
: l'état initial, cela signifie que l'opération asynchrone est toujours en cours, et que le résultat de l'opération n'est pas encore connu.Fulfilled
( Resolved
) : l'état résolu est accompagné d'une valeur qui représente le résultat de l'opération.Rejected
: si l'opération asynchrone échoue pour une raison quelconque, la Promise est dite "rejected". L'état rejeté est accompagné de la raison de l'échec.De plus, les promesses peuvent être enchaînées, de sorte que le résultat d'une promesse puisse être utilisé pour déterminer le résultat d'une autre promesse.
Par exemple, vous pouvez créer une promesse qui récupère certaines données d'un serveur, puis utiliser ces données pour créer une autre promesse qui effectue des calculs et d'autres actions :
var promise = MakeRequest("https://some.api") .Then(response => Parse(response)) .Then(result => OnRequestSuccess(result)) .Then(() => PlaySomeAnimation()) .Catch(exception => OnRequestFailed(exception));
Voici un exemple d'organisation d'une méthode qui effectue une opération asynchrone :
public IPromise<string> MakeRequest(string url) { // Create a new promise object var promise = new Promise<string>(); // Create a new web client using var client = new WebClient(); // Add a handler for the DownloadStringCompleted event client.DownloadStringCompleted += (sender, eventArgs) => { // If an error occurred, reject the promise if (eventArgs.Error != null) { promise.Reject(eventArgs.Error); } // Otherwise, resolve the promise with the result else { promise.Resolve(eventArgs.Result); } }; // Start the download asynchronously client.DownloadStringAsync(new Uri(url), null); // Return the promise return promise; }
Nous pourrions également envelopper des coroutines dans une Promise
:
void Start() { // Load the scene and then show the intro animation LoadScene("path/to/scene.unity") .Then(() => ShowIntroAnimation()) .Then( ... ); } // Load a scene and return a promise Promise LoadScene(string sceneName) { // Create a new promise var promise = new Promise(); // Start a coroutine to load the scene StartCoroutine(LoadSceneRoutine(promise, sceneName)); // Return the promise return promise; } IEnumerator LoadSceneRoutine(Promise promise, string sceneName) { // Load the scene asynchronously yield return SceneManager.LoadSceneAsync(sceneName); // Resolve the promise once the scene is loaded promise.Resolve(); }
Et bien sûr, vous pouvez organiser n'importe quelle combinaison d'ordre d'exécution des promesses en utilisant ThenAll
/ Promise.All
et ThenRace
/ Promise.Race
:
// Execute the following two promises in sequence Promise.Sequence( () => Promise.All( // Execute the two promises in parallel RunAnimation("Foo"), PlaySound("Bar") ), () => Promise.Race( // Execute the two promises in a race RunAnimation("One"), PlaySound("Two") ) );
Malgré toute la commodité d'utilisation, les promesses présentent également certains inconvénients:
La fonctionnalité async/wait fait partie de C# depuis la version 5.0 (2012) et a été introduite dans Unity 2017 avec l'implémentation du runtime .NET 4.x.
Dans l'histoire de .NET, on distingue les étapes suivantes :
BeginSmth
renvoie l'interface IAsyncResult
. La méthode EndSmth
prend IAsyncResult
; si l'opération n'est pas terminée au moment de l'appel EndSmth
, le thread est bloqué.Task
et Task<TResult>
.
Les approches précédentes sont devenues obsolètes en raison du succès de la dernière approche.
Pour créer une méthode asynchrone, la méthode doit être marquée avec le mot-clé async
, contenir un await
à l'intérieur et la valeur de retour doit être Task
, Task<T>
ou void
(non recommandé).
public async Task Example() { SyncMethodA(); await Task.Delay(1000); // the first async operation SyncMethodB(); await Task.Delay(2000); // the second async operation SyncMethodC(); await Task.Delay(3000); // the third async operation }
Dans cet exemple, l'exécution se déroulera comme ceci :
SyncMethodA
) sera exécuté.await Task.Delay(1000)
est lancée et devrait être exécutée. Pendant ce temps, le code à appeler lorsque l'opération asynchrone est terminée (la « suite ») sera enregistré.SyncMethodB
) commencera à s'exécuter.await Task.Delay(2000)
) est lancée et devrait être exécutée. En même temps, la continuation — le code suivant la deuxième opération asynchrone ( SyncMethodC
) sera préservée.SyncMethodC
sera exécuté, suivi de l'exécution et de l'attente de la troisième opération asynchrone await Task.Delay(3000)
.
Il s'agit d'une explication simplifiée, car en fait async/wait est du sucre syntaxique permettant d'appeler facilement des méthodes asynchrones et d'attendre leur achèvement.
Vous pouvez également organiser n'importe quelle combinaison d'ordres d'exécution en utilisant WhenAll
et WhenAny
:
var allTasks = Task.WhenAll( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); allTasks.ContinueWith(t => { Console.WriteLine("All the tasks are completed"); }); var anyTask = Task.WhenAny( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); anyTask.ContinueWith(t => { Console.WriteLine("One of tasks is completed"); });
Le compilateur C# transforme les appels async/wait en une machine d'état IAsyncStateMachine
, qui est un ensemble séquentiel d'actions qui doivent être effectuées pour terminer l'opération asynchrone.
Chaque fois que vous appelez une opération d'attente, la machine d'état termine son travail et attend la fin de cette opération, après quoi elle continue à exécuter l'opération suivante. Cela permet d'effectuer des opérations asynchrones en arrière-plan sans bloquer le thread principal, et rend également les appels de méthode asynchrones plus simples et plus lisibles.
Ainsi, la méthode Example
est transformée en création et initialisation d'une machine d'état avec l'annotation [AsyncStateMachine(typeof(ExampleStateMachine))]
, et la machine d'état elle-même a un nombre d'états égal au nombre d'appels en attente.
Exemple de la méthode transformée Example
[AsyncStateMachine(typeof(ExampleStateMachine))] public /*async*/ Task Example() { // Create a new instance of the ExampleStateMachine class ExampleStateMachine stateMachine = new ExampleStateMachine(); // Create a new AsyncTaskMethodBuilder and assign it to the taskMethodBuilder property of the stateMachine instance stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); // Set the currentState property of the stateMachine instance to -1 stateMachine.currentState = -1; // Start the stateMachine instance stateMachine.taskMethodBuilder.Start(ref stateMachine); // Return the Task property of the taskMethodBuilder return stateMachine.taskMethodBuilder.Task; }
Exemple de machine d'état générée ExampleStateMachine
[CompilerGenerated] private sealed class ExampleStateMachine : IAsyncStateMachine { public int currentState; public AsyncTaskMethodBuilder taskMethodBuilder; private TaskAwaiter taskAwaiter; public int paramInt; private int localInt; void IAsyncStateMachine.MoveNext() { int num = currentState; try { TaskAwaiter awaiter3; TaskAwaiter awaiter2; TaskAwaiter awaiter; switch (num) { default: localInt = paramInt; // Call the first synchronous method SyncMethodA(); // Create a task awaiter for a delay of 1000 milliseconds awaiter3 = Task.Delay(1000).GetAwaiter(); // If the task is not completed, set the current state to 0 and store the awaiter if (!awaiter3.IsCompleted) { currentState = 0; taskAwaiter = awaiter3; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine); return; } // If the task is completed, jump to the label after the first await goto Il_AfterFirstAwait; case 0: // Retrieve the awaiter from the taskAwaiter field awaiter3 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the first await goto Il_AfterFirstAwait; case 1: // Retrieve the awaiter from the taskAwaiter field awaiter2 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the second await goto Il_AfterSecondAwait; case 2: // Retrieve the awaiter from the taskAwaiter field awaiter = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; break; Il_AfterFirstAwait: awaiter3.GetResult(); // Call the second synchronous method SyncMethodB(); // Create a task awaiter for a delay of 2000 milliseconds awaiter2 = Task.Delay(2000).GetAwaiter(); // If the task is not completed, set the current state to 1 and store the awaiter if (!awaiter2.IsCompleted) { currentState = 1; taskAwaiter = awaiter2; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine); return; } // If the task is completed, jump to the label after the second await goto Il_AfterSecondAwait; Il_AfterSecondAwait: // Get the result of the second awaiter awaiter2.GetResult(); // Call the SyncMethodC SyncMethodC(); // Create a new awaiter with a delay of 3000 milliseconds awaiter = Task.Delay(3000).GetAwaiter(); // If the awaiter is not completed, set the current state to 2 and store the awaiter if (!awaiter.IsCompleted) { currentState = 2; taskAwaiter = awaiter; // Set the stateMachine to this ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } break; } // Get the result of the awaiter awaiter.GetResult(); } catch (Exception exception) { currentState = -2; taskMethodBuilder.SetException(exception); return; } currentState = -2; taskMethodBuilder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { /*...*/ } }
Dans l'appel AwaitUnsafeOnCompleted
, le contexte de synchronisation actuel SynchronizationContext
sera obtenu. SynchronizationContext est un concept en C# utilisé pour représenter un contexte qui contrôle l'exécution d'un ensemble d'opérations asynchrones. Il est utilisé pour coordonner l'exécution du code sur plusieurs threads et pour garantir que le code est exécuté dans un ordre spécifique. L'objectif principal de SynchronizationContext est de fournir un moyen de contrôler la planification et l'exécution d'opérations asynchrones dans un environnement multithread.
Dans différents environnements, SynchronizationContext
a différentes implémentations. Par exemple, dans .NET, il y a :
System.Windows.Threading.DispatcherSynchronizationContext
System.Windows.Forms.WindowsFormsSynchronizationContext
System.Threading.WinRTSynchronizationContext
System.Web.AspNetSynchronizationContext
Unity a également son propre contexte de synchronisation, UnitySynchronizationContext
, qui nous permet d'utiliser des opérations asynchrones avec une liaison à l'API PlayerLoop. L'exemple de code suivant montre comment faire pivoter un objet dans chaque image à l'aide de Task.Yield()
:
private async void Start() { while (true) { transform.Rotate(0, Time.deltaTime * 50, 0); await Task.Yield(); } }
Un autre exemple d'utilisation de async/wait dans Unity pour effectuer une requête réseau :
using UnityEngine; using System.Net.Http; using System.Threading.Tasks; public class NetworkRequestExample : MonoBehaviour { private async void Start() { string response = await GetDataFromAPI(); Debug.Log("Response from API: " + response); } private async Task<string> GetDataFromAPI() { using (var client = new HttpClient()) { var response = await client.GetStringAsync("https://api.example.com/data"); return response; } } }
Grâce à UnitySynchronizationContext
, nous pouvons utiliser en toute sécurité les méthodes UnityEngine
(telles que Debug.Log()
) juste après la fin d'une opération asynchrone, car l'exécution de ce code se poursuivra dans le thread principal Unity.
Cette classe permet de gérer un objet Task
. Il a été créé pour adapter les anciennes méthodes asynchrones à TAP, mais il est également très utile lorsque nous voulons envelopper une Task
autour d'une opération de longue durée qui sur un événement.
Dans l'exemple suivant, l'objet Task
à l'intérieur taskCompletionSource
se terminera 3 secondes après le début, et nous obtiendrons son résultat dans la méthode Update
:
using System.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private TaskCompletionSource<int> taskCompletionSource; private void Start() { // Create a new TaskCompletionSource taskCompletionSource = new TaskCompletionSource<int>(); // Start a coroutine to wait 3 seconds // and then set the result of the TaskCompletionSource StartCoroutine(WaitAndComplete()); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the TaskCompletionSource taskCompletionSource.SetResult(10); } private async void Update() { // Await the result of the TaskCompletionSource int result = await taskCompletionSource.Task; // Log the result to the console Debug.Log("Result: " + result); } }
Un jeton d'annulation est utilisé en C# pour signaler qu'une tâche ou une opération doit être annulée. Le jeton est transmis à la tâche ou à l'opération, et le code de la tâche ou de l'opération peut vérifier périodiquement le jeton pour déterminer si la tâche ou l'opération doit être arrêtée. Cela permet une annulation propre et gracieuse d'une tâche ou d'une opération, au lieu de simplement la tuer brusquement.
Les jetons d'annulation sont couramment utilisés dans les situations où une tâche de longue durée peut être annulée par l'utilisateur, ou si la tâche n'est plus nécessaire, comme un bouton d'annulation dans une interface utilisateur.
Le modèle global ressemble à l'utilisation de TaskCompletionSource
. Tout d'abord, un CancellationTokenSource
est créé, puis son Token
est passé à l'opération asynchrone :
public class ExampleMonoBehaviour : MonoBehaviour { private CancellationTokenSource _cancellationTokenSource; private async void Start() { // Create a new CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); // Get the token from the CancellationTokenSource CancellationToken token = _cancellationTokenSource.Token; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } private void OnDestroy() { // Cancel the token when the object is destroyed _cancellationTokenSource.Cancel(); } }
Lorsque l'opération est annulée, une OperationCanceledException
est levée et la propriété Task.IsCanceled
est définie sur true
.
Il est important de noter que les objets Task
sont gérés par le runtime .NET, pas par Unity, et si l'objet exécutant la tâche est détruit (ou si le jeu quitte le mode de jeu dans l'éditeur), la tâche continuera à s'exécuter comme Unity a aucun moyen de l'annuler.
Vous devez toujours accompagner await Task
avec le CancellationToken
correspondant. Cela conduit à une certaine redondance de code, et dans Unity 2022.2, des jetons intégrés au niveau MonoBehaviour
et l'ensemble du niveau Application
sont apparus.
Voyons comment l'exemple précédent change lors de l'utilisation du destroyCancellationToken
de l'objet MonoBehaviour
:
using System.Threading; using System.Threading.Tasks; using UnityEngine; public class ExampleMonoBehaviour : MonoBehaviour { private async void Start() { // Get the cancellation token from the MonoBehaviour CancellationToken token = this.destroyCancellationToken; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } }
Nous n'avons plus besoin de créer manuellement un CancellationTokenSource
et de terminer la tâche dans la méthode OnDestroy
. Pour les tâches non associées à un MonoBehaviour
particulier, nous pouvons utiliser UnityEngine.Application.exitCancellationToken
. Cela mettra fin à la tâche lors de la sortie du mode de lecture (dans l'éditeur) ou lors de la fermeture de l'application.
Malgré la commodité d'utilisation et les fonctionnalités fournies par les tâches .NET, elles présentent des inconvénients importants lorsqu'elles sont utilisées dans Unity :
Task
sont trop encombrants et provoquent de nombreuses allocations.Task
ne correspond pas au thread Unity (thread unique).
La bibliothèque UniTask contourne ces restrictions sans utiliser de threads ou SynchronizationContext
. Il réalise l'absence d'allocations en utilisant le type basé sur la structure UniTask<T>
.
UniTask nécessite la version d'exécution de script .NET 4.x, Unity 2018.4.13f1 étant la version officielle la plus faible prise en charge.
Vous pouvez également convertir toutes les AsyncOperations
en UnitTask
avec des méthodes d'extension :
using UnityEngine; using UniTask; public class AssetLoader : MonoBehaviour { public async void LoadAsset(string assetName) { var loadRequest = Resources.LoadAsync<GameObject>(assetName); await loadRequest.AsUniTask(); var asset = loadRequest.asset as GameObject; if (asset != null) { // Do something with the loaded asset } } }
Dans cet exemple, la méthode LoadAsset
utilise Resources.LoadAsync
pour charger une ressource de manière asynchrone. La méthode AsUniTask
est ensuite utilisée pour convertir l' AsyncOperation
renvoyée par LoadAsync
en une UniTask
, qui peut être attendue.
Comme auparavant, vous pouvez organiser n'importe quelle combinaison d'ordre d'exécution en utilisant UniTask.WhenAll
et UniTask.WhenAny
:
using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private async void Start() { // Start two Tasks and wait for both to complete await UniTask.WhenAll(Task1(), Task2()); // Start two Tasks and wait for one to complete await UniTask.WhenAny(Task1(), Task2()); } private async UniTask Task1() { // Do something } private async UniTask Task2() { // Do something } }
Dans UniTask, il existe une autre implémentation de SynchronizationContext
appelée UniTaskSynchronizationContext
qui peut être utilisée pour remplacer UnitySynchronizationContext
pour de meilleures performances.
Dans la première version alpha de Unity 2023.1, la classe Awaitable
a été introduite. Les coroutines en attente sont des types de type tâche compatibles async/wait conçus pour s'exécuter dans Unity. Contrairement aux tâches .NET, elles sont gérées par le moteur, et non par le runtime.
private async Awaitable DoSomethingAsync() { // awaiting built-in events await Awaitable.EndOfFrameAsync(); await Awaitable.WaitForSecondsAsync(); // awaiting .NET Tasks await Task.Delay(2000, destroyCancellationToken); await Task.Yield(); // awaiting AsyncOperations await SceneManager.LoadSceneAsync("path/to/scene.unity"); // ... }
Ils peuvent être attendus et utilisés comme type de retour d'une méthode asynchrone. Par rapport à System.Threading.Tasks
, ils sont moins sophistiqués mais utilisent des raccourcis améliorant les performances basés sur des hypothèses spécifiques à Unity.
Voici les principales différences par rapport aux tâches .NET :
Awaitable
ne peut être attendu qu'une seule fois ; il ne peut pas être attendu par plusieurs fonctions asynchrones.Awaiter.GetResults()
ne bloquera pas jusqu'à la fin. L'appeler avant la fin de l'opération est un comportement indéfini.ExecutionContext
. Pour des raisons de sécurité, les tâches .NET capturent les contextes d'exécution en attente afin de propager les contextes d'emprunt d'identité sur les appels asynchrones.SynchronizationContext
. Les continuations de coroutine sont exécutées de manière synchrone à partir du code qui déclenche la complétion. Dans la plupart des cas, cela proviendra du cadre principal de Unity.ObjectPool
a été amélioré pour éviter les vérifications des limites Stack<T>
dans les séquences get/release typiques générées par les machines à états asynchrones.
Pour obtenir le résultat d'une opération longue, vous pouvez utiliser le type Awaitable<T>
. Vous pouvez gérer l'achèvement d'un Awaitable
en utilisant AwaitableCompletionSource
et AwaitableCompletionSource<T>
, similaire à TaskCompletitionSource
:
using UnityEngine; using Cysharp.Threading.Tasks; public class ExampleBehaviour : MonoBehaviour { private AwaitableCompletionSource<bool> _completionSource; private async void Start() { // Create a new AwaitableCompletionSource _completionSource = new AwaitableCompletionSource<bool>(); // Start a coroutine to wait 3 seconds // and then set the result of the AwaitableCompletionSource StartCoroutine(WaitAndComplete()); // Await the result of the AwaitableCompletionSource bool result = await _completionSource.Awaitable; // Log the result to the console Debug.Log("Result: " + result); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the AwaitableCompletionSource _completionSource.SetResult(true); } }
Parfois, il est nécessaire d'effectuer des calculs massifs qui peuvent conduire à des blocages de jeu. Pour cela, il est préférable d'utiliser les méthodes Awaitable : BackgroundThreadAsync()
et MainThreadAsync()
. Ils vous permettent de quitter le thread principal et d'y revenir.
private async Awaitable DoCalculationsAsync() { // Awaiting execution on a ThreadPool background thread. await Awaitable.BackgroundThreadAsync(); var result = PerformSomeHeavyCalculations(); // Awaiting execution on the Unity main thread. await Awaitable.MainThreadAsync(); // Using the result in main thread Debug.Log(result); }
De cette façon, Awaitables élimine les inconvénients de l'utilisation des tâches .NET et permet également d'attendre les événements PlayerLoop et AsyncOperations.
Comme on peut le voir, avec le développement de Unity, il existe de plus en plus d'outils pour organiser les opérations asynchrones :
Unité | Coroutines | Promesses | Tâches .NET | UniTâche | Jetons d'annulation intégrés | API en attente |
---|---|---|---|---|---|---|
5.6 | ✅ | ✅ | | | | |
2017.1 | ✅ | ✅ | ✅ | | | |
2018.4 | ✅ | ✅ | ✅ | ✅ | | |
2022.2 | ✅ | ✅ | ✅ | ✅ | ✅ | |
2023.1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Nous avons examiné tous les principaux moyens de programmation asynchrone dans Unity. En fonction de la complexité de votre tâche et de la version d'Unity que vous utilisez, vous pouvez utiliser un large éventail de technologies allant des coroutines et promesses aux tâches et attentes, pour assurer un gameplay fluide et transparent dans vos jeux. Merci d'avoir lu, et nous attendons vos prochains chefs-d'œuvre.