Im Mai 2022 kündigten Alexandre Mutel und Kristyna Hougaard in ihrem Beitrag „Unity und .NET, was kommt als nächstes?“ an. dass Unity plant, weitere Funktionen von .NET zu übernehmen, einschließlich der praktischen Verwendung von async-await. Und es scheint, dass Unity sein Versprechen einhält. In der Alpha-Version von Unity 2023.1 wurde die Awaitable-Klasse eingeführt, die mehr Möglichkeiten zum Schreiben von asynchronem Code bietet.
Wartemethoden
In diesem Abschnitt werde ich nicht zu tief auf Methoden eingehen, die meiner Meinung nach in der offiziellen Unity-Dokumentation ausreichend beschrieben sind. Sie hängen alle mit asynchronem Warten zusammen.
Mit Awaitable.WaitForSecondsAsync() können Sie eine bestimmte Spielzeit abwarten. Im Gegensatz zu Task.Delay(), das eine Wartezeit in Echtzeit durchführt. Um den Unterschied zu verdeutlichen, werde ich später in einem Codeblock ein kleines Beispiel bereitstellen.
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."); }
In diesem Beispiel wird zu Beginn der Start()-Methode die Spielzeit mithilfe von Time.timeScale gestoppt. Für das Experiment wird eine Coroutine verwendet, um den Ablauf nach 5 Sekunden in der RunGameplay()-Methode fortzusetzen. Dann starten wir zwei Methoden zum Warten von einer Sekunde. Einer verwendet Awaitable.WaitForSecondsAsync() und der andere verwendet Task.Delay(). Nach einer Sekunde erhalten wir in der Konsole die Meldung „Waiting WaitWithTaskDelay() beendet“. Und nach 5 Sekunden erscheint die Meldung „Waiting WaitWithTaskDelay() beendet“.
Außerdem wurden weitere praktische Methoden hinzugefügt, um Ihnen mehr Flexibilität in der grundlegenden Player-Schleife von Unity zu bieten. Ihr Zweck geht aus dem Namen hervor und entspricht ihrer Analogie bei der Verwendung von Coroutinen:
- EndOfFrameAsync()
- FixedUpdateAsync()
- NextFrameAsync()
Wenn Sie zum ersten Mal mit Coroutinen arbeiten, empfehle ich Ihnen, selbst damit zu experimentieren, um ein besseres Verständnis zu erlangen.
Außerdem wurde eine Methode, Awaitable.FromAsyncOperation(), hinzugefügt, um die Abwärtskompatibilität mit der alten API, AsyncOperation, zu gewährleisten.
Verwendung der destroyCancellationToken-Eigenschaft
Einer der Vorteile der Verwendung von Coroutinen besteht darin, dass sie automatisch gestoppt werden, wenn die Komponente entfernt oder deaktiviert wird. In Unity 2022.2 wurde die Eigenschaft destroyCancellationToken zu MonoBehaviour hinzugefügt, sodass Sie die asynchrone Ausführung zum Zeitpunkt der Objektlöschung stoppen können. Es ist wichtig zu bedenken, dass das Stoppen der Aufgabe durch den CancellationToken-Abbruch die OperationCanceledException auslöst. Wenn die aufrufende Methode weder „Task“ noch „Awaitable“ zurückgibt, sollte diese Ausnahme abgefangen werden.
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); }
In diesem Beispiel wird das Objekt in Start() sofort zerstört, aber zuvor gelingt es Awake(), die Ausführung von DoAwaitAsync() zu starten. Der Befehl Awaitable.WaitForSecondsAsync(1, destroyCancellationToken) wartet 1 Sekunde und sollte dann die Meldung „Diese Nachricht wird nicht protokolliert“ ausgeben. Da das Objekt sofort gelöscht wird, stoppt destroyCancellationToken die Ausführung der gesamten Kette, indem es die OperationCanceledException auslöst. Auf diese Weise befreit uns destroyCancellationToken von der Notwendigkeit, manuell ein CancellationToken zu erstellen.
Wir können dies aber trotzdem tun, um beispielsweise die Ausführung zum Zeitpunkt der Objektdeaktivierung zu stoppen. Ich werde ein Beispiel geben.
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."); } } }
In dieser Form wird die Nachricht „Diese Nachricht wird jede Sekunde protokolliert“ gesendet, solange das Objekt, an dem dieses MonoBehaviour hängt, eingeschaltet ist. Das Objekt kann ausgeschaltet und wieder eingeschaltet werden.
Dieser Code scheint überflüssig zu sein. Unity enthält bereits viele praktische Tools wie Coroutines und InvokeRepeating(), mit denen Sie ähnliche Aufgaben viel einfacher ausführen können. Dies ist jedoch nur ein Anwendungsbeispiel. Hier haben wir es nur mit Awaitable zu tun.
Verwenden der Application.exitCancellationToken-Eigenschaft
In Unity stoppt die Ausführung asynchroner Methoden nicht von selbst, selbst nachdem der Wiedergabemodus im Editor verlassen wurde. Fügen wir dem Projekt ein ähnliches Skript hinzu.
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); } } }
In diesem Beispiel wird nach dem Wechsel in den Play-Modus die Meldung „Diese Nachricht wird jede Sekunde protokolliert“ auf der Konsole ausgegeben. Die Ausgabe erfolgt auch nach Loslassen der Play-Taste weiter. In diesem Beispiel wird Task.Delay() anstelle von Awaitable.WaitForSecondsAsync() verwendet, da hier zum Anzeigen der Aktion eine Verzögerung nicht in der Spielzeit, sondern in Echtzeit benötigt wird.
Analog zu destroyCancellationToken können wir Application.exitCancellationToken verwenden, das die Ausführung asynchroner Methoden beim Verlassen des Play-Modus unterbricht. Lassen Sie uns das Skript reparieren.
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); } } }
Jetzt wird das Skript wie vorgesehen ausgeführt.
Verwendung mit Ereignisfunktionen
In Unity können einige Ereignisfunktionen Coroutinen sein, beispielsweise Start, OnCollisionEnter oder OnCollisionExit. Aber ab Unity 2023.1 können alle davon „Awaitable“ sein, einschließlich Update(), LateUpdate und sogar OnDestroy().
Sie sollten mit Vorsicht verwendet werden, da nicht auf ihre asynchrone Ausführung gewartet werden muss. Zum Beispiel für den folgenden Code:
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()"); }
In der Konsole erhalten wir das folgende Ergebnis:
Awake() started OnEnable() Start() Awake() finished
Denken Sie auch daran, dass das MonoBehaviour selbst oder sogar das Spielobjekt möglicherweise nicht mehr existiert, während der asynchrone Code noch ausgeführt wird. In solch einer Situation:
private async Awaitable Awake() { Debug.Log(this != null); await Awaitable.NextFrameAsync(); Debug.Log(this != null); } private void Start() { Destroy(this); }
Im nächsten Frame gilt das MonoBehaviour als gelöscht. In der Konsole erhalten wir das folgende Ergebnis:
True Flase
Dies gilt auch für die OnDestroy()-Methode. Wenn Sie die Methode asynchron gestalten, sollten Sie berücksichtigen, dass nach der Wait-Anweisung das MonoBehaviour bereits als gelöscht gilt. Wenn das Objekt selbst gelöscht wird, funktioniert die Arbeit vieler darauf befindlicher MonoBehaviours zu diesem Zeitpunkt möglicherweise nicht ordnungsgemäß.
Beachten Sie, dass es bei der Arbeit mit Ereignisfunktionen wichtig ist, die Ausführungsreihenfolge zu kennen. Asynchroner Code wird möglicherweise nicht in der erwarteten Reihenfolge ausgeführt. Dies müssen Sie beim Entwerfen Ihrer Skripte unbedingt berücksichtigen.
Erwartbare Ereignisfunktionen fangen alle Arten von Ausnahmen ab
Es ist erwähnenswert, dass Awaitable-Event-Funktionen alle Arten von Ausnahmen abfangen, die unerwartet sein können. Ich hatte erwartet, dass sie nur OperationCanceledExceptions abfangen würden, was sinnvoller gewesen wäre. Da sie jedoch alle Arten von Ausnahmen abfangen, sind sie zum jetzigen Zeitpunkt nicht für den Einsatz geeignet. Stattdessen können Sie asynchrone Methoden ausführen und die erforderlichen Nachrichten manuell abfangen, wie im vorherigen Beispiel gezeigt.
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); }
Da die Komponente beim Start sofort gelöscht wird, wird die Ausführung von DoAwaitAsync() unterbrochen. Die Meldung „Diese Nachricht wird nicht protokolliert“ wird nicht in der Konsole angezeigt. Nur OperationCanceledException() wird abgefangen, alle anderen Ausnahmen können ausgelöst werden.
Ich hoffe, dass dieser Ansatz in Zukunft korrigiert wird. Derzeit ist die Verwendung von Awaitable Event Functions nicht sicher.
Freie Bewegung über Threads hinweg
Bekanntlich sind alle Operationen mit Spielobjekten und MonoBehaviours nur im Hauptthread erlaubt. Manchmal sind umfangreiche Berechnungen erforderlich, die zum Einfrieren des Spiels führen können. Es ist besser, sie außerhalb des Hauptthreads auszuführen. Awaitable bietet zwei Methoden, BackgroundThreadAsync() und MainThreadAsync(), die es ermöglichen, vom Hauptthread wegzugehen und zu ihm zurückzukehren. Ich werde ein Beispiel geben.
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}"); }
Hier wechselt die Methode beim Start zu einem zusätzlichen Thread. Hier gebe ich die ID dieses zusätzlichen Threads an die Konsole aus. Es wird nicht gleich 1 sein, da 1 der Hauptthread ist.
Anschließend wird der Thread für 10 Sekunden eingefroren (Thread.Sleep(10000)), wodurch umfangreiche Berechnungen simuliert werden. Wenn Sie dies im Hauptthread tun, scheint das Spiel für die Dauer seiner Ausführung einzufrieren. Aber in dieser Situation funktioniert alles weiterhin stabil. Sie können in diesen Berechnungen auch ein CancellationToken verwenden, um einen unnötigen Vorgang zu stoppen.
Danach wechseln wir zurück zum Hauptthread. Und nun stehen uns wieder alle Unity-Funktionen zur Verfügung. Zum Beispiel, wie in diesem Fall, das Deaktivieren eines Spielobjekts, was ohne den Hauptthread nicht möglich war.
Abschluss
Zusammenfassend lässt sich sagen, dass die in Unity 2023.1 eingeführte neue Awaitable-Klasse Entwicklern mehr Möglichkeiten zum Schreiben von asynchronem Code bietet und es so einfacher macht, reaktionsfähige und leistungsstarke Spiele zu erstellen. Die Awaitable-Klasse umfasst eine Vielzahl von Wartemethoden, wie etwa WaitForSecondsAsync(), EndOfFrameAsync(), FixedUpdateAsync() und NextFrameAsync(), die mehr Flexibilität in der grundlegenden Player-Schleife von Unity ermöglichen. Die Eigenschaften destroyCancellationToken und Application.exitCancellationToken bieten auch eine praktische Möglichkeit, die asynchrone Ausführung zum Zeitpunkt der Objektlöschung oder des Verlassens des Wiedergabemodus zu stoppen.
Es ist wichtig zu beachten, dass die Awaitable-Klasse zwar eine neue Möglichkeit zum Schreiben von asynchronem Code in Unity bietet, sie jedoch in Verbindung mit anderen Unity-Tools wie Coroutines und InvokeRepeating verwendet werden sollte, um die besten Ergebnisse zu erzielen. Darüber hinaus ist es wichtig, die Grundlagen von async-await und die Vorteile zu verstehen, die es für den Spieleentwicklungsprozess mit sich bringen kann, wie z. B. eine Verbesserung der Leistung und Reaktionsfähigkeit.
Zusammenfassend lässt sich sagen, dass die Awaitable-Klasse ein leistungsstarkes Tool für Unity-Entwickler ist, sie sollte jedoch mit Vorsicht und in Verbindung mit anderen Unity-Tools und -Konzepten verwendet werden, um die besten Ergebnisse zu erzielen. Es ist wichtig, damit zu experimentieren, um seine Fähigkeiten und Grenzen besser zu verstehen.