2015/08/18

Handling Multiple Resolutions with a Virtual Screen

My current retro 16 bit style guidelines dictate that my game use a virtual screen of 320x180 pixels. At first, that seems easy enough: when the game is running in window mode, we can just dictate the size to be a multiple of that and zoom in accordingly. In full screen 720 or 1080p, we can just scale by a factor of 4 or 6, respectively. Common laptop resolutions, like 1366x768, are not so easy, but we can give the option to either scale unevenly, by a factor of roughly 4.26 in this case, or scale evenly by a factor of 4, and have a black border around the virtual screen. On displays with different aspect ratios, we generally face the same problem, except even the unevenly scaled version would require letterboxing.

To handle all of these different cases, as well as the very concept of a virtual display, I created the class VirtualScreen. It uses a RenderTarget2D that is a multiple of 320x180 in size, and provides a CaptureFrame method that sets the render target on the GPU, and a DrawFrame method that renders the target on the screen.
The hairy bit is the Refresh method, which sets the size of the render target and calculates a destination rectangle for DrawFrame. There's also the Reset and Save methods, which load the properties of the class from the configuration and calls Refresh, and write the current state of the screen back into the configuratin and save it, respectively.

This is the bad boy in question:
It looks pretty hairy at first glance, and it is a bit heavy on the arithmetic side, but the real problem is, that it just explodes in size as we add rendering options, and there just is no design pattern or algorithm to save us here. We could factor out a RefreshFullScreen and a RefreshWindowed function, but that would only take away a couple lines of code and a level of indentation; it wouldn't help with the why does-this-math-work-question.
The real problem is, that this thing just explodes in complexity as you add video options - I added a SuperSampling property that doubles the size of the render target while keeping the destination rectangle the same, slightly blurring the pixel art but also smoothing out sprite movement (once we get to that, anyway).

But setting up the back buffer and render target isn't the only thing I wrote this class for.
The Scale property the Refresh method sets before calculating a destination is not configurable. Instead, it's supposed to be used to create transformation matrices for rendering on the virtual display. I now use it in my Camera class in place of its own scale member, so the camera automatically adjusts to changes to the rendering options.
Since we have a texture with the rendered world and UI by the time DrawFrame gets called, we can also do post-processing there, which I'll at least touch on in the next post.
I will also render a Super Game Boy inspired background image behind the render target, to make potential letterboxing less awful to look at. The original The Binding of Isaac did that to great effect, I find.

No comments:

Post a Comment