Weekly Lua post: mapmaking
May. 15th, 2011 01:13 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Lua is a pretty much ideal language for playing around with map generating functions. I made a cute one today, combining linear noise with a couple other things.
To start off with, here's how I create a Lua map class: I start by making a "Map" table that contains a "methods" table. Anything class-level (what Java would call "static") goes in Map; anything instance-level goes is Map.methods. I make a Map.new function that creates a new Map of a certain size, filling it with either a constant value or linear noise (I'm using 16 elevation levels):
One tricky thing: Lua libraries expect tables to be indexed from 1 (although it doesn't really matter). So, I (probably inadvisedly) made mine like that, and then got plagued by off-by-one errors because I've written this class so many times in other languages that it's practically muscle memory.
So anyway, I'm going to skip a lot of these functions and just describe what they do. I start off with a small map that I fill with noise, then run a simple smoothing function over. The smoothing function just takes, for each cell, the average of its value and the values of all its neighbors:
Now I can fractalize it. For each tile I pick new pixels to go in the middle of each edge and in the center, so the middle of the left side will be either the top-left or bottom-left pixels, and so on. Now I can divide it into four new tiles and recurse (because the new tiles have values in each of their corners).
Once I recurse all the way to the bottom, I run the smoothing function one more time on the new map and then print it out. Making an image out of it is another little trick: rather than deal with finding an image-writing library and figuring out its API, since I'm just playing around, I write the image data by hand in XPM format. X Pixmap is a really simple image format, readable by the Gimp and Emacs, that was invented for storing X Windows icons, and is human-readable and trivial to write. Here's an example:

So, since I can chain everything together, after playing with it some, I ended up with this to create a map:
So, enough chatter. Let's look at a few random maps:



I think these came out much better than my last attempt at mapmaking. The elevations actually work; mountains slope gradually down into seas.
To start off with, here's how I create a Lua map class: I start by making a "Map" table that contains a "methods" table. Anything class-level (what Java would call "static") goes in Map; anything instance-level goes is Map.methods. I make a Map.new function that creates a new Map of a certain size, filling it with either a constant value or linear noise (I'm using 16 elevation levels):
Map = {methods={}} -- Make a grid of random values 0..15, or a fill value function Map.new(width, height, fill) local map = {width=width, height=height} setmetatable(map,{__index=Map.methods}) for n = 1, (width * height) do map[n] = fill or (math.random(16) - 1) end return map endPretty basic so far. I'll add a few methods like
at
and set
that change values based on x and y coordinates.One tricky thing: Lua libraries expect tables to be indexed from 1 (although it doesn't really matter). So, I (probably inadvisedly) made mine like that, and then got plagued by off-by-one errors because I've written this class so many times in other languages that it's practically muscle memory.
So anyway, I'm going to skip a lot of these functions and just describe what they do. I start off with a small map that I fill with noise, then run a simple smoothing function over. The smoothing function just takes, for each cell, the average of its value and the values of all its neighbors:
function Map.methods.smooth(self) local new = Map.copy(self) for x = 0, self.width-1 do for y = 0, self.height-1 do local n = self:p2n{x=x,y=y} local s, c = self:neighbor_sum(x, y) local nv = (self:at(x,y) + s) / (c + 1) new[n] = math.round(nv) end end return new endI keep this pattern of having every method return self so I can chain methods together. Then, I scale the map up 8x, by making an 8x8 tile for each pixel, with the corners being the adjacent pixels. So, the pixel at (0,0) becomes the upper-left corner of a tile that also has (1,0) at the upper right, (0,1) at the lower left, and so on.
Now I can fractalize it. For each tile I pick new pixels to go in the middle of each edge and in the center, so the middle of the left side will be either the top-left or bottom-left pixels, and so on. Now I can divide it into four new tiles and recurse (because the new tiles have values in each of their corners).
Once I recurse all the way to the bottom, I run the smoothing function one more time on the new map and then print it out. Making an image out of it is another little trick: rather than deal with finding an image-writing library and figuring out its API, since I'm just playing around, I write the image data by hand in XPM format. X Pixmap is a really simple image format, readable by the Gimp and Emacs, that was invented for storing X Windows icons, and is human-readable and trivial to write. Here's an example:
/* XPM */ static char * map_xpm[] = { "16 16 16 1", "0 c #000033", "1 c #000044", "2 c #000055", . . . etc . . . "45575569ca787679", "3457679aa998977a", "4557799889b889ab", . . . etc . . . }Becomes this:

So, since I can chain everything together, after playing with it some, I ended up with this to create a map:
math.randomseed(1) -- Or whatever... m = Map.new(17, 17):smooth():scale(8):fractal_tile(8):smooth():slice(0,0,128,128) file = io.open("map.xpm", "w") file:write(m:xpm())(Since what I'm scaling are actually the spaces between cells, in order to get 16 tiles with corners I need a seed map 17x17)
So, enough chatter. Let's look at a few random maps:



I think these came out much better than my last attempt at mapmaking. The elevations actually work; mountains slope gradually down into seas.