Wednesday, August 3, 2016

Big Awful Game Jam #2: Walkthrough

The past month's seen some complications, so I've been coding rather than writing updates, but the game jam is over now.  Unfortunately, I did not finish the gameplay coding.  Said complications tightened the schedule too much to achieve my design plan and I was unwilling to compromise my personal goal for the project: generating world maps that wouldn't look out of place in a Tolkien-style fantasy book.

Sample Screenshots

So let's talk about map generation.


First, very quickly, we've got to address art style.  I've always wanted to find a way to make a game map look old book maps.  I adored the maps in the Lord of the Rings as a kid, and grew up playing Bungie's Myth games which have a very similar style in their world map.  Heck, I still occasionally peruse The Atlas of Middle Earth (which is next to my desk as I'm writing this) for fun.  Very memorable art references.  I knew before the jam theme was announced that I wanted to finally fit fantasy mapmaking into the project, which meant finding a way to produce the appearance of a parchment map.

The parchment look is produced by several layers of textures interpreted by a custom shader.  Thankfully this is made easy using ShaderForge (a very handy Unity plugin).  I've done manual shader coding in the past, before I discovered ShaderForge, but I'm much happier if the preset node options in ShaderForge do the job for me.  At a basic level, there's a set background parchment texture, a masking edge texture, and some calculated color burn approaching the edges.  The background and edge textures are made in Photoshop.  Color and shading shifted several times over the course of the project for clarity without clash.  Here's what a blank map looks like:

Basic Parchment
There are several layers of algorithms that go into creating the overlay textures (it takes a 3-5 seconds to spit out a full map, though I've not done any optimization - it could be sped up quite a bit).  Water, forests, and border shading are all on one texture, while mountains are on a second (because mountains sometimes overlap slightly with forests, a second texture is easier than manual detection).  As astute viewers have likely spotted from the initial screenshot, the game was intended to play on a hex-grid.  A hex-grid of Region objects forms the core of the map.  Each Region has - among many things - an elevation level and rainfall level, which together determine whether each hex is sea, land, forest, or mountain.

Elevation and rainfall values are set by a simple nearest neighbor search in two master terrain-type arrays.  While there's some noise to shake things up slightly (I made use of Unity's Mathf.PerlinNoise function to avoid totally random noise, though only extremes actually affect the Region hex-grid and they're never allowed to wipe out basic land), the overall shape of the continents is determined by the elevation master array.  The rainfall master array is similar, but also dependent on the elevation array: each time a mountain point is added to the elevation array, wet and dry points are added to the moisture array to simulate rain shadow.  Sadly, one feature I didn't have time to implement was rivers, but they'd be a combined product of elevation, rainfall, and a recursive flow function (very similar to my unfinished Dominions 4 map generator, from which many of this project's technical ideas originated).

Early Region Grids - Elevation Debug View

One benefit of targeting a specific subset of maps - traditional fantasy maps - is that they're generally modeled off of Europe and thus have a relatively limited feature set.  They are in the northern hemisphere, they feature seas to the west and south, and they rise to mountains in the north and sometimes east.  The master elevation array is seeded to produce a similar result, and the rainfall array assumes that moisture comes in off the seas to the southwest.  Also worth noting is that the a dry point is placed in the center of the inhabited kingdom (which isn't visible on the map texture, since cities are part of the partly-finished interface - NGUI sprites, to be exact).  The reason is simple: we humans chop down trees like crazy wherever we settle.  Tweaking the rainfall master array was the simplest way to simulate this venerable form of terraforming.

Translating a small grid of Regions into a large grid of pixels is where most of the work lies.  Thankfully a hex-grid makes for a series of triangles, which makes it relatively easy to find the 3 closest Region points for each pixel.  Here's an example, using the same simple closest-type algorithm as the Regions did:

Basic Elevation Texture
Obviously built out of hexes though.  What if we add a layer of simple Perlin noise on top of interpolating smoothly between Region points (with cutoff values for each elevation type)?

Noised Elevation Texture
Now it's getting interesting!  The exact scale of noise sampling changed as much as color choice over the course of the project, but this system is at the base of every pixel.  Forests are done the same way, but by interpolating rainfall values as well as elevation (using the same Perlin noise addition to the rainfall level, but different elevation cutoffs).  The result is a set of forests that roughly parallels mountain and sea borders, but never precisely matches.

Forests and Color Tweaks
The resulting image looks very flat without border shading.  Luckily edge detection is fairly simple for this kind of image.  It's worth noting that I've been saving data as custom PixelInfo objects, not raw Colors.  This stage is where it starts to make a difference in manipulation.   For example, it's very simple to look through the whole array once and tell if any neighbors are of a different type.  Combine with a pass to eliminate isolated pixels and shading passes to fade out on each side of the border, and it looks much better:

Borders!

Starting to look familiar now, though tweaks on color, boldness, and shading distance never stop.  It's not as obvious in this screenshot as in the final version, but the water fades towards a white (technically teal) border rather than a black one.  It's a simple trick I picked up long ago, making texture maps for custom Myth II maps, but it makes water look much more crisp and real than the dark shading that I still sometimes see on drawn water.

You'll notice that the mountain areas are still flat textures in these screenshots.  That doesn't fit the target style: we want "hand-drawn" looking hills and mountains.  Instead of drawing the flat mountain color (and it's borders) on the texture, we need to composite a bunch of smaller sprites onto another texture to form the mountains.  The system needs to sprinkle sprites onto mountain areas, with the type of sprite depending on the elevation of the target pixel.  That means referring to pixel elevation again, along with a random check to see if a sprite should be placed.  Jumping ahead a bit, it surprisingly looked best for sprite placement to be totally randomly determined (albeit restricted to within mountain regions), and instead for the sprite type to be set using the more complex Perlin-influenced individual pixel elevation (from among 3 different sprite categories of increasing size, which I called hills, mountains, and whitecaps).

Implementing this system isn't too hard, but it does require attention to a couple of simple steps.  First, iterate through the pixel array by going from top to bottom (rather than the usual bottom to top).  This means the sprites will appropriately layer them from background to foreground.  Second, each sprite needs an additional mask: when placed, it needs to over-write any other mountain pixels in part of it's area (the interior of the mountain) - often with transparency!  So it not only needs an alpha channel to determine the transparency of it's pixels, but an additional alpha channel to determine where it overwrites it's brethren.  Finally, each sprite also needs an exclusion zone in which other sprites can't be placed (or else it sometimes looks too crowded).  With those additional pieces of data in place, the system works quite smoothly - and if I spent additional time in Photoshop to make more sprites, it would be little trouble to integrate an arbitrary number of variations in hill/mountain/whitecap styles.  I'm quite pleased with the existing sprite arrays though:

Hill and Mountain Sprites
The result is a program that achieves my objective of creating parchment fantasy maps.  I'd be even happier if I'd had time to implement rivers, roads, and cities, but that doesn't make me unhappy with what's there.  I'm undecided whether or not to continue development, likely publishing the resulting game in some small independent form (got a whole game design partially implemented, after all).  In map generation terms, the lack of rivers bugs me enough to want to implement that feature.  At minimum I think it's been a successful proof of concept for the generation method, though I'd certainly appreciate constructive criticism from any readers.

No comments: