Pumping the Guide
I've never been happy with the design of the XNA Framework Guide methods.
I want to write code like this:
int? button = Guide.ShowMessageBox("Save Game",
"Do you want to save your progress?",
new string[] { "OK", "Cancel" },
0, MessageBoxIcon.None);
if (button == 0)
{
StorageDevice storageDevice = Guide.ShowStorageDeviceSelector();
if (storageDevice != null)
{
using (StorageContainer storageContainer = storageDevice.OpenContainer("foo"))
{
...
}
}
}
But there are no such simple ShowMessageBox or ShowStorageDeviceSelector methods. Even if these methods did exist, the above code would not work. Instead, I have to deal with a tangled mess of Guide.BeginShow*, IAsyncResult, and make a state machine to track when I should call Guide.EndShow*.
Why so complicated?
Back in Game Studio 1.0, we did have just such a simple storage device selector API. It worked fine as long as you called it from a background thread, but if anyone was so foolish as to call it from their main game thread, boom! The Xbox hangs.
People were justifiably surprised by this behavior. The problem is that the Guide UI is displayed over the top of the game, and relies on the game loop calling Present at regular intervals. When the game is blocked inside a ShowStorageDeviceSelector call, it is no longer cycling through the game loop, thus not calling Present, thus the Guide never gets a chance to render itself, thus the user has no way to interact with it, so the Guide call never completes.
It struck us as a bad idea for such a seemingly simple API to behave so rudely, so we spent some time trying to improve it for Game Studio 2.0. The main idea we considered was to make these blocking methods automatically call Present while the Guide was visible. Unfortunately, there are many problems with such a design:
- Xbox Games are still visible underneath the Guide, partially faded out. If we called Present directly, outside of the normal game loop, this would not be possible, so XNA Framework games would just show black behind the Guide. Ugly.
- What if we called Game.Draw before each Present, rather than just clearing to black?
- This only works if Draw is truly independent of Update, and safe to call even while Update is suspended. Sure, a well written game ought to work this way, but can we really assume all games are robust enough?
- What if the game calls ShowStorageDeviceSelector from inside their Draw method, so we are now calling Draw recursively? No sensible game would do this, but we have to worry about the not-so-sensible ones too :-)
- To call Draw, the Guide APIs would need access to the Game instance. But Game is defined in the Microsoft.Xna.Framework.Game assembly, which is intended to be optional. We don't want to force anyone to use Game if they prefer to host the framework some other way.
- To keep the game visible without calling Draw, what if we took a screen grab before activating the Guide, and used that as the background?
- The game would no longer animate behind the Guide, but who cares.
- We'd need extra memory to store the image. Do we really want to take that space away from every game? We can't just allocate it on demand, because we mustn't get in a situation where the Guide can't come up because the game is using too much memory.
- What if someone calls ShowStorageDeviceSelector from a background thread, in which case the game loop is still running in parallel? Extreme badness would ensure if we tried to call Present at the same time the main thread was drawing something.
- Ok, so automatically pumping Present is a can of worms. How about if we made this explicit, and had the game pass a delegate into the blocking Guide.Show APIs? We would call this at periodic intervals, so it could do whatever drawing was appropriate.
- Dang, there goes our nice simple API...
- This is error prone, and only works if the developer understands enough to specify exactly the right delegate.
- In fact, what should they specify here? They can't just use Game.Draw, as that would skip the preamble and postamble code which handles lost devices and does the final Present.
- Oh yeah, lost devices. What should happen if the user locks the desktop while the Guide is up? What if they resize the game window, or drag it to a second monitor, or close it? These actions trigger many crazy events, and there are many things that could go wrong if the game is not expecting them.
When in doubt, play it safe and at least try to do no harm.
Game Studio 2.0 only provides async Begin/End versions of the message box, keyboard input, and storage device Guide calls. These are a pain to use, but at least they explicitly force developers to deal with the resulting state machine, rather than being surprised when crazy stuff happens and unexpected events are raised in the middle of a Guide call. No magic is better than confusing magic that only works half the time, right?
I'm still not happy about this. I keep revisiting it, looking at it from different angles, and concluding that it's still a mess. I don't like what we have now, but I also don't like any of the alternatives!