Technical explanation of one rooms

As promised, although heavily delayed, I am writing a technical explanation of the way the rooms are rendered in one rooms. Featuring basic linear algebra, old school pixel-by-pixel rendering, and depth buffers. Sounds fun already!

But first, some updates. There has been a lot of work outside of gamedev for me, so with the time I actually put into working on personal projects, I rewrote this blog from a really ugly PHP codebase into a somewhat organised Haxe project … which is then transpiled into PHP again, but at least I don't have to work with that! There are very few visible changes apart from the comment system (not that it was ever used for anything but spam, but I can always hope). Then I wrote a simple editor for myself to write these blog entries in. Reinventing the wheel, I know, but it is a satisfying one-day project. Now I am writing this article as promised, then (hopefully before the next LD) I want to get back to one rooms and release a more complete version. I thought a little bit about releasing it on Greenlight, but for that it would have to be polished a lot more, mostly so that I would be happy with it. So maybe Kongregate? We'll see.

Okay, so let's dive right into the good stuff. Rooms in the game are represented as three arrays:

Rendering sprites

Sprites are probably the easiest to understand. Rendering them is basically just copying them from the spritesheet onto the screen. The owner of the sprite (e.g. the Knig object 'owns' the Knig sprite) chooses which part of the spritesheet to render at the given time according to the current room rotation, the walk cycle, et cetera.

You might notice that sprites can be hidden behind other sprites (the skeletons in a row), behind walls or floors. Sorting sprites according to their y position / distance from the camera would solve the first problem, but walls and floors don't have a single distance from the camera. So more on that a bit later!

Rendering walls

Walls were more or less the first part of the game that I wrote. From the beginning I knew I was going for that kind of faux-3D pixel art aesthetic, so they were obviously the foundation for that look. First, let's review some very basic trigonometry, because you can't have anything rotating in a game without some trig. There are some beautiful visual explanations for what sines and cosines are, and if you ever had a HS-level Maths lesson, you probably know how to solve equations with them. In the context of rendering walls however, this is all we need to know:

And with just that it's quite easy to see how a wall might be rendered. First, let's think of just the top line of pixels of the wall. To render it, we start at some point, then we take a number of steps in a certain direction and draw a pixel at each step. The number of steps depends on the length of the wall. The direction depends on the angle of the wall, and the angle of the camera. Because the length of the wall in pixels is equal to the width of the bitmap texture that this wall should look like, we can simply use the number of steps taken as the x coordinate within that bitmap.

Here you should notice that with each step we move $\sin(angle)$ pixels vertically and $\cos(angle)$ pixels horizontally. Neither of these numbers change along the way, otherwise the walls would be curved. Because of this, we can pre-calculate them once before drawing each wall instead of recalculating them with every step. This makes the rendering much faster, of course. You might notice that $\sin$ and $\cos$ are real numbers, expressed as decimals, not whole numbers. We keep track of our position as we are drawing as a pair of real-numbered coordinates, but when we actually render a pixel, we round these numbers to the closest integer.

Next, to draw the whole wall, not just the top line of pixels, we simply draw whole columns of the bitmap instead of single pixels. The height of the wall is also equal to the height of its texture. It doesn't really matter which direction we start drawing the wall. The columns that we draw in later steps will overdraw the columns drawn earlier if they happen to overlap, but this only happens at angles very close to 90 degrees (perpendicular to the screen).

Finally, you might realise this would make walls look the same length no matter what angle they are currently facing. In the game, it appears as if the camera is looking down on the room from a 45 degree perspective. The walls that are perpendicular to the screen are drawn half as long as the walls that are parallel to the screen. In other words, we need to take half-steps when moving horizontally. Or rather, with each step, we move $\sin(x) / 2$ pixels vertically.

Rendering floors

To draw floors, we can first consider a simpler problem: how to draw the edges of the floor? All the floors in one rooms are rectangular, so drawing the top and left edges of a floor is in fact the same as drawing (one row of pixels of) two perpendicular walls. The angle for one is 90 degrees more than the angle for the other one. How do we use this to draw the floor itself?

