paint-brush
O Primer completo de programação assíncrona para desenvolvimento Unitypor@dmitrii
5,860 leituras
5,860 leituras

O Primer completo de programação assíncrona para desenvolvimento Unity

por Dmitrii Ivashchenko31m2023/03/30
Read on Terminal Reader

Muito longo; Para ler

Neste artigo, falaremos sobre como evitar esses problemas. Recomendamos técnicas de programação assíncrona para executar essas tarefas em um thread separado, deixando assim o thread principal livre para executar outras tarefas. Isso ajudará a garantir uma jogabilidade suave e responsiva e (esperamos) jogadores satisfeitos.
featured image - O Primer completo de programação assíncrona para desenvolvimento Unity
Dmitrii Ivashchenko HackerNoon profile picture
0-item

Algumas tarefas no desenvolvimento de jogos não são síncronas — elas são assíncronas. Isso significa que eles não são executados linearmente dentro do código do jogo. Algumas dessas tarefas assíncronas podem exigir muito tempo para serem concluídas, enquanto outras estão associadas a cálculos intensivos.


Algumas das tarefas assíncronas de jogos mais comuns são as seguintes:

  • Executando solicitações de rede

  • Carregando cenas, recursos e outros ativos

  • Lendo e gravando arquivos

  • Inteligência artificial para tomada de decisão

  • Sequências longas de animação

  • Processamento de grandes quantidades de dados

  • Encontrando o caminho


Agora, crucialmente, como todo o código do Unity é executado em um thread, qualquer tarefa como uma das mencionadas acima, se executadas de forma síncrona, levaria ao bloqueio do thread principal e, portanto, à queda de quadros.


Olá a todos, meu nome é Dmitrii Ivashchenko e sou o chefe da equipe de desenvolvimento da MY.GAMES. Neste artigo, falaremos sobre como evitar esses problemas. Recomendamos técnicas de programação assíncrona para executar essas tarefas em um thread separado, deixando assim o thread principal livre para executar outras tarefas. Isso ajudará a garantir uma jogabilidade suave e responsiva e (esperamos) jogadores satisfeitos.

Corrotinas

Primeiro, vamos falar sobre corrotinas. Eles foram introduzidos no Unity em 2011, mesmo antes de async / await aparecer no .NET. No Unity, as corrotinas nos permitem executar um conjunto de instruções em vários quadros, em vez de executá-las todas de uma vez. Eles são semelhantes aos threads, mas são leves e integrados ao loop de atualização do Unity, tornando-os adequados para o desenvolvimento de jogos.


(A propósito, historicamente falando, as corrotinas foram a primeira maneira de executar operações assíncronas no Unity, então a maioria dos artigos na Internet são sobre elas.)


Para criar uma co-rotina, você precisa declarar uma função com o tipo de retorno IEnumerator . Essa função pode conter qualquer lógica que você deseja que a co-rotina execute.


Para iniciar uma co-rotina, você precisa chamar o método StartCoroutine em uma instância MonoBehaviour e passar a função co-rotina como um argumento:

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


Existem várias instruções de rendimento disponíveis no Unity, como WaitForSeconds , WaitForEndOfFrame , WaitForFixedUpdate , WaitForSecondsRealtime , WaitUntil e algumas outras. É importante lembrar que seu uso leva a alocações, portanto, devem ser reutilizados sempre que possível.


Por exemplo, considere este método da documentação:

 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 cada iteração do loop, uma nova instância de new WaitForSeconds(.1f) será criada. Em vez disso, podemos mover a criação para fora do loop e evitar alocações:

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


Outra propriedade importante a ser observada é que yield return pode ser usado com todos os métodos Async fornecidos pelo Unity porque AsyncOperation s são descendentes de YieldInstruction :

 yield return SceneManager.LoadSceneAsync("path/to/scene.unity");

Algumas possíveis armadilhas das corrotinas

Dito isso, as corrotinas também têm algumas desvantagens a serem observadas:


  • É impossível retornar o resultado de uma longa operação. Você ainda precisa de retornos de chamada que serão passados para a co-rotina e chamados quando ela terminar de extrair quaisquer dados dela.
  • Uma co-rotina está estritamente ligada ao MonoBehaviour que a inicia. Se o GameObject for desligado ou destruído, a co-rotina para de ser processada.
  • A estrutura try-catch-finally não pode ser usada devido à presença da sintaxe yield.
  • Pelo menos um quadro passará após o yield return antes que o próximo código comece a ser executado.
  • Alocação do lambda e da própria corrotina

Promessas

As promessas são um padrão para organizar e tornar as operações assíncronas mais legíveis. Eles se tornaram populares devido ao seu uso em muitas bibliotecas JavaScript de terceiros e, desde o ES6, foram implementados nativamente.


