Async with OnityTask

OnityTask and OnityTask<T> are Onity’s Unity-facing awaitables. They cover frame waits, delays, predicates, scene loading, AsyncOperation, web requests, reactive streams, and async message delivery without adding a third-party runtime package.

using Onity.Unity.Async;

Use OnityTask for gameplay flows driven by Unity. Keep Task when a plain .NET service already exposes it as part of its contract; bridge at the boundary with OnityTask.FromTask(...) or AsTask().

Start and cancel a Unity flow

Own a CancellationTokenSource for the same lifetime as the component that started the work. Cancel it in OnDisable when the flow must stop while the component is inactive.

using System;
using System.Threading;
using Onity.Unity.Async;
using UnityEngine;

public sealed class BootFlow : MonoBehaviour
{
    private CancellationTokenSource m_lifetime;

    private void OnEnable()
    {
        m_lifetime = new CancellationTokenSource();
        RunAsync(m_lifetime.Token).Forget(Debug.LogException);
    }

    private void OnDisable()
    {
        m_lifetime.Cancel();
        m_lifetime.Dispose();
        m_lifetime = null;
    }

    private static async OnityTask RunAsync(CancellationToken cancellationToken)
    {
        try
        {
            await OnityTask.NextFrame(cancellationToken);
            await OnityTask.Delay(0.25f, cancellationToken);
            await OnityTask.WaitUntil(() => IsReady(), cancellationToken);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // Normal lifetime cancellation; real faults still reach Forget's handler.
        }
    }

    private static bool IsReady()
    {
        return true;
    }
}

Cancellation of pooled Unity waits is completed through the Onity player-loop runner, so the awaiting continuation resumes on Unity’s main thread. Canceling a fixed-frame wait also completes while Time.timeScale is zero.

Common operations

Need API
Next rendered frame await OnityTask.NextFrame(ct)
Next fixed update await OnityTask.NextFixedFrame(ct)
Next late update await OnityTask.NextLateFrame(ct)
Scaled delay await OnityTask.Delay(seconds, ct)
Unscaled delay await OnityTask.DelayUnscaled(seconds, ct)
Wait for a condition await OnityTask.WaitUntil(predicate, ct)
Wait while a condition holds await OnityTask.WaitWhile(predicate, ct)
Wait for several operations await OnityTask.WhenAll(tasks)
Completed typed result await OnityTask.FromResult(value)

Single-consumer rule for pooled tasks

Frame, delay, predicate, and AsyncOperation.AsOnityTask() operations use pooled sources. Each returned OnityTask value is single-consumer:

  • Await the value once, or call AsTask() once.
  • Do not copy the value to several consumers.
  • Do not await it and then call AsTask() on the old copy.
  • WhenAll consumes its input task values; do not await those inputs separately.

When several consumers must observe one operation, materialize one independent Task and share that instead:

using System.Threading.Tasks;
using Onity.Unity.Async;

OnityTask wait = OnityTask.NextFrame();
Task shared = wait.AsTask();

await shared;
// Other consumers may await the same Task instance.

Generation checks reject stale pooled task copies instead of letting them read a later operation that reused the same source.

Scene loading

private static async OnityTask LoadGameplayAsync(CancellationToken cancellationToken)
{
    await OnityTask.LoadScene(
        "Gameplay",
        progress => Debug.Log($"Loading: {progress:P0}"),
        cancellationToken);
}

LoadSceneAdditive, UnloadScene, and ActivateScene use the same progress and cancellation shape. LoadSceneAsync returns the underlying AsyncOperation when you need to control activation yourself.

Unity AsyncOperation bridge

ResourceRequest request = Resources.LoadAsync<TextAsset>("GameConfig");
ResourceRequest completed = await request.AsOnityTask(
    cancellationToken: cancellationToken);

The operation itself is not canceled by every Unity API; cancellation stops the await and reports OperationCanceledException. Consult the Unity API you wrap when the underlying operation has separate cancellation behavior.

Web requests

The caller owns a request passed to Send and must dispose it:

using UnityEngine.Networking;

using UnityWebRequest request = UnityWebRequest.Get(url);
UnityWebRequest completed = await OnityTask.Send(
    request,
    progress => Debug.Log($"Download: {progress:P0}"),
    cancellationToken);

GetJson<TResponse> and PostJson<TRequest, TResponse> provide compact JsonUtility-based DTO helpers. Failed requests throw OnityUnityWebRequestException with the response details.

Reactive and messaging bridges

public readonly struct SaveRequested
{
}

int firstScore = await scoreStream.FirstOnityTask(cancellationToken);

await asyncPublisher.PublishOnityTask(
    new SaveRequested(),
    cancellationToken);

Use FirstOnityTask / ToOnityTask for reactive streams and PublishOnityTask / SubscribeOnityTask for Onity’s async message channels.

Fire and forget

Prefer await. When a detached operation is intentional, call Forget so exceptions reach a callback or the Unity log:

OnityTask.LoadScene("Gameplay").Forget(Debug.LogException);

Long-running operations appear in Onity → Diagnostics → Task Tracker when tracking is enabled. Stack-trace capture is useful for leak diagnosis but adds Editor allocation overhead, so leave it disabled during performance runs.

See also