It’s generally best not to be ambitious when learning a new game engine, as soon enough simple plans spiral into the next AAA hit that you’ll never be able to make. As it happens, I was looking into the open source Godot engine, around the time I was playing through Bioshock Remastered and thought, “that seems like a reasonably straightforward thing to implement”.
The following is not intended as a tutorial, nor a technical guide to the inner workings of my implementation, merely a write-up of my attempt to recreate this minigame: the methodologies I used, the challenges I faced and the solutions I had to come up with. If you would like to view the (currently incomplete) code for this project, you can find it on GitHub
Modelling and Rendering
Blender 2.79 added a bunch of exciting features that I was excited to try out on a project, namely support for wide-colour-gamut ACES, and a new principled PBR shader based on the Pixar/Disney model. For this reason, I decided to implement the minigame in the same way as in Bioshock, using rendered 2D sprites.
The pipes were modelled after the version of the minigame seen in Bioshock Remastered, in a manner that made the renders physically correct. For example, the inner surface of the glass piping is modelled, so that reflections on the liquid inside can be seen. A normal map based on a simple noise generator was added to the metal to create a sort of hammered bronze look—though if I wanted to strive for a perfect recreation, I would have had to create a custom texture for the metal parts.
Where Bioshock simply rotated the same sprite for parts in different orientations, I rendered each part separately, so that the specular highlights are correct for all pieces. The scene was lit using area lights, with multiple thin key lights creating similar specular highlights to the original assets and also a number of wider, dimmer lights creating fill and rim lighting. This setup can be seen in the source Blender file.
The sprites were then rendered using an orthographic projection with a 512×512 resolution. Because all pixels in a raytraced material must resolve to full opacity, the empty pipes were rendered with an opaque glass material, and a glass mask image was rendered so the alpha channel could be composited in afterwards. The results for the vertical pipe piece were as follows.
The renders were scaled down to 256×256 and the alpha channel added, after which they were ready to use in-engine. A pipe class was created, which had a ‘flow percentage’ variable to denote how full the pipe was with water. a ‘reverse flow’ boolean was added later, as water may flow through the pipe in either direction.
To smoothly animate the filling of the pipes, I used two polygons—one textured as an empty pipe, and one as the filled pipe. Fortunately, Godot implements polygons in a really developer-friendly way; triangulation on Ngons is performed automatically and we don’t need to manually specify UV coordinates for vertices, we just need to set each polygon as a set of vertex arrays.
The straight pipes are straightforward, two vertices in each polygon are fixed, and the other two move either horizontally or vertically, according to their fill percentage. For the corner pieces, each polygon starts with a single triangle, whose edges meet in the centre of the pipe, then the dominant half gains an additional triangle, and stretches to where the lesser half ends.
As a fun bonus problem, because the moving vertex on the corner piece tracks along the square edge of the tile, when moving at a linear speed the flow of water slows down around the centre of the pipe. To fix this, the motion is remapped using a trigonometric function—The solution I came up with was −|sin(x)| + 1, with additional multipliers and offsets.
We are now at the point that our assets are both rendered, and programmed to draw correctly as instructed. In the next part of this blog post, I shall attempt to program the behaviour of the water as it flows through a full path of pipe pieces.