XNA stops calling Update
and Draw
while the game window is being resized or moved.
Why? Is there a way to prevent this behaviour?
(It causes my network code to desynchronise, because network messages aren't being pumped.)
Answer
Yes. It involves a small amount of messing with XNA's internals. I've got a demonstration of a fix in the second half of this video.
Background:
The reason this happens is because XNA suspends its game clock when Game
's underlying Form
is resized (or moved, the events are the same). This, in turn, is because it is driving its game loop from Application.Idle
. When a window is resized, Windows does some crazy win32 message loop stuff that prevents Idle
from firing.
So, if Game
didn't suspend its clock, after a resize it will have accumulated an enormous amount of time that it would then have to update through in a single burst - clearly not desirable.
How to fix it:
There is a long chain of events in XNA (if you use Game
, this doesn't apply to custom windows) that basically connects the resize event of the underlying Form
, to Suspend
and Resume
methods for the game clock.
These are private, so you'll need to use reflection to unhook the events (where this
is a Game
):
this.host.Suspend = null;
this.host.Resume = null;
Here is the necessary incantation:
object host = typeof(Game).GetField("host", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this);
host.GetType().BaseType.GetField("Suspend", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(host, null);
host.GetType().BaseType.GetField("Resume", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(host, null);
(You could disconnect the chain elsewhere, you can use ILSpy to figure it out. But there's nothing else besides the window resize that suspends the timer - so these events are as good as any.)
Then you need to provide your own tick source, for when XNA isn't ticking from Application.Idle
. I simply used a System.Windows.Forms.Timer
:
timer = new Timer();
timer.Interval = (int)(this.TargetElapsedTime.TotalMilliseconds);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
Now, timer_Tick
will eventually call Game.Tick
. Now that the clock isn't suspended, we're free to just keep using XNA's own timing code. Easy! Not quite: we only want our manual ticking to happen when XNA's built-in ticking doesn't happen. Here is some simple code to do that:
bool manualTick;
int manualTickCount = 0;
void timer_Tick(object sender, EventArgs e)
{
if(manualTickCount > 2)
{
manualTick = true;
this.Tick();
manualTick = false;
}
manualTickCount++;
}
And then, in Update
, put this code to reset the counter if XNA is ticking normally.
if(!manualTick)
manualTickCount = 0;
Note that this ticking is considerably less accurate that XNA's. However it should remain more-or-less lined up with real time.
There is still one small flaw in this code. Win32, bless its stupid ugly face, stops essentially all messages for a few hundred milliseconds when you click the title-bar of a window, before the mouse actually moves (see that same link again). This prevents our Timer
(which is message-based) from ticking.
Because we now don't suspend XNA's game clock, when our timer eventually starts ticking again, you'll get a burst of updates. And, sadly, XNA caps the amount of accumulatable time to an amount slightly lower than the length of time Windows stops messages when you click the title bar. So if a user happens to click and hold your title-bar, without moving it, you'll get a burst of updates and fall a few frames short of real time.
(But this should still be good enough for most cases. XNA's timer already sacrifices accuracy to prevent stuttering, so if you need perfect timing, you should do something else.)
No comments:
Post a Comment