When WinForms met Game Loop

Originally posted to Shawn Hargreaves Blog on MSDN, Monday, December 6, 2010

WinForms and XNA have quite different ideas about how a program should run.

XNA assumes that games are always in motion, moving and animating. It constantly cycles around the game loop, calling your Update and Draw methods. This is convenient for most games, but wasteful if the program reaches a static state, because XNA will keep calling Draw even though the image has not actually changed.

WinForms assumes that programs tend to just sit there doing nothing, and only wake up when the user interacts with them by pressing a key, clicking the mouse, etc. It uses an event based programming model, where input and paint requests are delivered to the program via the Win32 message queue. If no messages arrive, the program runs no code at all and thus consumes no CPU.

When you combine XNA with a WinForms app, such as in our WinForms Graphics Device sample, you must decide whether you want to follow the XNA model, or the WinForms model, or something in between. Are you a game that happens to include some WinForms UI controls, or a WinForms app that happens to include some 3D XNA graphics rendering?

Our sample actually demonstrates two such options. Its SpriteFontControl does not animate, so uses the standard WinForms programming model where Draw is only called in response to Win32 paint messages. SpinningTriangleControl, on the other hand, is animated using the second technique described below.

 

I'm a polite WinForms app, so please tick me whenever you have a spare moment

The WinForms way to arrange for an animated control to be redrawn at regular intervals is to use a Timer object. Add this field to your control:

    Timer timer;

Set it going in your Initialize method:

    timer = new Timer();
    timer.Interval = (int)TargetElapsedTime.TotalMilliseconds;
    timer.Tick += Tick;
    timer.Start();

Pros:

Cons:

 

I'm lazy and want to write the least code possible :-)

Our WinForms Graphics Device sample animates its SpinningTriangleControl by hooking the WinForms Application.Idle event:

    Application.Idle += Tick;

This event is raised after processing Win32 messages, whenever the queue becomes empty.  Using it for animation is a bit of hack, and only works if the Tick method calls Invalidate:

If the Tick method did not call Invalidate, there would only be a single Idle event rather than a steady stream of them.

Pros:

Cons:

 

I'm a game, dammit!  I want to run flat out, tick tock tick tock, hogging all the CPU

The built-in Microsoft.Xna.Framework.Game class customizes how Win32 messages are processed, taking over the message loop so the game can run as smoothly as possible in the gaps between message processing. You can do the same thing if you want this same behavior. First, hook the Application.Idle event:

    Application.Idle += TickWhileIdle;

Whenever the Win32 message pump becomes idle, we go into an infinite loop, calling Tick as fast as possible for maximum game performance and smoothness. But we must check to see if a Win32 message has arrived, so we can break out of the loop if there is UI processing to be done:

    void TickWhileIdle(object sender, EventArgs e)
    {
        NativeMethods.Message message;

        while (!NativeMethods.PeekMessage(out message, IntPtr.Zero, 0, 0, 0))
        {
            Tick(sender, e);
        }
    }

WinForms does not expose the native Win32 PeekMessage function, so we must import this ourselves:

    static class NativeMethods
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct Message
        {
            public IntPtr hWnd;
            public uint Msg;
            public IntPtr wParam;
            public IntPtr lParam;
            public uint Time;
            public System.Drawing.Point Point;
        }

        [DllImport("User32.dll"]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool PeekMessage(out Message message, IntPtr hWnd, uint filterMin, uint filterMax, uint flags);
    }

Pros:

Cons:

 

Invalidate vs. Draw

Any of the above techniques will provide repeated calls to our Tick method. But to display animation, Tick must somehow cause the control to redraw itself.

The easiest way is for Tick to call Invalidate.  This tells Win32 "hey, whenever you get a moment, could you please send a WM_PAINT message to this control?"  That triggers the GraphicsDeviceControl.OnPaint method, which calls BeginDraw to set up the device, then calls the main Draw method, and finally calls EndDraw to present the image onto the screen.

For maximum smoothness you may wish to avoid Win32 message processing entirely, and have Tick directly call BeginDraw, Draw, and EndDraw (the same way the existing OnPaint method does) instead of Invalidate.

 

Fix that timestep, fix it good

So we've chosen one of the above techniques, and hooked it up to call our Tick method.  But our animation is not smooth, because Tick is not called at a steady rate!  The third technique will usually call Tick more steadily than the first, but none of them can guarantee how often this will occur.  Whenever Tick is called we must examine the clock to see how much actual time has passed, adjusting our update logic and animation playback accordingly.

First, we must decide whether we want fixed or variable timestep update logic.

Variable timestep is easy. Just use a Stopwatch to see how much time has passed, perform the appropriate update calculations, then redraw the control.

Fixed timestep logic is implemented like so:

    Stopwatch stopWatch = Stopwatch.StartNew();

    readonly TimeSpan TargetElapsedTime = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / 60);
    readonly TimeSpan MaxElapsedTime = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / 10);

    TimeSpan accumulatedTime;
    TimeSpan lastTime;


    void Tick(object sender, EventArgs e)
    {
        TimeSpan currentTime = stopWatch.Elapsed;
        TimeSpan elapsedTime = currentTime - lastTime;
        lastTime = currentTime;

        if (elapsedTime > MaxElapsedTime)
        {
            elapsedTime = MaxElapsedTime;
        }

        accumulatedTime += elapsedTime;

        bool updated = false;

        while (accumulatedTime >= TargetElapsedTime)
        {
            Update();

            accumulatedTime -= TargetElapsedTime;
            updated = true;
        }

        if (updated)
        {
            Invalidate();
        }
    }

The MaxElapsedTime check avoids running excessively many Updates to catch up after the program has been paused, for instance when debugging.

If you are using the second of the previously mentioned ticking techniques, remove the "if (updated)" check from around the Invalidate call.

Blog index   -   Back to my homepage