Think about how a foreach
works:
foreach (var number in Enumerable.Range(1, 1000000))
{
if (number > 10) break;
}
The control over the iteration is on the caller - if you stop the iteration (here with break
), that's it.
The yield
keyword is a simple way to make an enumerable in C#. The name hints at this - yield return
yields control back to the caller (in this case, our foreach
); it's the caller who decides when to continue to the next item. So you can make a method like this:
IEnumerable<int> ToInfinity()
{
var i = 0;
while (true) yield return i++;
}
This naively looks like it will run forever; but in reality, it depends entirely on the caller. You can do something like this:
var range = ToInfinity().Take(10).ToArray();
This can be a bit confusing if you're not used to this concept, but I hope it's also obvious that this is a very useful property. It was the simplest way that you could yield control to your caller, and when the caller decides to follow up, it can just do the next step (if Unity was made today, it would probably use await
instead of yield
; but await
didn't exist back then).
All you need to implement your own coroutines (needless to say, the simplest stupidest coroutines) is this:
List<IEnumerable> continuations = new List<IEnumerable>();
void StartCoroutine(IEnumerable coroutine) => continuations.Add(coroutine);
void MainLoop()
{
while (GameIsRunning)
{
foreach (var continuation in continuations.ToArray())
{
if (!continuation.MoveNext()) continuations.Remove(continuation);
}
foreach (var gameObject in updateableGameObjects)
{
gameObject.Update();
}
}
}
To add a very simple WaitForSeconds
implementation, you just need something like this:
interface IDelayedCoroutine
{
bool ShouldMove();
}
class Waiter: IDelayedCoroutine
{
private readonly TimeSpan time;
private readonly DateTime start;
public Waiter(TimeSpan time)
{
this.start = DateTime.Now;
this.time = time;
}
public bool ShouldMove() => start + time > DateTime.Now;
}
And the corresponding code in our main loop:
foreach (var continuation in continuations.ToArray())
{
if (continuation.Current is IDelayedCoroutine dc)
{
if (!dc.ShouldMove()) continue;
}
if (!continuation.MoveNext()) continuations.Remove(continuation);
}
Ta-da - that's all a simple coroutine system needs. And by yielding control to the caller, the caller can decide on any number of things; they might have a sorted event table rather than iterating through all the coroutines on every frame; they might have priorities, or dependencies. It allows for very simple implementation of cooperative multi-tasking. And just look at how simple this is, thanks to yield
:)
"Fire1"
, is that something you can set up in the engine to allow for key remappings rather than typing outKeycode.Foo
? \$\endgroup\$yield
is effectively short for "Yield control to the caller until the next item in the Enumerable is requested." \$\endgroup\$StopAllCoroutines()
in this case. It's fine when you're only ever using one coroutine, but if you ever planned to have more than one, this would have undesired effects. Instead you should useStopCoroutine()
and just stop the one that's relevant instead of all of them. (StopAllCoroutines()
would be useful e.g. when ending the level or loading a new area, etc., but not for specific stuff like "I'm not shooting anymore".) \$\endgroup\$