Unity bridges
This page shows how to connect Unity’s async mechanisms (UnityEvent, AsyncOperation, Addressables, Awaitable, Task) to flows. It also covers the helpers for waiting on uGUI buttons and the Scope Tree window for inspecting running flows.
Turning a UnityEvent into a Signal
Section titled “Turning a UnityEvent into a Signal”async FlowTask Shop(UnityEvent<int> onPurchased){ using var purchased = onPurchased.ToSignal(); // EventSignal<int>, owned by the current scope var itemId = await purchased.Next();}UnityEvent and UnityEvent<T> have ToSignal(). You can wait on the returned EventSignal<T> with Next(), NextOrClosed(), and Subscribe(policy).
-
Call it from flow code. The current scope owns the listener; it is removed when the scope ends (or on an earlier Dispose), and the EventSignal closes. If you call it outside flow code, dispose it yourself.
-
Invoke does not resume receivers immediately. They resume at the next Flush point or Tick (see “Tick and Flush” in Setup).
-
For events with two or more arguments, bridge them with
FlowBridge.FromCallbackand a single lambda.using var hits = FlowBridge.FromCallback<(GameObject, int)>(emit =>{UnityAction<GameObject, int> listener = (target, damage) => emit((target, damage));onHit.AddListener(listener);return () => onHit.RemoveListener(listener); // called when the scope ends});
Next() is an edge
Section titled “Next() is an edge”Next() only receives Invokes that happen after you start waiting. Invokes while nobody is waiting are dropped. If you don’t want to miss Invokes that happen while the loop body runs, subscribe before the loop.
using var purchased = onPurchased.ToSignal();using var queue = purchased.Subscribe(BufferPolicy.Latest); // keeps the latest one while the loop body runswhile (true){ var itemId = await queue.Next(); await Grant(itemId);}The kinds of BufferPolicy (Latest, Queue(capacity, overflow)) and the difference between edges and subscriptions are covered in Signals. The analyzer reports an info diagnostic (FLOW006) for code that waits on Next() inside a loop.
Callbacks that arrive after closing
Section titled “Callbacks that arrive after closing”Waiting with Next() on an EventSignal after it has closed throws SignalClosedException, and values that arrive after it has closed are silently dropped. Receive callbacks that can arrive after a screen closes, such as rewarded ad rewards or purchase results, in a flow that outlives the screen (a service started with FlowTaskUnity.World.Run).
Waiting on an AsyncOperation
Section titled “Waiting on an AsyncOperation”await SceneManager.LoadSceneAsync("Battle").AsFlow();var tex = (Texture2D)await Resources.LoadAsync<Texture2D>("icon").AsFlow(); // a ResourceRequest returns the assetAsyncOperation.AsFlow()returns aFlowTask, andResourceRequest.AsFlow()returns aFlowTask<UnityEngine.Object>that yields the loaded asset.- Completion is received through
AsyncOperation.completed, and the flow resumes at the next Flush point or Tick. An operation that has already completed completes immediately, without allocating. - If the scope is canceled first, the handler is removed and the completion is ignored.
AsyncOperationhas no API to cancel it, so the bridge only stops waiting, and the operation keeps running.- Stop an operation that can be stopped in the scope’s cleanup (
finallyorFlow.AddCleanup), for example withUnityWebRequest.Abort()(see “Addressables and UnityWebRequest” below). - Loads that finish after the bridge stopped waiting (a scene, an AssetBundle) are cleaned up by the flow’s own code. Only when the flow stopped waiting (it was canceled), register the cleanup on
operation.completed. A handler registered on a finished operation is called too.
- Stop an operation that can be stopped in the scope’s cleanup (
- To show progress, race the load against a loop that reads
progressevery frame (the parallel loading scenario in Samples).
Note: Inside a FlowTask method, do not await an
AsyncOperationorAwaitabledirectly without.AsFlow(). It is an await outside the scope, so when it suspends there, the scope ends withFlowMisuseException. The analyzer reports it as error FLOW002.
Addressables and UnityWebRequest
Section titled “Addressables and UnityWebRequest”An Addressables AsyncOperationHandle is not an AsyncOperation, so bridge its .Task with FlowBridge.FromTask.
using var cancel = view.CancelButton.ClickedSignal();var r = await FlowTask.Race( cancel.Next(), FlowBridge.FromTask( _ => Addressables.LoadAssetAsync<GameObject>("Boss").Task, onDiscard: asset => Addressables.Release(asset)).ToFlowTask()); // a result nobody received is releasedif (!r.TryGet1(out var boss)) return; // canceled: onDiscard releases the asset when it arrives- The load takes no token, so a canceled bridge only stops waiting. A result that arrives later goes to
onDiscard(seeonDiscardin Task / ValueTask bridges). - To pass a bridge with a result to a Race and similar, add
.ToFlowTask(). Without it, the type arguments cannot be inferred. - Loading several assets with progress, and releasing together every one nobody received, is shown in the parallel loading scenario in Samples.
UnityWebRequest’s SendWebRequest() is an AsyncOperation, so you can await it with .AsFlow(). When the flow is canceled, call Abort() in finally to stop the request. Put a real-time limit on it with timeout, because FlowTask.WaitForSeconds advances in game time and stops while the game is paused (see “Timeouts” in Composition).
using var request = UnityWebRequest.Get(url);request.timeout = 10; // a real-time limit that a paused game doesn't stoptry{ await request.SendWebRequest().AsFlow();}finally{ if (!request.isDone) request.Abort(); // the scope gave up: stop the request}Waiting on an Awaitable
Section titled “Waiting on an Awaitable”await Awaitable.WaitForSecondsAsync(1f).AsFlow();int n = await ComputeAsync().AsFlow(); // Awaitable<T>Awaitable.AsFlow()is built onFlowBridge.FromTaskand, liketask.AsFlow(), returns aTaskBridge(TaskBridge<T>forAwaitable<T>). Besides awaiting it, you can pass it to a Race and similar. When you pass aTaskBridge<T>, add.ToFlowTask().- Whatever thread it completes on (including
Awaitable.BackgroundThreadAsync()), the flow resumes on the World’s thread. - When the scope is canceled, it calls
Awaitable.Cancel()and ignores the result. - An Awaitable canceled outside the flow throws
OperationCanceledExceptionat the await. To catch it, writecatch (OperationCanceledException e) when (e is not FlowCanceledException). Canceling the scope doesn’t reach thiscatch; the flow simply unwinds.
In Unity, calling Cancel() on the Awaitable of an async method does not propagate to the Awaitables it is waiting on inside. To stop the waits inside too, have the method take a CancellationToken and pass it to those waits, and pass the scope’s token with FlowBridge.FromTask.
var map = await FlowBridge.FromTask(async ct => await BuildMapAsync(ct)); // the scope's token reaches the awaits inside
static async Awaitable<Map> BuildMapAsync(CancellationToken ct){ await Awaitable.NextFrameAsync(ct); // stops when the scope is canceled return Map.Generate();}Task and ValueTask bridges in general (FromTask, onDiscard, external cancellation) are covered in Task / ValueTask bridges.
Task and UnitySynchronizationContext
Section titled “Task and UnitySynchronizationContext”Unity’s main thread has a UnitySynchronizationContext. If you await without ConfigureAwait(false) inside a FlowBridge.FromTask factory, the continuation is posted to this context and runs in the Update phase (ScriptRunDelayedTasks).
var bytes = await FlowBridge.FromTask(async ct =>{ var data = await File.ReadAllBytesAsync(path, ct); // continues on the main thread in ScriptRunDelayedTasks return Decode(data);});This is fine in a normal game. But code that blocks the main thread while waiting for a Task to complete (such as a test that runs Ticks in a loop) never finishes, because the continuation never runs. In such code, add ConfigureAwait(false) to the awaits inside the factory. How to handle this in tests is covered in Testing.
Move heavy computation to another thread with FlowBridge.FromTask(ct => Task.Run(() => Work(), ct)). The same goes for work an Awaitable moved to another thread with BackgroundThreadAsync(): the flow receives the result on the World’s thread (see Threads).
When a completed Task resumes its flow:
- The bridge queues the completion to the World synchronously, on the thread that completed the Task. Whatever thread it completes on, the flow resumes at the next Tick or Flush point.
- A Task completed in
MonoBehaviour.Updateresumes its flow at the Flush at the end of Update in the same frame.
In the other direction, to wait on a flow from Task code, use FlowHandle.AsTask(). This Task completes on the World’s thread right after the Tick, Flush, Run, or Dispose in which the flow ended. If the main thread blocks on this Task with .Result or Wait(), the Tick stops too, so it never finishes (see “FlowHandle” in Flows and the World).
uGUI buttons
Section titled “uGUI buttons”The FlowTask.Unity.UI assembly (compiled only when com.unity.ugui is present) has helpers for waiting on button clicks.
using var ok = view.OkButton.ClickedSignal(); // EventSignal<FlowUnit> over Button.onClickawait ok.Next();button.ClickedSignal()returns anEventSignal<FlowUnit>. For other controls, bridge the UnityEvent directly (toggle.onValueChanged.ToSignal()). LikeToSignal()above, call all of these from flow code.- Clicks happen in uGUI’s EventSystem (in the Update phase), so a waiting flow resumes at the Flush at the end of Update in the same frame.
Catching clicks during an animation
Section titled “Catching clicks during an animation”Next() is an edge, so clicks before you start waiting (during a screen’s opening animation, for example) are not delivered, and no warning is given (the same as UniTask’s button.OnClickAsync()). For clicks you want to catch, create the signal and subscribe before the animation. Creating the signal before the animation is not enough.
using var ok = view.OkButton.ClickedSignal();using var okClicks = ok.Subscribe(BufferPolicy.Latest); // before the animation: a click during it is keptawait FadeIn(view);await okClicks.Next();For clicks you want to drop, such as repeated taps during a purchase, keep using Next() (see the purchase scenario in Samples).
Using it with UniTask
Section titled “Using it with UniTask”FlowTask and UniTask run on the same PlayerLoop. The conversion APIs and a migration guide are in UniTask bridge and migration; this section only covers ordering on Unity’s PlayerLoop.
- UniTask inserts its runners at both the start and the end of each phase (
PlayerLoopTiming.LastUpdateand so on). Continuations atPlayerLoopTiming.Updaterun before FlowTask’s Tick. If such a continuation changesTime.timeScale, the new value takes effect from the next frame (see “Time” in Setup). - Continuations at
PlayerLoopTiming.LastUpdaterun afterMonoBehaviour.Update. An Emit there resumes its receivers within the same frame, at the Flush at the end of PreLateUpdate at the latest. - A flow that bridges a wait completed by UniTask’s
DelayFrameresumes within that frame. ToUniTask()posts its completion to the SynchronizationContext current at the time of the call. One called on the main thread completes inScriptRunDelayedTasks.- Reinstalling FlowTask’s systems with
FlowTaskUnity.Configurekeeps UniTask’s systems.
Scope Tree window
Section titled “Scope Tree window”Open it with Window > FlowTask > Scope Tree. During Play Mode, it shows the scope tree of the default World and of any World registered with FlowWorldRegistry.Register(world), refreshing every 0.25 seconds.
- It shows each node’s kind (scope, combinator, wait), its Clock, what it is waiting on, where that was created, and for how long (
waiting: Signal.Next at Enemy.cs:42 for 2.3s, for example), and scopes being canceled (in orange, with the reason). The top shows the number of live scopes and, for each Clock, its time, Pause state, and scale. - The toolbar toggles auto-refresh, text view (the output of
FlowWorld.Dump()), showing scopes only, a name filter, and copying the dump. If there are several Worlds, you can choose which one to show. - Double-clicking a row opens the line where its wait was created (
FlowScopeInfo.WaitingFile,WaitingLine). The scope row of a FlowTask method without a place (one that awaits the call of another FlowTask method) opens the script that declares the method. The context menu opens either. - If the game is stuck and won’t move (a Pause you forgot to release), look at
Paused clocks:at the top of the text view. For each paused Clock, it shows the pause count and the owners (for example,Game: paused x2 by Main > PauseMenu, <outside any flow>). - When there is no World (outside Play Mode, after recompiling while playing, and so on), it shows the reason.
To show a World you created, register it when you create it and unregister it before disposing it (disposed Worlds disappear from the list automatically).
FlowWorldRegistry.Register(world); // shown in the Scope Tree window// ...FlowWorldRegistry.Unregister(world);world.Dispose();How to read the dump and the diagnostics API are covered in Debugging and diagnostics.