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!