Sitemap

Journey Into The Hills: Part 2

5 min readMar 3, 2017

In part 1 I looked at the various approaches I took to rendering heightmap based terrain in PICO-8. This time I’ll explain how the terrain is generated and show a PICO-8 cart ‘2D Terrain Demo’ (PICO-8 BBS) that demonstrates some of the solutions.

The terrain is stored as an array of numbers. In the example cart, this array has length 128, the width of the PICO-8 screen for simplicity. The function generate_terrain() (line 57) is called upon initialisation of the cart and mutates the terrain array using the simplified Diamond-Squared algorithm described in the previous post. The first thing this does is initialise the array to a uniform height.

This is interesting because the height values of the terrain will broadly follow this base height (with variation depending on a random factor — more later). If you try initialising this with random values, you get sharp jagged variations in height. Like this:

The next step is to pick a starting ‘step’ value, which you use to traverse the array. This starts with just dividing the length of the array in two. I got caught out the first time I wrote this, because I didn’t realise that Lua doesn’t have integers, so kept referencing elements in the array that didn’t exist.

Next up you have a while loop that forms the main part of the terrain generation. You loop until the step gets too small. At the end of each loop, the step is divided by 2, to fill the increasing small gaps in the terrain . If you change the minimum step size, you’ll notice that vertical slices are still at their initial value and haven’t been ‘smoothed’ into the curve of the landscape.

while(step>=1) do
local segmentstart=1
while(segmentstart<=width) do
... -- see below
end
randomness/=2
step/=2
end

Within the main while loop, you use the step variable to traverse the array to set the midpoint of a sub-segment of the array. Each segment spans from your current positions and your current position plus the step value. One gotcha is that the step can ‘overstep’ the bounds of the array. In this case, the right/segment-end index is looped around to the start of the array.

while(segmentstart<=width) do
local left=segmentstart
local right=left+step
if right>width then
right-=width
end
generate_height_at_midpoint(left,right,randomness)
segmentstart+=step
end

You take the current height values at the start and end of the segment, take the average, and set this as the height value at the element at the midpoint of the segment. See the code below for how this is done.

function generate_height_at_midpoint(left,right,randomness)
terrain[flr((left+right)/2)]=
(terrain[left]+
terrain[right])/2
+(rnd(1)*randomness-(randomness/2))
end

A random factor is added here, which is what causes the peaks and valleys in the landscape. In the demo cart, try pressing up and down to change the amount of randomness. More randomness = more jagged terrain. At the end of the nested ‘traverse array’ while loop, the randomness is reduced by half. If you don’t reduce the randomness after each pass of the terrain, you don’t get the nice smoothing effect that repeated passes of the array yields.

Lots of randomness
Not much randomness

Once this is all done, the terrain has been created!

In the demo cart, the 1D heightmap in the `terrain` array is then copied across to a 2D array of pixel values, to be used to render the landscape in the per-pixel form.

The _draw() function contains three ways of drawing the terrain. The first is using lines, to give a green block of terrain. The second uses the ‘terrain texture’ in the sprite data. It’s not much more complicated than the line version. It uses sspr() to draw 1-pixel wide slices of the terrain texture. This texture is stored at 0,0 in the sprite data.

Notice that the line() call is still here, it’s used to draw the dark blue area. It would be possible to change the fourth argument to sspr() and use a larger vertical area of the sprite data (utilising the second page), if you wanted to texture more of the landscape. I quite like the contrast between the textured/untextured area, at least for my current project.

function draw_textured()
for i=1,width do
sspr(i%32,0,1,32,i-1,128-terrain[i])
line(i-1,128,i-1,128-(terrain[i]-32),1)
end
end

You might notice that the terrain value is subtracted from 128 because of the y axis increasing as you go down the screen. This could be flipped earlier at the terrain generation time.

The per-pixel drawing mode is also fairly simple. It uses a nested for-loop to traverse the 2D pixel array and has to do a nil check to see if there is a value at the current x,y coordinates. I talked about the problems of this in the last post. If you don’t do the nil check, it just draws black pixels where there isn’t a terrain pixel value.

I hope this post+cart are of some use to you. Please use it as the basis for any other projects, and let me know if you do (or if you have any questions) on Twitter @Powersaurus!

--

--

No responses yet