This blog is no longer updated. Feel free to copy any useful information to other blogs or wikis. Thanks.


The magic of yield

February 18th, 2010 | 11 comments

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 to track the enumerator. To get the enumerator, we call the delegate like normal. From there, we simply call MoveNext on the IEnumerator which will start execution at the last yield return statement.

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.

  1. February 19th, 2010 at 01:52
    Quote | #1

    Cool! I have to research this further. My script does pause events but using another method but this one looks to be better

  2. February 19th, 2010 at 05:09
    Quote | #2

    I have to say I’m quite impressed with this use of yield. It’s a clever use that I’ve never thought of.

  3. February 19th, 2010 at 07:25
    Quote | #3

    This is really slick. I like this alot.

  4. Pete
    February 19th, 2010 at 10:48
    Quote | #4

    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.

  5. February 19th, 2010 at 11:02
    Quote | #5

    Yeah, I didn’t bother to profile for perf or garbage. That sounds like something a talented MVP could tackle. ;-)

  6. February 19th, 2010 at 11:31
    Quote | #6

    Yeah, let’s hope a non-lazy one tackles it soon ;)

  7. CJ
    February 19th, 2010 at 13:18
    Quote | #7

    [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?

  8. February 19th, 2010 at 18:06
    Quote | #8

    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 :-)

    • February 19th, 2010 at 19:13
      Quote | #9

      Thanks Joel for the enlightenment.

  9. February 19th, 2010 at 18:08

    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

  10. February 19th, 2010 at 19:29

    @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.

Comments are closed.