Monday, January 23, 2012

Replacing XNA's Game Class: XnaControl

This article is the first in a series of articles about replacing XNA's Game class. Note that the code used in these articles was designed to meet the needs of Text Adventure, so expect to change it for your own projects. Consider the code in these articles to be a template.

Shortly after I started work on Text Adventure's engine UI, I quickly determined that I didn't much like XNA's Game class. Here are some of the Game class' major responsibilities:
  • Run() acts as both the message pump and the game loop for the application
  • The class manages a GraphicsDevice instance
  • The class manages the window to which it renders the game
For me, all these responsibilities in one class became problematic when I determined I wanted to run my game in a Form of my own design, rather than the simple window provided by the Game class. My first challenge was to create a Control that would manage the GraphicsDevice instance independent of the Game class. I wanted this Control to be reusable across games or within a single game with very little effort. The resulting class is called XnaControl:
public class XnaControl : Control
{
    public XnaControl()
    {
        SetStyle(ControlStyles.DoubleBuffer, true);
        SetStyle(ControlStyles.ResizeRedraw, false);
        SetStyle(ControlStyles.UserPaint, true);
        TabStop = false;
    }

    [Browsable(false)]
    public GraphicsDevice GraphicsDevice
    {
        get;
        private set;
    }

    protected override Size DefaultSize
    {
        get
        {
            return new Size(100, 100);
        }
    }

    private PresentationParameters GetPresentationParameters()
    {
        return new PresentationParameters
                   {
                       BackBufferFormat = SurfaceFormat.Color,
                       BackBufferWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width,
                       BackBufferHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height,
                       DepthStencilFormat = DepthFormat.Depth24Stencil8,
                       DeviceWindowHandle = Handle,
                       IsFullScreen = false,
                       RenderTargetUsage = RenderTargetUsage.DiscardContents,
                       PresentationInterval = PresentInterval.Immediate
                   };
    }

    private void DisposeGraphicsDevice()
    {
        if (GraphicsDevice != null)
        {
            GraphicsDevice.Dispose();
            GraphicsDevice = null;
        }
    }

    protected override void OnHandleCreated(EventArgs e)
    {
        if (!DesignMode)
        {
            GraphicsDevice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, GraphicsProfile.HiDef, GetPresentationParameters());
        }

        base.OnHandleCreated(e);
    }

    protected override void OnHandleDestroyed(EventArgs e)
    {
        DisposeGraphicsDevice();

        base.OnHandleDestroyed(e);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            DisposeGraphicsDevice();
        }

        base.Dispose(disposing);
    }
}
The constructor sets a couple of control styles that remove Windows' responsibility to paint the control, eliminating any flicker when the control is resized. The GraphicsDevice instance is conveniently tied to the lifetime of the control's handle. GetPresentationParameters uses the screen's current resolution for the back-buffer, which allows me to avoid resetting the GraphicsDevice instance when the control is resized; instead, I simply present a source rectangle that is the size of the control. In one of my derived classes, I draw the Text Adventure logo if a game is not loaded:
public class TextAdventureXnaControl : XnaControl
{
    public TextAdventureXnaControl()
    {
        DrawBackground = true;
    }

    public bool DrawBackground
    {
        get;
        set;
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        if (DrawBackground)
        {
            e.Graphics.FillRectangle(Brushes.Black, ClientRectangle);

            var logoRectangle = new Rectangle(
                new Point((ClientSize.Width / 2) - (Resources.Game_Thumbnail.Width / 2), (ClientSize.Height / 2) - (Resources.Game_Thumbnail.Height / 2)),
                Resources.Game_Thumbnail.Size);

            e.Graphics.DrawImage(Resources.Game_Thumbnail, logoRectangle);
        }

        base.OnPaint(e);
    }
}
This control can be dropped on any form and its GraphicsDevice property can be supplied to an XnaGame instance, as we'll see in my next post.

No comments:

Post a Comment