2015/08/16

Matrix Transformations in 2D Space - The Camera

While I was factoring out the useful bits of the map rendering proto-code, I tested them a lot more thoroughly and a fixed a couple of bugs, mostly just missing calls into classes I wanted to plug into the engine. One bug in particular, however, was related to the way we currently rotate tiles;
When we set the origin in our draw call to (8 8), we not only rotate tiles around their center, as I'd assumed, but also shift them to the top left by the same amount. To fix that, I removed the origin parameter and used some evil bit magic again:
This is not today's topic though, so explaining how that works is left as an exercise for the reader.

Doing Math on Camera

With the refactoring done, we could go straight to implementing map objects, but I decided to do some polishing first. We can kill quite a few birds with one stone here, and that stone is a 2D camera.
We usually talk about cameras in 3D games, because their control is usually fundamental to the gameplay. But it's also common to have a camera in a 2D game, because it's very convenient for the programmer: If you can just move the camera and the environment magically scrolls on the screen, there are so many fewer headaches involved in just figuring out where to render a sprite.
The easiest way to implement such magic is with a transformation matrix that you pass to the GPU, which will use that to automatically figure out where sprites should be rendered, and at what size.
XNA/MonoGame also provide us with a Matrix structure, so we don't even have to worry about the math behind the magic too much. We just call a couple static functions, multiply the results together, and pass the product to SpriteBatch.Begin.

However, in case you actually are unfamiliar with the math, I highly recommend you read up on it. There's tons of more focused resources on linear algebra than this blog, and you need it constantly when you're doing any sort of graphics or physics work. In any case, here's a brief rundown of the essentials:
A matrix is a NxM field of real numbers. The (x y) vectors we've been working with are essentialy 1x2 matrices. Other than vectors, the most common kind of matrix in graphics programming is the 4x4 matrix, which is also what a Matrix in XNA is.
You can use arithmetic operators on matricess; the two most important operations for us are multiplying 1x4 matrices - Vector4 - with 4x4 matrices to get another Vector4, and multiplying one 4x4 Matrix with another to get a third 4x4 Matrix.
The latter lets us combine multiple transformation matrices into one, so we can perform multiple transformations of a vector with just one matrix multiplication.
The former works by calculating four linear combinations of the elements in the original vector, with the columns of the matrix serving as the coefficients for each elemnt in the new vector. Since we mostly work with 2- or 3-component vectors, the remaining elements are usually padded with 1.0, giving us the power to do translations, i.e. shifting the vectors around by a constant offset. We can also scale a vector by setting the factors on the main diagonal.

Our camera doesn't even need to do any more than that:
First, we want to zoom in by a factor of 16. This lets us render tiles into destination rectangles of size 1, which should remove a lot of potential *16 and /16 from our code, such as in the bug fix above.
Then, as the camera's position goes to the bottom right, we want to move the scene to the top left. This lets us render the map with the origin at (0 0), no matter which part of the level should currently be focused. We could also easily add a screenshake offset, without anything having to change in our rendering code.
Finally, we want to zoom in some more so our virtual 320x180 screen doesn't look so small on a 1080p display. Having this configurable easily will help us once we actually code the virtual screen to deal with multiple resolutions.

I use a full class TopDownCamera for this, with properties and dirty flags, but at its heart is just this one line of code:
Passing that to SpriteBatch.Begin does all the rest. However, there's one other thing we can do, now that we have the camera working:

Polishing Tiles

At this point, I added a Vector4 Viewport property to my camera class that would give me the left, top, right and bottom limits of the visible area of the world. Using that, we can calculate the upper and lower limits of the x and y loops in our DrawLayer method. This has two major advantages:
We only consider tiles that are actually on screen - we can remove map size from the performance equation.
We also get to consider values that are not on the map. This may sound stupid at first, but whenever the camera gets close to the edge of the screen, you see Cornflowe Blue, or whatever other color you clear your screen with. If the map data contained a set of border blocks we could render there instead, we can easily detect when to render border blocks, depending on whether the x and y indices are within the bounds of the blocks array.

Depending on how much attention you want to give this, determining which tile to render can actually be more difficult. If you just define a single border block, you can just go
I, however, wanted the border blocks to be almost full featured maps of themselves. They share the same layer settings and obviously won't get any collision data, but I defined a ushort[,,] blocks, which should be repeatedly rendered around the map.
Finding the proper index into this array is a bit more complicated. You could just modulate the coordinates by the length of the array in the respective dimension, but then you'd still get negative values. Negating these won't work, though, because that would inverse the block pattern upwards and to the left of the map. However, you could add the length of the array and modulate again, which gives the correct pattern, like this:

Finally, here is the code in action:
The ugliest island map in the world!
The water is all border blocks, by the way.

No comments:

Post a Comment