Sprint 28 - Adam-Poppenheimer/Civ-Clone GitHub Wiki
Goals
- Explore alternate methods for rendering the map.
Risks and Mitigation
- While I have some vague idea for how to combine different heightmaps for different shape and terrain types, it's not clear how that'll work with rivers or the sea.
- I know Unity has built-in solutions for water. I can probably handle the coast by creating a flat plain of water and ducking the terrain mesh underneath it for underwater cells. I think the key here will be a ElevationBuilder class that can select between a number of heightmap policies. The center of the cells will take from one of the pre-defined heightmaps (for flatland, hills, mountains, and the ocean). Cell edges will need to mix the height results for their adjacent cells, and corners will need to mix results from all three cells. That mixing will need to be overridden for rivers, which are a special case. Those can seek out a particular configured elevation, perhaps, but otherwise remain flat. Or maybe I don't represent rivers as a deformation of the terrain mesh at all and opt for drawing them as a mesh onto the surface of the world. That would simplify mesh triangulation a fair bit.
- The whole reason why I want to do this is to make my map rendering code organized and possibly unit-tested. If I can't come up with a sensible architecture, though, then the whole point is moot.
- I'll need to very carefully plan out my architecture ahead of time, well before I start rendering triangles to the screen. I can probably organize it in a similar way as the old triangulation code was developed: consider inner cells, two-cell boundaries, and three-cell corners. Probably I'll need a class to figure out heights, another one to figure out texture splatting. I'll probably need, maybe, an ISecondaryMeshBuilder interface to wrap additional meshes around the terrain mesh once I have it. This might have a set of standardized methods (TriangulateCenter, TriangulateEdges, TriangulateCorners). I'll probably want some notion of cell edges, and possibly methods for perturbing them, so that I can draw things like rivers along cell edges and have them follow irregular paths. It might also be prudent to create interfaces around the terrain mesh object so I can mock the terrain out during unit testing. Oh, and I should make a clearer distinction between the simulation side and rendering side of HexMap, perhaps by separating them into distinct namespaces.
- Exploration is implemented in an unusual way that might not be compatible with a future solution, which could be very difficult given my lack of experience with that matter.
- It's possible that the existing solution I've got'll work, though it's not the most organized plan. I could probably deal with features and units by disabling objects on unexplored cells, so that they don't render at all. And if I can get the cell data system working properly, I can use that to obscure terrain. There's probably a way to push that into the non-terrain secondary meshes, as well. Either way, exploration is likely a late-stage task, to be implemented only once I'm sure I want to use this new rendering method over the old one.
- While I'm sure there are some things from my rendering system I can repurpose, it might be difficult to disentangle them from the current mess of architecture I've got.
- Some of the things have already been separated out. Feature placement, for instance, is fairly well encapsulated, so repurposing it won't be that difficult. HexCellShaderData can probably be carried over, as well, since it's agnostic to the triangulation strategies I'm trying to remove. The shaders are likely to be troublesome, if for no other reason than I'm not that experienced building shaders. And the triangulation code is being intentionally tossed aside because it's impossible to think about. It might be that very little should be reused at all.
- Even if I start down a much better rendering strategy, that strategy could take so long to work through that it's ultimately not worth the time to build it.
- The key here is time-boxing. I'll devote a single sprint to this endeavor to see how it goes, mostly because I'm curious about a better way of doing things. Afterwards I can decide whether I should keep what I've done and convert everything else over or throw it away and go back to what I had before. This is an exploratory period, and its results will be the resolution of this conundrum.
- I'm not entirely sure how I'm going to handle cell data in my new solution. Given how important pushing cell data into the shaders is to the current implementation, that could be a huge problem.
- I'll have to make sure that my new architecture operates with cells in mind. I'll probably build out from cell centers, edges, and corners as I have, passing in appropriate index data. That task should probably be handled by whatever the highest-level deformation class is, since it's a service required by a lot of different subtasks. How exactly I manage that isn't clear to me yet.
- My current implementation makes heavy and fairly intelligent use of multiple meshes to render various components of the map. It's not clear to me how I'll build out those alternate meshes without my triangulation code.
- The terrain mesh should let me pull the height of the mesh at arbitrary locations. As long as I'm always pulling from the same set of points (so I set and read the heightmap from the same collection of points) I should be able to conform meshes to the terrain without hovering over or clipping through it by copying its height. It's also possible that some meshes (like roads) can be implemented as terrain and can just be painted onto the Terrain object directly.
- This task is so large that doing it all at once will just overwhelm me. But it's also not entirely clear what its component subproblems look like.
- There's a few basic things I'll need to address as a high priority. I'll definitely need to figure out chunking. I'll need to resize the terrain I've got. I'll need to paint basic textures on it and figure out how to blend them. I'll need to figure out how to read from and mix various heightmaps and apply their height to the terrain mesh. There's a whole bunch of other, more sophisticated tasks, but that should be enough for now. Once I've got a better idea of my architecture, I'll have a much better idea of what my subtasks are.
Review
The sprint started out fairly slowly and imprecisely, as one might expect. The first 19 or so pomodoros were dedicated to figuring out how Unity's Terrain system works and building a foundation for future work. There were a number of problems I didn't anticipate. Aligning my terrain meshes so that they properly surrounded the entire map without any dead space turned out to take some doing. And chunk boundaries ended up having to run between cells. But eventually I resolved these core issues and got a very basic start for alphamaps and heightmaps. I established some of the architecture of the whole solution but couldn't work through it all because I didn't understand the parameters of the problem well enough to do so.
After getting the basics down, I moved onto elevation setting, since better elevation was a big motivator for this sprint in the first place. I ended up spending ~41 pomodoros on this open-ended task. Several important things happened during this task.
First, I ended up reinforcing and elaborating on the architecture I'd established earlier. I opted to calculate heightmaps on a point-by-point basis rather than on a cell-by-cell basis so that I could have individual points operate independently of each-other. While I do think I should add per-cell refresh for performance reasons, I think the per-point solution is sufficient for basic height and alphamap tasks.
Second, I ended up with a pretty damn good solution for the basic tasks. Hills in particular look natural and flow across the terrain neatly. Mountains have ridges, and flatlands shallow undulations. The various shapes flow into each-other smoothly. While I didn't end up with a river solution and I'm sure there's much more to be done, I'm quite happy with how the heightmaps are looking. And since that was one of the main motivations for this sprint, I'm quite happy with how my graphical redesign went in general.
Third, I finally managed to add some test coverage to my rendering code. Exhaustion and a lack of focus has left it a bit spotty in some places, and I still don't have unit tests for HexGrid, but it's much better than what I had before. There are some places where unit tests seem either unhelpful or extremely unpleasant to build (particularly some of the triangulation code I ended up adding), but overall I feel better about my architecture and more confident about the correctness of my solution overall.
After heightmaps I moved on to alphamaps. This is a task I had already partially resolved during the more generic work at the start of the sprint, so it didn't take nearly as long as heightmaps. But I did manage to establish my special cases for mountains and farms, and came up with a reasonable solution for blending different terrain types that looks somewhat natural. Unfortunately the higher-quality heightmaps and alphamap blending has left my farmland solution looking absolutely hideous. I'll definitely need to rip that solution out and start from scratch, though I suspect that'll be easier now that I've got a smooth terrain mesh. As with all of these tasks there is more to be done, but I feel good about what I've managed so far.
Next I moved on to a solution for standing water. This ended up being more difficult than I'd anticipated but took less time than my heightmap solution. I decided to set aside the water solution I inherited from the Hex Map Guide and make use of one of Unity's more complete solutions. So I spent a fair amount of time trying to repurpose Unity's water, fiddling with config variables and trying to get it to chunk. I ended up taking Water4 and tweaking it a bit to add per-vertex color instead of per-mesh color, so that I could distinguish between shallow, deep, and fresh water. I also went back to the old triangulation pattern in order to create my plane of water and make proper use of chunking.
Water went reasonably well. I think my current solution is better than what I had before, even though it lacks waves. I have ideas about how I can resolve that lack of waves, though it's not particularly important at the moment. At the very least having different water colors is an important improvement. Unfortunately I opted to drop unit testing, partially because it wasn't clear what unit testing would accomplish and partially because the task seemed onerous and I did not want to do it. I don't know how I feel about returning to the old triangulation strategy. If I decide to keep this paradigm, there's probably things I can do to make it easier to manage. But for now I've got a reasonable water solution going.
After handling water, I attended to a few smaller tasks. First, I went back and filled in some missing unit tests. After that, I added features back into the game. This turned out to be incredibly easy. I got features fully functional by changing almost nothing about the codebase. To my great satisfaction, it looks like I successfully decoupled feature management from terrain triangulation. Thus features became one of those magical engineering tasks that just suddenly worked perfectly on the first try.
I spent the last two days of the sprint trying to figure out rivers, both their troughs and their surfaces. These two days were by far the rockiest of the entire sprint. I had some morale issues on Thursday and was generally not at my best while trying to work through this problem. And as with the original implementation, rivers turn out to be a very challenging special case. The fact that I wasted several hours impotently changing code that wasn't being called into didn't help, either.
Working with rivers simultaneously created new problems and led to the resurgence of old problems. Troughs, especially corner troughs, remain hard. I've managed a partial corner trough solution, but it doesn't work properly right in the middle of the trough. I think the paradigm I'm using is too complex to manage properly. It's certainly difficult to reason about, which is not a good result.
For river surfaces, I managed to convert the Water4 implementation to give it a bit of flow and got water flowing down river edges fairly well. But that solution ran head-long into an old problem for river corners. Namely, how do you get a smooth curve (or a smooth confluence) on a single triangle?
The whole river problem makes me want to throw away my old solution and come up with something completely different. It's not like my old river solution looked particularly good, anyways. It's very likely that I'll throw away the work I did for rivers and try something more ambitious, more like how Civ 5 does rivers. If I'm going to struggle with them, I might as well create rivers with smooth curves and variable width. Might as well get something good out of the struggle.
Overall, I think this sprint has gone very well. It represents a much better solution to the problem of map rendering than what I had before, both from a visual and an organizational standpoint. There's still much work to be done, but I will commit more sprints to do it. If I can reapply my old solutions well and get improvements for farmlands and rivers as substantial as those for heightmaps, then my project will be in a very good place.
Retrospective
What went well?
- I've figured out a much better way to do terrain heights.
- I've figured out a fairly good way of doing terrain mixing, and one which can be more easily extended in the future.
- I've managed to establish at least some test coverage over my map rendering code.
- My feature placement subsystem turned out to have an excellent architecture, since I was able to rip it out of my old solution and throw it in my new solution with almost no changes.
- I build a somewhat better solution for water, one that makes use of Unity's built-in tools and visually differentiates between different types of water.
- I've established a reasonable architecture upon which I should be able to build a fairly good rendering solution.
- I've learned a lot about Unity's Terrain system, and a bit about its Water solutions, both of which are important subsystems of the engine that I've not made use of before.
What could be improved?
- I need to come up with a completely different way of doing rivers. The current solution simply won't cut it.
- It's not clear to me how I'll manage things like flood plains and cultural boundaries. I need to come up with a plan for features that lie on the edges of cells (which might also include coastlines at some point).
- The quality of my farmlands solution is no longer tolerable. It needs to be rebuilt from the ground up.
- While test coverage in my terrain rendering is a lot better, it's still not stellar. I need to develop a better testing discipline for my graphics code, including a solution for triangulation
- Given that MapRendering has nothing to do with the simulation, I should probably move it out of the Simulation namespace and into a new Rendering namespace.
- I need to be more diligent about switching away from tasks when I'm overwhelmed so that I don't lose precious hours staring blankly at a bunch of math on a screen.