Running on physics frames
The World of FlowWorldNode runs in _Process. For flows that should advance every physics frame, create a second World and Tick it from _PhysicsProcess. This page explains how to create that World and how the two Worlds relate. To wait for a single physics frame, you don’t need another World (see “Waiting for a single physics frame” below).
Creating a physics World
Section titled “Creating a physics World”Add a few lines to the autoload subclass.
// res://FlowAutoload.csusing System;using Katout.FlowTask;using Katout.FlowTask.Godot;using Godot;
public partial class FlowAutoload : FlowWorldNode{ public FlowAutoload() => ProcessPhysicsPriority = int.MinValue; // before other nodes' _PhysicsProcess
public static FlowWorld Physics { get; private set; }
public override void _EnterTree() { base._EnterTree(); if (Physics == null || Physics.IsDisposed) { Physics = new FlowWorld("GodotPhysics"); // Route exception reports and warnings to Godot's log like FlowWorldNode does // (without OnUnhandledException, an unhandled exception is thrown from _PhysicsProcess) Physics.OnUnhandledException = info => { if (info.Kind == FlowExceptionKind.Undelivered) GD.PushWarning($"[FlowTask] {info.Kind} at '{info.ScopePath}': {info.Exception}"); else GD.PushError($"[FlowTask] {info.Kind} at '{info.ScopePath}': {info.Exception}"); }; Physics.OnWarning += w => GD.PushWarning("[FlowTask] " + w); } }
// Same time model as FlowWorldNode: tick with the unscaled step, let DefaultClock follow Engine.TimeScale public override void _PhysicsProcess(double delta) { var w = Physics; if (w == null || w.IsDisposed) return; var scale = Engine.TimeScale; if (scale >= 0 && w.DefaultClock.TimeScale != scale) ApplyTimeScale(w, scale); w.Tick(1.0 / Engine.PhysicsTicksPerSecond); }
// A rejected scale goes to the log and the World ticks with the previous one; // skipping the Tick on the exception would stop the physics World static void ApplyTimeScale(FlowWorld w, double scale) { try { w.DefaultClock.TimeScale = scale; } catch (ArgumentOutOfRangeException ex) { GD.PushError($"[FlowTask] Engine.TimeScale {scale} was not applied to the physics World. {ex.Message}"); } }
public override void _ExitTree() { Physics?.Dispose(); // unwinds the physics flows Physics = null; base._ExitTree(); }}Start flows with this World’s Run.
FlowAutoload.Physics.Run(Dash(body));
async FlowTask Dash(CharacterBody2D body){ for (var i = 0; i < 10; i++) { body.Velocity = new Vector2(600, 0); body.MoveAndSlide(); await FlowTask.NextFrame(); // = the next physics frame }}How time advances
Section titled “How time advances”FlowTask.NextFrame()andFlowTask.DelayFrames(n)count physics frames.UnscaledClockadvances by the physics step (1 / Engine.PhysicsTicksPerSecond).DefaultClockadvances by Godot’s physics delta (step ×Engine.TimeScale). This is the same time handling as the main World, so the Clocks of the two Worlds stay in step (see Time in Setup).- Do not pass
_PhysicsProcess’sdeltastraight toTick. EvenUnscaledClockwould followEngine.TimeScaleand stop while the scale is 0. Pass the step, as above. - The physics step is fixed, and Godot limits the number of steps per frame. So there is no need to clamp long frames as the main World does.
Unhandled exceptions and shutdown
Section titled “Unhandled exceptions and shutdown”Set OnUnhandledException on any World you create yourself. Without it, unhandled exceptions and the other reports are thrown as FlowUnhandledException from the outermost Tick (_PhysicsProcess), Flush, Run or Dispose (_ExitTree). Handling them is covered in Failures.
The physics World also unwinds all its flows on Dispose in _ExitTree. The caveats on shutdown (Dispose does not run Ticks, so an await in finally only runs up to its first await) are the same as for the main World (see Shutdown in Setup).
How the two Worlds relate
Section titled “How the two Worlds relate”Both Worlds are on the main thread, so they can work together through Signal<T>. When a flow in the main World Emits, a flow waiting in the physics World resumes at the next physics Tick.
// main World: input decides, physics World: moves the bodyreadonly Signal<FlowUnit> _dashRequested = new("DashRequested");
async FlowTask InputLoop() // run on FlowWorldNode.Default{ while (true) { await FlowTask.WaitUntil(() => Input.IsActionJustPressed("dash")); _dashRequested.Emit(FlowUnit.Default); // resumes PhysicsLoop at the next physics Tick await FlowTask.NextFrame(); }}
async FlowTask PhysicsLoop(CharacterBody2D body) // run on FlowAutoload.Physics{ using var requests = _dashRequested.Subscribe(BufferPolicy.Latest); while (true) { await requests.Next(); await Dash(body); }}Scopes have no parent-child relationship across Worlds. When a flow in the main World ends, flows started in the physics World do not stop. To tie a physics flow to another lifetime, do one of the following:
- To tie it to a node’s lifetime, start it with
node.RunWhileInTree(task, FlowAutoload.Physics.DefaultClock). The flow runs in the physics World and unwinds when the node leaves the tree (see Node lifetime). - Keep the handle returned by
Physics.Run, and callCancel()in afinallyorFlow.AddCleanupon the main side. - Communicate through
Signal<T>.
async FlowTask Stage(CharacterBody2D body) // on the main World{ var physics = FlowAutoload.Physics.Run(PhysicsLoop(body)); try { await RunStage(); } finally { physics.Cancel(); // the physics flow ends with this one }}Waiting for a single physics frame
Section titled “Waiting for a single physics frame”To wait for a single physics frame, you don’t need a separate World. In a flow on the main World, wait on the SceneTree’s physics_frame signal.
using var physicsFrame = GetTree().ToFlowSignal(SceneTree.SignalName.PhysicsFrame);await physicsFrame.Next(); // resumes at the main World's Tick, after the physics step- The code after it runs not inside the physics step, but at the main World’s Tick that follows.
- For flows that should keep running on every physics frame, use the separate World on this page. A World cannot have a Clock that advances on physics frames, because
Tick(dt)advances every Clock in the World by the same dt. How Worlds and Clocks relate is covered in Time and Clocks.