We can think of the floor as a grid, with each pixel having coordinates in this grid according to how far it is along each of the two edge walls. So, to draw a pixel which is 2 steps along the top wall and 3 steps along the left wall, we start in the top left corner, move 2 steps along the top wall, then move 3 steps along the left wall. It sounds extremely trivial, because it is. But remember, a step along a wall is actually adding $\cos(angle)$ to our x coordinate and $\sin(angle)$ to our y coordinate. Also, the angles for the two walls are not the same, so we are adding different values depending on which wall we are stepping along.

The above should ring some bells for anyone familiar with the basics of linear algebra. What we did is in essence a basis change. We transform coordinates from the texture into pixel coordinates on the screen. From the way we render walls, we have already determined the $\hat i$ unit vector (representing x coordinates) gets transformed to $\begin{bmatrix}\cos(angle) & \sin(angle)\end{bmatrix}^T$. The j unit vector (representing y coordinates) gets transformed to $\begin{bmatrix}\cos(angle + \frac{\pi}{2}) & \sin(angle + \frac{\pi}{2})\end{bmatrix}^T$. These two column vectors are then the columns of a linear transformation / matrix we apply to each point:

However, because we are iterating over the entire bitmap anyway, it would be slow to apply that transformation to every single pixel, so we walk along the grid one step at a time.

Hiding walls

In the game, room walls which are facing away from the camera are not visible. To achieve this, walls are always defined in a strict order. To make the inside walls visible, they are defined clockwise – the top wall starts in the top-left corner and continues to the right. To make the outside walls, they are defined counterclockwise.

Then we can simply skip walls whose angle is not between 90 degrees (going away from the camera) and -90 degrees (going towards the camera). Some walls are shown anyway, e.g. phone booth walls.

Handling overlap

As I've already hinted at in the sprites rendering part, we need to handle situations when there are more objects in the same place on the screen. Which should be visible? Does the wall hide a sprite, or is the sprite in front of it?

To handle this, we have an additional hidden bitmap apart from the actual screen bitmap we are rendering to. (It's not actually a bitmap, because it holds numbers which do not represent colours, but it can be thought of as a bitmap because it shares the same coordinates with the screen bitmap.) This bitmap is our z-buffer and its purpose is to remember how far each pixel is from the camera. When drawing a pixel that would overlap a previously-drawn pixel, we need to look into the z-buffer and see whether the new pixel would be closer to the camera than the old one. If yes, we draw it and overwrite the z-buffer value. If not, we keep the old pixel as is.

To know the distance from camera the formula $(x + y) * 1000 + z$ is used in the game. This is actually larger when approaching the camera, so when drawing overlapping pixels, we keep the ones with the larger value of this formula. The $x$ and $y$ in the formula represent coordinates on the floor (x going left to right, y going top to bottom), while $z$ represents the height of the pixel, or its distance from the floor. We multiply $(x + y)$ by a thousand to make sure that anything which is 'actually' closer to the camera is not overdrawn by higher pixels. Nonetheless, higher pixels should have some preference to lower ones with the same $(x + y)$ values.

I realised after finishing one rooms that the rendering would be much neater if I simply used $y * 1000 + z$, but it was too late to change this! Because our x coordinate represents strictly a left-to-right movement, it doesn't actually make sense to include it in the calculation. I was mistakenly thinking of isometric depth calculations, where an increase in either coordinate represents a step towards the camera. This introduced some minor visual glitches into the game. Oh well.

Interaction map

There is actually one more hidden bitmap! This one is drawn to whenever the main screen bitmap is drawn to, and it gives each pixel a number, which identifies the object that is visible at that part of the screen. This way it is very simple to tell at which object the player is pointing. Likewise, if the pixel belongs to the floor, it is simple to tell where to move the player when they click there. This also makes the pixel detection pixel-perfect. (Though in retrospect the interaction zone for the phone should have been a little bigger!)

Conclusion

And that is about it! The only other somewhat interesting effect in the game is the warping effect, which is achieved by simply tuning the parameters of the rendering process. Writing that effect was also a bit of a trial and error, so there is not much to be written about that. You are of course welcome to see the source code for yourself. Hopefully you found this article somewhat interesting, especially if you are starting with gamedev.