The magic of yield
C# has this ‘yield’ keyword for all sorts of crazy voodoo. Maybe I’m just late to the party, but it seems like this is one of those tricks you learn from really smart people (as I did) and realize that there is so much you don’t know about a language. You might want to start off by reading up some of the MSDN articles and other links found from this conveniently linked Bing search.
Now that you have at least a basic idea of how this works, let’s talk about how we can use this to do fun things. First let’s talk about using it for scripting. A common scenario for scripts are to be able to execute, pause a bit, and continue executing. For instance, your characters says something, pauses, and then says something more. Traditionally you’d use some sort of state management to track this process and handle all the updates. With ‘yield’, we can actually create an interesting array of ways to implement scripting into our game in a way that, visually, looks like exactly how we’d want to write scripts.
First we define a delegate for our scripts:
public delegate IEnumerator<float> Script();
Next we create a ScriptEngine class to handle all the logic of update a script, sleeping, and so forth. I’ll just toss in my script engine (with comments) so you can see how I implemented things:
// handles a set of scripts
public class ScriptEngine : GameComponent
{
// the currently executing scripts
private readonly List<ScriptState> scripts = new List<ScriptState>();
public ScriptEngine(Game game)
: base(game)
{
}
public void ExecuteScript(Script script)
{
// wrap the script in our state
ScriptState scriptState = new ScriptState(script);
// the script may complete in one go
scriptState.Execute(null);
// if not, add it to our list
if (!scriptState.IsComplete)
{
scripts.Add(scriptState);
}
}
public override void Update(GameTime gameTime)
{
// execute all of our scripts
foreach (var scriptState in scripts)
{
scriptState.Execute(gameTime);
}
// remove any completed scripts
scripts.RemoveAll(s => s.IsComplete);
}
// a wrapper over the Script delegate to manage sleeping and the enumerator
private class ScriptState
{
private float sleepLength;
private Script script;
private IEnumerator<float> scriptEnumerator;
// the script is complete when we null out our script
public bool IsComplete { get { return script == null; } }
public ScriptState(Script script)
{
if (script == null)
throw new ArgumentNullException("script");
this.script = script;
}
// executes the script until the next sleep time.
public void Execute(GameTime gameTime)
{
// the first run needs to get the script enumerator and first sleepLength (if any)
if (scriptEnumerator == null)
{
scriptEnumerator = script();
sleepLength = scriptEnumerator.Current;
}
// if we are sleeping, subtract the time from our timer
if (sleepLength > 0 && gameTime != null)
{
sleepLength -= (float)gameTime.ElapsedGameTime.TotalSeconds;
}
// if the sleep timer is done...
if (sleepLength <= 0)
{
bool unfinished = false;
do
{
// MoveNext continues execution of our script until the end or until
// the next yield return. MoveNext returns true if a yield return is
// hit or false if the method is complete.
unfinished = scriptEnumerator.MoveNext();
sleepLength = scriptEnumerator.Current;
// as soon as we are finished or we need to sleep, we exit our loop
} while (sleepLength <= 0 && unfinished);
// if the script is not unfinished (i.e. is complete), we null out our
// script and enumerator which flags the script as IsComplete and lets
// the engine clean it up.
if (!unfinished)
{
script = null;
scriptEnumerator = null;
}
}
}
}
}
You can see that I implemented my engine as a game component to make life a little easier. I also wrap each script into a nice little state object that tracks sleeping and the enumerator.
Now let’s see a little test game:
public class Game1 : Game
{
ScriptEngine engine;
public Game1()
{
new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
Components.Add(engine = new ScriptEngine(this));
}
protected override void LoadContent()
{
engine.ExecuteScript(TestScript);
}
private IEnumerator<float> TestScript()
{
Console.WriteLine("Hello... (wait for it)");
yield return 3f;
Console.WriteLine("World!");
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
base.Draw(gameTime);
}
}
When run you’ll see “Hello… (wait for it)” printed to the debug and then, three seconds later, “World!” is printed out. The yield return does the magic of handling our enumerator for us because the compiler really is building a whole state machine type class over that TestScript method. Then our ScriptEngine and ScriptState classes use an IEnumerator
I know this is a bit complex (especially if you dig in to the details of the compiler plumbing that makes it all work), but it’s still something you can play with. I find it nice because our TestScript method looks, for the most part, like what we’d want to script. Once we have this engine, we can tuck it away and not deal with it, making our life much easier for making scripts in our game. We could, of course, extend the scripting to not just return a float value. You could return any type you wanted to get information back to the engine. Maybe a custom type that has a sleep amount, other scripts to invoke before continuing, or any number of things. It’s really quite powerful.
Cool! I have to research this further. My script does pause events but using another method but this one looks to be better
I have to say I’m quite impressed with this use of yield. It’s a clever use that I’ve never thought of.
This is really slick. I like this alot.
Yes, somehing like this is what the guys from Unity3D use for scripting.
I ever wondered the degree of garbage generation penalty, if any, for re-executing scripts unless you mantain a cache of executed scripts.
Yeah, I didn’t bother to profile for perf or garbage. That sounds like something a talented MVP could tackle.
Yeah, let’s hope a non-lazy one tackles it soon
[quote]scripts.RemoveAll(s => s.IsComplete)[/quote]
I thought LINQ [or is it LAMBDA here?] causes garbage. Are you using it in your applications nevertheless? Can you give me a small statement on that?
re: garbage — yield is used in conjunction with foreach. The compiler generated code will instantiate a new enumerator each time you enumerate … which means garbage will be generated and will cause periodic GCs
Thanks Joel for the enlightenment.
Oh, and @CJ, yes, it would cause garbage because it makes an anonymous delegate. However, that doesn’t mean that you shouldn’t use linq … just be careful not to use it in something that happens each frame. If it happens only periodically that’s fine
@CJ: actually you are watching both of them in action. The “s” statement is a Lamda Expression adn the RemoveAll call is a LINQ Extension Method.
Joel is right; the only way to avoid generating garbage each frame is by caching the delegate by declaring a memeber reference in the class and store the function there to use it each loop.