Coroutines and waiting
A coroutine is a method that can pause and continue later, so a sequence that takes seconds or several physics steps reads as one block of code instead of state spread across OnUpdate.
Start a coroutine
Write a method that returns IEnumerator and contains yield return, then pass a call of it to StartCoroutine from any method of your script. It returns a Coroutine handle you can keep to stop it or to check IsRunning.
StartCoroutine runs the method straight away, up to its first yield return. Everything after that runs when the wait is over.
DoorOpener.cs
public sealed class DoorOpener : EntityScript { public float OpenSeconds = 1.5f; private Coroutine? moving; // Call this from a trigger, a button or another script. public void Toggle(bool open) { // Only one movement at a time: stop the running one before starting the next. if (moving is { IsRunning: true }) StopCoroutine(moving); moving = StartCoroutine(Slide(open ? 2.5f : 0.0f)); } private IEnumerator Slide(float targetHeight) { Transform transform = Transform; // a handle: writes go straight to the entity Vector3 start = transform.Position; Vector3 end = new(start.X, targetHeight, start.Z); for (float t = 0.0f; t < OpenSeconds; t += Time.DeltaTime) { transform.Position = Vector3.Lerp(start, end, t / OpenSeconds); yield return null; // continue next frame } transform.Position = end; } }
What you can wait for
yield return | Continues | Use it for |
|---|---|---|
null | The next frame, after this script's OnUpdate. | Animating something frame by frame. |
new WaitForSeconds(seconds) | After that many seconds of game time, checked every frame. | Delays, timers, cooldowns. |
new WaitForFixedUpdate() | In the next fixed step, after this script's OnFixedUpdate. | Changing physics: velocities, forces, character moves. |
new WaitForLateUpdate() | This frame, after every script's OnLateUpdate. | Work that must see the frame's final positions, such as cameras. |
Anything else you yield (a number, a string, a Coroutine) is treated like null: the coroutine simply continues next frame.
Wait a number of seconds
WaitForSeconds counts down with each frame's deltaTime, so it follows game time and is never shorter than one frame. A value of zero or below continues next frame.
Respawner.cs
public sealed class Respawner : EntityScript { public float Delay = 3.0f; public void OnDied() => StartCoroutine(RespawnLater()); private IEnumerator RespawnLater() { Debug.Log($"Respawning in {Delay} s"); yield return new WaitForSeconds(Delay); // game time, counted in frame updates Transform transform = Transform; transform.Position = new Vector3(0.0f, 1.0f, 0.0f); Debug.Log("Respawned"); } }
Work in the fixed step
Physics runs in fixed steps (60 per second by default, see Fixed step or every frame). Changes to rigid bodies and characters belong there. WaitForFixedUpdate resumes once per step, so a loop around it acts for an exact number of steps whatever the frame rate.
LaunchPad.cs
public sealed class LaunchPad : EntityScript { public int PushSteps = 10; public float SpeedPerStep = 1.5f; protected override void OnTriggerEnter(Entity other) => StartCoroutine(Push(other)); private IEnumerator Push(Entity body) { // Physics changes belong in the fixed step: resume once per step, PushSteps times. for (int step = 0; step < PushSteps; ++step) { yield return new WaitForFixedUpdate(); if (!RigidBody.Has(body)) yield break; Vector3 velocity = RigidBody.GetLinearVelocity(body); velocity.Y += SpeedPerStep; RigidBody.SetLinearVelocity(body, velocity); RigidBody.SetAwake(body, true); } } }
Run after everything else in the frame
WaitForLateUpdate resumes after every script's OnLateUpdate in the current frame, which is where camera followers run. Use it when your code must overrule them, for example to snap a camera after a teleport.
SnapCameraOnTeleport.cs
public sealed class SnapCameraOnTeleport : EntityScript { // Called by a portal script that knows the camera entity. public void Teleport(Vector3 destination, Entity camera) => StartCoroutine(TeleportAndSnap(destination, camera)); private IEnumerator TeleportAndSnap(Vector3 destination, Entity camera) { Transform transform = Transform; transform.Position = destination; // Wait until every script has run OnLateUpdate this frame (the camera follower too), // then place the camera so it does not glide across the map. yield return new WaitForLateUpdate(); if (camera.IsValid) { Transform cameraTransform = camera.Transform; cameraTransform.Position = destination + new Vector3(0.0f, 2.0f, -5.0f); } } }
Wait for another coroutine
There is no yield return for a coroutine itself. Keep its handle and wait while IsRunning is true, one frame at a time.
IntroSequence.cs
public sealed class IntroSequence : EntityScript { protected override void OnStart() => StartCoroutine(Play()); private IEnumerator Play() { Coroutine fade = StartCoroutine(Fade(1.0f)); // Wait for another coroutine: check IsRunning every frame. while (fade.IsRunning) yield return null; Debug.Log("Fade finished, starting the level"); yield return new WaitForSeconds(0.5f); Debug.Log("Level started"); } private IEnumerator Fade(float seconds) { for (float t = 0.0f; t < seconds; t += Time.DeltaTime) yield return null; } }
Stop coroutines
StopCoroutine(handle)stops one;StopAllCoroutines()stops every coroutine of this script.- A coroutine also ends when its method returns or reaches
yield break. - Coroutines belong to the script instance: they stop when the entity is destroyed or the session ends.
- A disabled script gets no updates, so its coroutines pause and continue when it is enabled again. If
OnEnablestarts a coroutine, stop the old ones inOnDisable, or both will run.
Patroller.cs
public sealed class Patroller : EntityScript { private Coroutine? patrol; protected override void OnEnable() => patrol = StartCoroutine(Patrol()); // A disabled script gets no updates, so its coroutines pause. OnEnable starts a new patrol, // so stop the old ones here or they would resume next to it. protected override void OnDisable() => StopAllCoroutines(); public void Alert() { if (patrol is { IsRunning: true }) StopCoroutine(patrol); Debug.Log("Alerted: stopped patrolling"); } private IEnumerator Patrol() { while (true) { Debug.Log("Checking the gate"); yield return new WaitForSeconds(5.0f); } } }
Common mistakes
- Reusing a WaitForSeconds. Each instance counts down once. Yielding a used one continues immediately, so create a new one for every wait. See YieldInstruction.
- Calling the method without StartCoroutine.
Cycle();only creates the iterator and runs nothing. WriteStartCoroutine(Cycle());. - Moving physics bodies after
yield return null. That runs in the frame update. UseWaitForFixedUpdatefor physics. - Expecting a coroutine to keep running while its script is disabled. It pauses.
- Custom wait types.
YieldInstructioncannot be subclassed in game code; wait in a loop instead, for examplewhile (!doorOpen) yield return null;.
KroysEngine 0.1.0 documentation, built from the engine repository by Tools/BuildDocsSite.py.