Ao usar Promises, retornamos imediatamente um objeto de sua função assíncrona. Isso permite que o chamador aguarde a resolução (ou um erro) da operação.


Essencialmente, isso faz com que os métodos assíncronos possam retornar valores e “agir” como métodos síncronos: em vez de retornar o valor final imediatamente, eles fazem uma “promessa” de que retornarão um valor em algum momento no futuro.


Existem várias implementações de Promises para Unity:


A principal maneira de interagir com uma promessa é por meio de funções de retorno de chamada .


Você pode definir uma função de retorno de chamada que será chamada quando uma promessa for resolvida e outra função de retorno de chamada que será chamada se a promessa for rejeitada. Esses retornos de chamada recebem o resultado da operação assíncrona como argumentos, que podem ser usados para executar outras operações.


De acordo com essas especificações da organização Promises/A+, uma Promise pode estar em um dos três estados:


  • Pending : o estado inicial, isso significa que a operação assíncrona ainda está em andamento e o resultado da operação ainda não é conhecido.
  • Fulfilled ( Resolved ): o estado resolvido é acompanhado por um valor que representa o resultado da operação.
  • Rejected : se a operação assíncrona falhar por qualquer motivo, a promessa é considerada "rejeitada". O estado rejeitado é acompanhado pelo motivo da falha.

Mais sobre promessas

Além disso, as promessas podem ser encadeadas, de modo que o resultado de uma Promise possa ser usado para determinar o resultado de outra Promise.


Por exemplo, você pode criar um Promise que busca alguns dados de um servidor e, em seguida, usar esses dados para criar outro Promise que executa alguns cálculos e outras ações:

 var promise = MakeRequest("https://some.api") .Then(response => Parse(response)) .Then(result => OnRequestSuccess(result)) .Then(() => PlaySomeAnimation()) .Catch(exception => OnRequestFailed(exception));


Veja um exemplo de como organizar um método que executa uma operação assíncrona:

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


Também poderíamos envolver corrotinas em uma 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(); }


E, claro, você pode organizar qualquer combinação de ordem de execução de promessa usando ThenAll / Promise.All e 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") ) );

As partes “pouco promissoras” das promessas

Apesar de toda a comodidade de uso, as promessas também apresentam alguns inconvenientes:


  • Sobrecarga : a criação de promessas envolve sobrecarga adicional em comparação com o uso de outros métodos de programação assíncrona, como corrotinas. Em alguns casos, isso pode levar à diminuição do desempenho.
  • Depuração : Depurar promessas pode ser mais difícil do que depurar outros padrões de programação assíncrona. Pode ser difícil rastrear o fluxo de execução e identificar a origem dos bugs.
  • Tratamento de Exceções : O tratamento de exceções pode ser mais complexo com Promises em comparação com outros padrões de programação assíncrona. Pode ser difícil gerenciar erros e exceções que ocorrem em uma cadeia Promise.

Tarefas Assíncronas/Aguardando

O recurso async/await faz parte do C# desde a versão 5.0 (2012) e foi introduzido no Unity 2017 com a implementação do tempo de execução .NET 4.x.


Na história do .NET, podemos distinguir as seguintes etapas:


  1. EAP (Padrão Assíncrono Baseado em Evento): Esta abordagem é baseada em eventos que são acionados após a conclusão de uma operação e um método regular que invoca esta operação.
  2. APM (Modelo de Programação Assíncrona): Essa abordagem é baseada em dois métodos. O método BeginSmth retorna a interface IAsyncResult . O método EndSmth usa IAsyncResult ; se a operação não for concluída no momento da chamada EndSmth , o thread será bloqueado.
  3. TAP (padrão assíncrono baseado em tarefa): esse conceito foi aprimorado pela introdução de async/await e dos tipos Task e Task<TResult> .


As abordagens anteriores tornaram-se obsoletas devido ao sucesso da última abordagem.

Para criar um método assíncrono, o método deve ser marcado com a palavra-chave async , conter um await dentro e o valor de retorno deve ser Task , Task<T> ou void (não recomendado).

 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 }


Neste exemplo, a execução ocorrerá assim:


  1. Primeiro, o código que precede a chamada para a primeira operação assíncrona ( SyncMethodA ) será executado.
  2. A primeira operação assíncrona await Task.Delay(1000) é iniciada e espera-se que seja executada. Enquanto isso, o código a ser chamado quando a operação assíncrona for concluída (a "continuação") será salvo.
  3. Após a conclusão da primeira operação assíncrona, a "continuação" — o código até a próxima operação assíncrona ( SyncMethodB ) começará a ser executada.
  4. A segunda operação assíncrona ( await Task.Delay(2000) ) é iniciada e espera-se que seja executada. Ao mesmo tempo, a continuação — o código após a segunda operação assíncrona ( SyncMethodC ) será preservada.
  5. Após a conclusão da segunda operação assíncrona, SyncMethodC será executado, seguido pela execução e aguardando a terceira operação assíncrona await Task.Delay(3000) .


Esta é uma explicação simplificada, pois na verdade async/await é um açúcar sintático para permitir a chamada conveniente de métodos assíncronos e aguardar sua conclusão.


Você também pode organizar qualquer combinação de ordens de execução usando WhenAll e 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"); });

IAsyncStateMachine

O compilador C# transforma chamadas async/await em uma máquina de estado IAsyncStateMachine , que é um conjunto sequencial de ações que devem ser executadas para concluir a operação assíncrona.


Toda vez que você chama uma operação de espera, a máquina de estado conclui seu trabalho e aguarda a conclusão dessa operação, após a qual continua a executar a próxima operação. Isso permite que operações assíncronas sejam executadas em segundo plano sem bloquear o thread principal e também torna as chamadas de método assíncronas mais simples e legíveis.


Assim, o método Example é transformado em criar e inicializar uma máquina de estado com a anotação [AsyncStateMachine(typeof(ExampleStateMachine))] , e a própria máquina de estado possui um número de estados igual ao número de chamadas await.


  • Exemplo do método transformado 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; }


  • Exemplo de uma máquina de estado gerada 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) { /*...*/ } }

SynchronizationContext

Na chamada AwaitUnsafeOnCompleted , o contexto de sincronização atual SynchronizationContext será obtido. SynchronizationContext é um conceito em C# usado para representar um contexto que controla a execução de um conjunto de operações assíncronas. Ele é usado para coordenar a execução do código em vários threads e para garantir que o código seja executado em uma ordem específica. O principal objetivo do SynchronizationContext é fornecer uma maneira de controlar o agendamento e a execução de operações assíncronas em um ambiente multithread.


Em diferentes ambientes, o SynchronizationContext tem diferentes implementações. Por exemplo, em .NET, existem:


  • WPF : System.Windows.Threading.DispatcherSynchronizationContext
  • WinForms : System.Windows.Forms.WindowsFormsSynchronizationContext
  • WinRT : System.Threading.WinRTSynchronizationContext
  • ASP.NET : System.Web.AspNetSynchronizationContext


O Unity também tem seu próprio contexto de sincronização, UnitySynchronizationContext , que nos permite usar operações assíncronas com ligação à API PlayerLoop. O exemplo de código a seguir mostra como girar um objeto em cada quadro usando Task.Yield() :

 private async void Start() { while (true) { transform.Rotate(0, Time.deltaTime * 50, 0); await Task.Yield(); } }


Outro exemplo de uso de async/await no Unity para fazer uma solicitação de rede:

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


Graças ao UnitySynchronizationContext , podemos usar métodos UnityEngine com segurança (como Debug.Log() ) logo após a conclusão de uma operação assíncrona, pois a execução desse código continuará no thread principal do Unity.

TaskCompletitionSource<T>

Esta classe permite que você gerencie um objeto Task . Ele foi criado para adaptar antigos métodos assíncronos ao TAP, mas também é muito útil quando queremos envolver uma Task em torno de alguma operação de longa duração que ocorre em algum evento.


No exemplo a seguir, o objeto Task dentro de taskCompletionSource será concluído após 3 segundos do início e obteremos seu resultado no método 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); } }

Token de Cancelamento

Um token de cancelamento é usado em C# para sinalizar que uma tarefa ou operação deve ser cancelada. O token é passado para a tarefa ou operação e o código dentro da tarefa ou operação pode verificar o token periodicamente para determinar se a tarefa ou operação deve ser interrompida. Isso permite um cancelamento limpo e elegante de uma tarefa ou operação, em vez de simplesmente eliminá-la abruptamente.


Tokens de cancelamento são comumente usados em situações em que uma tarefa de execução longa pode ser cancelada pelo usuário ou se a tarefa não for mais necessária, como um botão de cancelamento em uma interface de usuário.


O padrão geral se assemelha ao uso de TaskCompletionSource . Primeiro, um CancellationTokenSource é criado, então seu Token é passado para a operação assíncrona:

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


Quando a operação for cancelada, uma OperationCanceledException será lançada e a propriedade Task.IsCanceled será definida como true .

Novos recursos assíncronos no Unity 2022.2

É importante observar que os objetos Task são gerenciados pelo tempo de execução .NET, não pelo Unity, e se o objeto que executa a tarefa for destruído (ou se o jogo sair do modo de jogo no editor), a tarefa continuará sendo executada como o Unity não há como cancelá-lo.


Você sempre precisa acompanhar await Task com o CancellationToken correspondente. Isso leva a alguma redundância de código e, no Unity 2022.2, os tokens integrados no nível MonoBehaviour e todo o nível Application apareceram.


