Tuesday, July 19, 2016

My World, Part 4: Map Renderer

I was going to follow up the last post with an updated map image showing the civilized vs. wild distinction, freshly added to the rendered map. Unfortunately I've gotten stuck on the last 10% of the code, the crucial part which tells the renderer how to color the triangles. It sounds trivial, but owing to the way I've structured the file, it's not as easy as it sounds to simply give them the appropriate color.

No worries, though. This is the kind of hangup where I'll come back over the weekend and instantly see what needs doing. All I need do is let the idea sit untouched for a few days, and I'll think of something. As I said previously, this is not the first attempt at the triangle maker, it's just the first successful one after a few months: so what's another day or two?

In the meantime I'm going to explain how the map renderer itself works.

First, the World Generator program creates, as its primary output, three files which contain data about my game world. One file contains info on towns, another is for roads, and the third and largest contains data about the hexes themselves -- including their sub-triangles.

Each generator output file corresponds to a parser I've written in Haskell. Now what is a parser? Well, it's a type of program which consumes ordinary text and attempts to arrange it into a structured format for further manipulation. Initially, World Generator output contains chunks like "Elevation 0.133533" and "Climate Mediterranean" and "[Coord (49, -65, 16),Coord (48, -65, 17),Coord (48, -66, 18)]". However, plain old text like this isn't useful to a computer; we have to pre-specify that, for example, "Elevation 0.133533" refers to a type of data, with "Elevation" being the data constructor and "0.133533" being the value. If the string were malformed due to an error -- say that "Elevation" was spelled "Evelation" -- the parser would fail to parse, i.e. be unable to assign the structure I've specified to the text string.

Here's the structure I'm using to store information about a sub-triangle (referred to as simply a "Sub" here):

The first definition states that SubInfo is a 3-tuple. I parse that 3-tuple, and then immediately convert it into a correspondence between a Direction (definition not shown) and a Sub, specified on the next line, since I need to be able to look up that data by naming a given Direction.

Next, here's the parser which verifies input data and converts it into the above SubInfo structure.

Note that parseSubInfo is a parser which it itself composed of smaller parsers, as I've indicated with arrows. For example, parseQuality is defined as validating (and then throwing away) the string "Quality", and then doing the same with some spaces. Then it tries to validate either the word "Civilized" or the word "Wild." If successful, it converts that word into the appropriate data type (a Civilized or a Wild -- no longer a string, but a value.)

Higher up in the program I use parseSubInfo itself as a component in an even larger parser, which slurps up all the data for a single hex. This technique of combining small parsers into larger ones is extremely intuitive and straightforward; the functions here are usually referred to as parser combinators, because of their combining nature. (Moving forward, unless I say otherwise, when I say "parser" I mean the final, primary parser in each program, which may be made of smaller components.)

So: one parser for roads, one for towns, one for hexes. We use the parsers to structure the generator output into useful shapes in memory, and then pass that now-useful data into the main body of World Renderer. This program does the actual work of drawing each layer of map data into the final image. Such work includes:

  • calculating the pixel coordinates at which a given hex should be drawn, based on its hex coordinates
  • coloring each hex based on climate, or moisture, or elevation, or whatever else I specify from the data (thus enabling me to recreate the entire map with a new color scheme, whenever necessary)
  • drawing in the roads between the locations of each town
  • drawing the little town-name indicators themselves

The important part to take away is that most of what I'm doing is defining different ways to draw different data. If I added a new kind of info to each hex, all I'd have to do is specify how it should be represented as a shape or color, and I'd be ready to render a freshly improved map. I can also specify exactly which hexagons I want to render, which lets me zoom in on chunks of the world, as has been the case with the maps I've posted recently.

Here's one function from the renderer program. It calculates the output pixel coordinates for a given hexagon, based on its cubic coordinates.

And here's the primary function (one below the top-level function, which calls the parsers and then feeds their results into this function.) I think my naming conventions make it clear what is going on: we draw the hexes, then we draw the roads on top of them, then we write the town names on top of that. Note that we don't need to draw the town names before the roads, since although they haven't been rendered yet, we still have their locations in memory as part of the road info. Also note that the definition for "filtered" is how I restrict the render to a certain section of the whole ("wanted" is defined elsewhere.)

I give the renderer executable program a filename and it draws the map into that file. And that's all there is to it!

For the record, the last step is the one I'm stuck in for the civilized/wild triangle data. I'm having difficulty specifying exactly the right commands to extract triangle data for each hex and color them appropriately. Everything's parsed and ready; the renderer just needs its new drawing commands. Now you have an idea of how close I am to nailing this!

Please speak up if I've piqued your interest or if something's unclear.


  1. This is incredibly clever. I also strongly approve of your dividing hexes into their component equilateral triangles - the lower number of combinations mean that it's a system that works even for those without the programming skills to write an application like the one you've created. Have you, or do you plan to, incorporate Alexis' infrastructure numbers into your populating process?

    1. Can't tell you how much I appreciate knowing someone is reading these, Dani. One person is enough.

      I do plan to incorporate infrastructure numbers. I'm not sure how it'll happen yet, but I expect that I'll be giving each hex a rating based on its civ/wild distribution, with more civ => higher number, and clumpier civ => higher number. I'll probably also add a bonus based on town populations in the hex, with the idea that more settlements means more improvement of the surrounding area. From there it's just a matter of spreading the numbers around from hex to hex, which I've already learned how to do for the purpose of moving moisture from water to land.

      One open question is: do I give an infrastructure rating to each individual triangle, or just use those to determine layout and magnitude of infrastructure, and assign infrastructure values only to whole hexes? I'm thinking the answer is 'the latter' but I'll have to play with it.

      There's also the newly-unearthed challenges of assigning towns to indiv. triangles, and of wanting roads (and eventually rivers when I add them) to path between individual triangles, not just grossly pathing between hexagon centers. That will all take some doing, and may require rewrites of certain parts of the architecture.

      Good catch on the viability of triangles for analog environments. I hadn't thought of that. In truth I'm already thinking about subdividing these triangles into subsubtriangles, giving me 24 locations per hex -- although I'd still determine civ/wild on the basis of the subtriangles, not the subsub ones, so as to get the nice clumpiness, and not have everything degenerate into randomness. However, I definitely won't take that on any time soon -- I'd rather get some results out of the current triangles before recursing into another sub-realm. There's already plenty to do.

  2. Hello there !

    Just wanted to say that Dani is not the only one reading and appreciating those. I always wanted to code Alexis' Infrastructure and map generation ideas, but life give me too little time for that (barely enough for RPGs so ... ^^).

    I really dig your blog, mainly World Creation things.

    Keep it up !

    1. Thanks for your kind words, Vlad. I appreciate everyone who takes the time to write something.