Sunday, February 12, 2017

c# - How do I prevent XNA from pausing Updates while the window is being resized/moved?


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

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...