Resolution independence means that elements on a computer
screen are rendered at sizes independent of the pixel grid, resulting in a graphical user interface that is displayed at
a consistent size, regardless of the resolution of the screen. This idea was pioneered by the
great Donald Knuth, as early as 1978, when his typesetting
system TeX introduced resolution independence into the world of computers.
The goal behind resolution independence is to not care too much about the screen resolution during game development,
to divert more energy into creating the actual gameplay. The idea is elementary: a virtual resolution is fixed, and the
entire game is developed with that virtual resolution in mind. Later on, when deployed, the game stretches, or shrinks,
all graphical elements depending on the player’s actual screen resolution.
Basically all that needs to be done is to decide for which resolution the game should be developed for. In these
tutorials, I opted for a resolution of pixels. To finally render the game at different resolutions, it is
enough to stretch or shrink the game graphics and user interface. The best place to do so is when creating the viewport.
For example, if the real screen resolution is bigger then the virtual resolution, the viewport will be stretched, that
is the entire game will be stretched and rendered on a bigger viewport. For a smaller screen, the opposite happens: the
viewport is shrunk, and the game graphics will shrink to fit into this smaller viewport. To avoid ugly artifacts, it is
a good idea to not change the aspect ratio defined by the virtual resolution in both cases.
Let be the width and the height of the current screen resolution and and the virtual width and
height respectively. The first thing to do is thus to calculate the desired (virtual) aspect ratio
Knowing the aspect ratio of the virtual resolution, it is now possible to compute the largest area of pixels (in the
actual resolution) that fits into the desired aspect ratio.
Let and be the width, respectively the height, of the viewport to be created, then, knowing the width of the
screen resolution, to keep the aspect ratio of the virtual resolution, the height of the viewport must be , as we know the desired ratio between height and width. In C++-code, this looks as follows:
If , if the computed height of the viewport is larger than the height of the screen resolution, then the game
doesn’t fit into the current resolution, and thus a technique called
pillarbox must be used to place black bars on the sides of the viewport. To
do so, the height of the viewport is set to the height of the screen resolution and the width of the viewport is then
recomputed with respect to the desired virtual aspect ratio: , or in C++:
Knowing the height and width of the viewport, it is now easy to place it in the middle of the backbuffer, via a
translation:
Here is all of the code in one place:
Matrix Transformation
The actual transformation of the game graphics is done using elementary linear algebra, as seen in a previous tutorial
on transformations
Lost in Translation
The first thing to do is to create a translation matrix, that is, a matrix defining the translation of each game object
by the vector
Scaling
To scale the graphics, it is sufficient to compute the ratio between the current and the virtual width, as well as
between the current and the virtual height — and to then define the corresponding matrix to scale each game object
accordingly, regarding the centre of the screen:
Combining Transformations
It is a well-known fact from linear algebra that the concatenation of two linear maps is equivalent to the
multiplication of their associated matrices:
Now behold the entire function that grants us resolution independence:
Note that the transformation matrix as well as its inverse (which can be computed using the Invert function
provided by Direct2D) are stored. The inverse of such a transformation matrix decodes a translation in the other
direction (by the same length) with the scaling factor inverted.
All that is left to do now is to set the transformation before drawing:
Transforming the Mouse
As you can see, the transformation is reset before the mouse cursor is drawn, as the translation leads to clipping
errors (it seems as if the mouse is transformed internally anyway?). To handle mouse input, the stored information about
the transformation matrix, or rather, its inverse, is used to compute the virtual coordinates of the mouse pointer. Let
be the current position of the mouse and the transformation matrix as explained above, then the
virtual position, , of the mouse can be computed as follows:
To multiple a matrix by a vector, Direct2D offers
the TransformPoint
method. In C++ the above computation thus looks as follows:
To get the virtual position of the mouse cursor, it is now sufficient to call the getTransformedMousePosition method:
To see this technique of resolution independent rendering in action, have a look at the latest beta version of
Stécker vum Himmel, a Tetris clone powered by the bell0bytes engine
developed in these tutorials, and play around with the screen resolution.