Skip to content

async / await

Work that does not fit in one frame goes in an async FrameTask method. The method suspends at each await, returns to Udon, and resumes from there on a later frame (nothing runs in parallel, and by default only one instance runs at a time).

SyntaxDescriptionNote
async FrameTask M()Suspending and resumingThe caller does not wait for completion; it proceeds to the statement after the call within the same frame.
await Async.Frames(30)Specifying the frame countWaits are counted in frames only; seconds are not available.
[MaxTasks(4)]How many can run at onceThe attribute is only needed when you want a count (without it, one at a time). Each one keeps its own arguments and intermediate values, and they advance independently. A call made while every slot is taken does not start. Note that the body is duplicated once per slot, so the program grows with the number.
FrameTask t = M(); if (!t.Started)Checking whether it startedA call made while the previous run has not finished does not start, and Started is false.
if (i % 256 == 255) await Async.Frame();Splitting a loopThe loop variable i and the running total keep their values across the suspension.
async FrameTask Fade(int frames, int target)Passing argumentsThe argument target still reads the value it had at the call, even after await.
await Async.Frames(speed * 2 + 1)An expression for the frame countThe expression is evaluated when the await is reached.
private static async FrameTask Tick()static methods
await Fade(30)Waiting for another frame-spanning methodIf it is still running, the caller waits for it to finish and then starts it (this is where await differs from a plain call, which does not start it at all). If it never suspends, the rest continues without crossing a frame.
async FrameTask Start()Making an event span framesUdon still calls Start as before, and the suspended remainder resumes on a later frame. Field initializers run before the body. If you do this to an event that fires every frame, such as Update, the later calls do not start because the previous run has not finished. An event inherited from the base class, such as Interact(), cannot span frames: C# does not allow an override to change its return type.
SyntaxDescriptionErrorReasonAlternative
async void M()No return value, so you cannot read whether it startedTUKI0001by designReturn async FrameTask
[MaxTasks(0)]Writing a count below oneTUKI0001by designWrite one or more
[MaxTasks(100000)]Writing a count above the limitTUKI0001by designLower the count (the body is duplicated, so the program grows with it)
await AMethodWithTwoOrMoreSlots()Awaiting a method that has more than one slotTUKI0001by designCall it without await, or give the awaited method a single slot
await B() in A(), await A() in B()Two awaiting methods wait for each otherTUKI0001by designCall it without await, or break the cycle
async FrameTask M<T>()Take a type parameterTUKI0001not yetRewrite it without the type parameter
Async.Frame();Write the wait without awaitTUKI0001by designAdd await
FrameTask t = M(); await t;Awaiting a FrameTask held in a variableTUKI0001by designWrite await M(), awaiting the call itself
yield returnReturn values one at a timeTUKI0001runtimeUse async and await to span frames; fill an array to return values

The constructs allowed in the body are in Constructs allowed in an async method; combining it with the GPU is in GPU kernels and await.

The method suspends where await Async.Frame() is written and resumes from there on the next frame.

using UnityEngine;
using Tsukimi;
public class ParAsyncFrames : TsukimiBehaviour
{
public int total;
private async FrameTask Sum()
{
int running = 0;
for (int i = 0; i < 1000; i++)
{
running += i;
if (i % 128 == 127) await Async.Frame();
}
total = running;
}
void Start()
{
Sum();
}
}

Async.Frames(n) waits the specified number of frames before resuming.

using UnityEngine;
using Tsukimi;
public class ParAsyncWaitMany : TsukimiBehaviour
{
public int step;
private async FrameTask Countdown()
{
step = 3;
await Async.Frames(30);
step = 2;
await Async.Frames(30);
step = 1;
await Async.Frames(30);
step = 0;
}
void Start()
{
Countdown();
}
}

Writing a number in the attribute lets that many copies of the method run at once.

using UnityEngine;
using Tsukimi;
public class ParAsyncSeats : TsukimiBehaviour
{
public int finished;
[MaxTasks(4)]
private async FrameTask Petal(int frames)
{
await Async.Frames(frames);
finished = finished + 1;
}
public override void Interact()
{
Petal(10);
Petal(20);
Petal(30);
Petal(40);
}
}

Started on the return value tells whether that call started the work.

using UnityEngine;
using Tsukimi;
public class ParAsyncSeat : TsukimiBehaviour
{
public int done;
private async FrameTask Once()
{
await Async.Frames(10);
done = done + 1;
}
public override void Interact()
{
FrameTask task = Once();
if (!task.Started) return;
}
}

Placing await Async.Frame() every 256 iterations advances 256 iterations per frame.

using UnityEngine;
using Tsukimi;
public class ParAsyncLoopSplit : TsukimiBehaviour
{
public int[] board;
public int score;
private async FrameTask Evaluate()
{
int sum = 0;
for (int i = 0; i < board.Length; i++)
{
sum += board[i] * board[i];
if (i % 256 == 255) await Async.Frame();
}
score = sum;
}
public override void Interact()
{
Evaluate();
}
}

A frame-spanning method can take arguments.

using UnityEngine;
using Tsukimi;
public class ParAsyncArgs : TsukimiBehaviour
{
public int level;
private async FrameTask Fade(int frames, int target)
{
await Async.Frames(frames);
level = target;
}
public override void Interact()
{
Fade(30, 100);
}
}

The number of frames to wait can be an expression.

using UnityEngine;
using Tsukimi;
public class ParAsyncComputedFrames : TsukimiBehaviour
{
public int speed;
public int done;
private async FrameTask Wait()
{
await Async.Frames(speed * 2 + 1);
done = done + 1;
}
void Start()
{
speed = 10;
Wait();
}
}

A frame-spanning method can also be static.

using UnityEngine;
using Tsukimi;
public class ParAsyncStatic : TsukimiBehaviour
{
private static int ticks;
private static async FrameTask Tick()
{
await Async.Frame();
ticks = ticks + 1;
}
void Start()
{
Tick();
}
}

Awaiting one of your own frame-spanning methods runs the rest only after that one finishes.

using UnityEngine;
using Tsukimi;
public class ParAsyncAwaitAsync : TsukimiBehaviour
{
public int step;
private async FrameTask Fade(int frames)
{
await Async.Frames(frames);
}
private async FrameTask Sequence()
{
step = 1;
await Fade(30);
step = 2;
await Fade(30);
step = 3;
}
public override void Interact()
{
Sequence();
}
}

A Unity event can itself be a frame-spanning method.

using UnityEngine;
using Tsukimi;
public class ParAsyncEntry : TsukimiBehaviour
{
public int step;
private async FrameTask Start()
{
step = 1;
await Async.Frames(30);
step = 2;
await Async.Frames(30);
step = 3;
}
}