Skip to content

Instantly share code, notes, and snippets.

@lucasteles
Last active August 28, 2025 17:50
Show Gist options
  • Save lucasteles/21f30908c1396bc799ec6a5570f5fd16 to your computer and use it in GitHub Desktop.
Save lucasteles/21f30908c1396bc799ec6a5570f5fd16 to your computer and use it in GitHub Desktop.
Godot Coroutine (unity like)
global using Coroutine = System.Collections.Generic.IEnumerable<Coroutines.Wait>;
global using Wait = Coroutines.Wait;
using System;
// ReSharper disable AsyncVoidMethod
public static class Coroutines
{
public static async void Start(this Coroutine steps)
{
var mainLoopTree = Engine.GetMainLoop();
foreach (var step in steps)
{
var frames = step.FrameCount;
if (frames > 0)
{
for (var i = 0; i < frames; i++)
await NextFrame();
continue;
}
if (step is { Header: WaitType.Predicate, Predicate: { } pred })
while (!pred.Invoke())
await NextFrame();
}
return;
SignalAwaiter NextFrame() => mainLoopTree.ToSignal(mainLoopTree, SceneTree.SignalName.ProcessFrame);
}
public enum WaitType : byte
{
Frame,
FrameCount,
Seconds,
Predicate,
}
public readonly struct Wait
{
public readonly WaitType Header;
public readonly float Amount;
public readonly Func<bool> Predicate;
public Wait() : this(WaitType.Frame) { }
Wait(WaitType header) => Header = header;
Wait(int frames) : this(WaitType.FrameCount) => Amount = frames;
Wait(float seconds) : this(WaitType.Seconds) => Amount = seconds;
Wait(Func<bool> predicate) : this(WaitType.Predicate) => Predicate = predicate;
public int FrameCount =>
Header switch
{
WaitType.Frame => 1,
WaitType.FrameCount => (int)Amount,
WaitType.Seconds => (int)Mathf.Round(Amount * Engine.GetFramesPerSecond()),
WaitType.Predicate => -1,
_ => throw new ArgumentOutOfRangeException(),
};
public static readonly Wait Frame = new();
public static Wait Frames(int count) => new(count);
public static Wait Seconds(float amount) => new(amount);
public static Wait Until(Func<bool> pred) => new(pred);
public static implicit operator Wait(int count) => Frames(count);
}
}
public partial class TestLabel : Node
{
[Export]Label label;
bool pressedRight;
// Coroutine Test
public override void _Input(InputEvent @event)
{
if (!GetWindow().HasFocus()) return;
if (@event is InputEventKey { Pressed: true, Keycode: Key.Escape })
GetTree().Quit();
pressedRight = @event.IsActionPressed("ui_right");
if (Input.IsActionJustPressed("ui_accept"))
{
var option = GD.RandRange(1, 4);
switch (option)
{
case 1:
Fade().Start();
break;
case 2:
FadeDelayFrames().Start();
break;
case 3:
FadeDelaySecond().Start();
break;
case 4:
FadePredicate().Start();
break;
}
}
}
Coroutine Fade()
{
Color c = label.Modulate;
for (float alpha = 1f; alpha >= 0f; alpha -= 0.1f)
{
c.A = alpha;
label.Modulate = c;
yield return default;
}
}
Coroutine FadeDelayFrames()
{
Color c = label.Modulate;
for (float alpha = 1f; alpha >= 0f; alpha -= 0.1f)
{
c.A = alpha;
label.Modulate = c;
yield return 2;
}
}
Coroutine FadeDelaySecond()
{
Color c = label.Modulate;
for (float alpha = 1f; alpha >= 0f; alpha -= 0.1f)
{
c.A = alpha;
label.Modulate = c;
yield return Wait.Seconds(0.1f);
}
}
Coroutine FadePredicate()
{
Color c = label.Modulate;
label.Modulate = c with { A = 1f };
yield return Wait.Until(() => pressedRight);
label.Modulate = c with { A = 0.1f };
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment