On C# you make use of IEnumerators and the yield keyword. Unity, for example, makes use of these features for coroutines.
How it works? the yield, which only can be declared on a IEnumerator, statement returns an object, then on the next tick of that IEnumerator reference continues on the yield (not at the start of a function).
As an example, suppose you have a deltaTime global variable, preferably on a static Time class (I am just inventing this class for example's purposes, every GameLoop library must have some sort of delta time that tracks how much time passed last frame), on your GameLoop you would have an IEnumerator variable getting called its MoveNext() method (it both moves to the next iterator and also returns true if it has successfully moved to a next element), when the iterator has no more reasons to yield (on this case, when currentTime reaches approximately zero), the IEnumerator wont't move to a next iterator, finishing the wait.
Just take note, that this is not the same as a threading, since that is parallel programming, this is concurrent, it means that it will run after another instruction, when parallel runs certain operations at the same time.
Example:
public static class Time
{
private static int fps;
private static float deltaTime;
public static int FPS
{
get { return fps; }
set
{
fps = value;
deltaTime = (1f/fps);
}
}
public static float DeltaTime { get { return deltaTime; } }
static Time()
{
FPS = 60;
}
}
public class Game
{
public float waitTime;
IEnumerator waitAndDo;
void GameLoop()
{
while(true)
{
if(waitAndDo == null) waitAndDo = WaitAndDo(waitTime, DoSomething);
if(!waitAndDo.MoveNext()) waitAndDo = null;
}
}
void DoSomething()
{
// Does something...
}
IEnumerator WaitAndDo(float seconds, Action onWaitEnds)
{
float currentTime = seconds;
while(currentTime > 0.0f)
{
/// On 60 frames divided by seconds it will reach 0.
currentTime -= Time.DeltaTime;
yield return null;
}
if(onWaitEnds != null) onWaitEnds();
}
}
This is something rather hard to explain by myself, so I encourage you to check Jamie King's channel on Youtube, he has a pretty good series of IEnumerators and yield statements (see this IEnumerator's video). This is basically an interface used for all data types that a foreach can receive, so it is used most of the times for iterations, but it can be used for the aforementioned coroutine pattern. Of course, always check .NET's API first than any other thing regarding C#.
Hope it helps, any question don't hesitate to ask, since I may have omitted something or left ambiguous terms.