Vejamos como o exemplo anterior muda ao usar o destroyCancellationToken do objeto 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); } } }


Não precisamos mais criar manualmente um CancellationTokenSource e concluir a tarefa no método OnDestroy . Para tarefas não associadas a um MonoBehaviour específico, podemos usar UnityEngine.Application.exitCancellationToken . Isso encerrará a tarefa ao sair do Modo Play (no Editor) ou ao sair do aplicativo.

UniTask

Apesar da conveniência de uso e dos recursos fornecidos pelo .NET Tasks, eles apresentam desvantagens significativas quando usados no Unity:


  • Objetos Task são muito complicados e causam muitas alocações.
  • Task não corresponde ao encadeamento do Unity (encadeamento único).


A biblioteca UniTask ignora essas restrições sem usar threads ou SynchronizationContext . Ele atinge a ausência de alocações usando o tipo baseado em estrutura UniTask<T> .


O UniTask requer a versão de tempo de execução de script .NET 4.x, sendo o Unity 2018.4.13f1 a versão oficial com suporte mais baixo.


Além disso, você pode converter todas as AsyncOperations em UnitTask com métodos de extensão:

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


Neste exemplo, o método LoadAsset usa Resources.LoadAsync para carregar um ativo de forma assíncrona. O método AsUniTask é usado para converter o AsyncOperation retornado por LoadAsync em um UniTask , que pode ser aguardado.


Como antes, você pode organizar qualquer combinação de ordem de execução usando UniTask.WhenAll e 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 } }


No UniTask, há outra implementação do SynchronizationContext chamada UniTaskSynchronizationContext que pode ser usada para substituir UnitySynchronizationContext para melhor desempenho.

API aguardável

Na primeira versão alfa do Unity 2023.1, a classe Awaitable foi introduzida. Coroutines aguardáveis são tipos semelhantes a tarefas compatíveis com async/await projetados para serem executados no Unity. Ao contrário das Tarefas .NET, elas são gerenciadas pelo mecanismo, não pelo tempo de execução.

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


Eles podem ser aguardados e usados como o tipo de retorno de um método assíncrono. Em comparação com System.Threading.Tasks , eles são menos sofisticados, mas usam atalhos de aprimoramento de desempenho com base em suposições específicas do Unity.


Aqui estão as principais diferenças em comparação com o .NET Tasks:


  • O objeto Awaitable só pode ser aguardado uma vez; não pode ser aguardado por várias funções assíncronas.
  • Awaiter.GetResults() não bloqueará até a conclusão. Chamá-lo antes que a operação seja concluída é um comportamento indefinido.
  • Nunca capture um ExecutionContext . Por motivos de segurança, as Tarefas .NET capturam contextos de execução durante a espera para propagar contextos de representação em chamadas assíncronas.
  • Nunca capture SynchronizationContext . As continuações de corrotina são executadas de forma síncrona a partir do código que gera a conclusão. Na maioria dos casos, isso será do quadro principal do Unity.
  • Aguardáveis são objetos agrupados para evitar alocações excessivas. Esses são tipos de referência, portanto, podem ser referenciados em diferentes pilhas, copiados com eficiência e assim por diante. O ObjectPool foi aprimorado para evitar verificações de limites Stack<T> em sequências get/release típicas geradas por máquinas de estado assíncronas.


Para obter o resultado de uma operação demorada, você pode usar o tipo Awaitable<T> . Você pode gerenciar a conclusão de um Awaitable usando AwaitableCompletionSource e AwaitableCompletionSource<T> , semelhante a 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); } }


Às vezes é necessário realizar cálculos massivos que podem levar ao congelamento do jogo. Para isso, é melhor usar os métodos Awaitable: BackgroundThreadAsync() e MainThreadAsync() . Eles permitem que você saia do thread principal e retorne a ele.

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


Dessa forma, os Awaitables eliminam as desvantagens do uso de Tarefas .NET e também permitem a espera de eventos PlayerLoop e AsyncOperations.

Conclusão

Como podemos ver, com o desenvolvimento do Unity, existem cada vez mais ferramentas para organizar operações assíncronas:

Unidade

Corrotinas

Promessas

Tarefas .NET

UniTask

Tokens de cancelamento integrados

API aguardável

5.6





2017.1




2018.4



2022.2


2023.1


Consideramos todas as principais formas de programação assíncrona no Unity. Dependendo da complexidade da sua tarefa e da versão do Unity que você está usando, você pode usar uma ampla variedade de tecnologias, desde Coroutines e Promises até Tasks e Awaitables, para garantir uma jogabilidade suave e contínua em seus jogos. Obrigado pela leitura e aguardamos suas próximas obras-primas.