Jack Robinson | Blog

Generating Islands with Love2d and Box2d

Recently I’ve been back dabbling in some development with Love, and in my project I came to a point where I needed some islands generated in my game world.

On the surface, I could just generate n points and form islands around those, but at the time of implementing this, I wanted to provide a strict “budget” of islands - n islands of xl size, m islands of lg size, and so on.

Rather than agonise over an algorithm that’d shuffle squares away from one another, I instead opted for an inbuilt tool - box2d. Love provides access to this awesome library via its love.physics module, making full physics simulations pretty painless.

Running the simulation for a bit, this is what I’ve ended up with:

And applying some noise and a falloff equation, we can get this out the other side:

How, What?

What we’re doing is abusing love.physics to generate a bunch of blocks in the dead center of our screen. We then then run the simulation until it resolves everything, align it to the tile grid, then for every box, generating the landmass.

Here’s the physics part in action:

Disclaimer

I guess before we get started and you go a-copying this work, a small side-note.

Box2d is only kinda deterministic - given the same input, and binary, it should reproduce the same simulation.

To nitpick, though, this isn’t always going to be the case. Depending on the binary or platform it’s run on, floating point numbers might cause slight differences. At that point it’s getting into some hairy stuff well above my pay grade - but it’s something to keep in mind.

With that outta the way, let’s get setup

Setup

We’ll keep everything in a single main.lua file for this tutorial. Let’s start with some skeletons of what we want.

-- ==============================================
-- World Generation
-- ==============================================

local getIslandBudget()
end

local function generateNoise(x, y)
end

-- ==============================================
-- Physics
-- ==============================================

local world -- physics world

local function physicsBody(x, y, size)
end

-- ===============================================
-- Entrypoint
-- ===============================================

local function generate(world)
end

local function generateHeightMaps()
end

-- ==============================================
-- Love functions
-- ==============================================

function love.load()

  generate()
end

function love.update(dt)
end

function love.draw()
end

Physics

We’ll start with setting up our physics. The Box2d concepts we care about here are Worlds, Bodies, Shapes and Fixtures:

I’m ignoring collisions here as we’re not going to deal with those, but they’re probably the other major concept we’re missing here. The love.physics wiki page has an awesome breakdown.

Going in barebones, we just need to add the following to our love.load function.

function love.load()
-- Initialise to have no x or y gravity.
  world = love.physics.newWorld(0, 0)
end

We also need the world to update in our game loop, so we’ll call it in love.update:

function love.update(dt)
  world:update(dt)
end

Next, we’ll setup our physicsBody function to create our objects to simulate:

local function physicsBody(x, y, size)
  local body = love.physics.newBody(world, x, y)
  local shape = love.physics.newRectangleShape(size, size)
  local fixture = love.physics.newFixture(body, shape)
end

And with that, we now need something, or somethings to simulate.

Island Generation

I mentioned in the opening, I wanted the ability to define a ‘budget’ of islands that could be spawned in. This allows me to control, say, one big island, a couple medium sized, and a bunch of small ones per zone.

Simple enough, we just define a function that returns a table of sizes we then pass to physicsBody.

local XL = 17
local LG = 9
local MD = 7
local SM = 5
local XS = 3

local function getIslandBudget()
  local budget = { XL }

  for i = 1, 6 do
    table.insert(budget, LG)
  end

  for i = 1, 12 do
    table.insert(budget, MD)
  end

  for i = 1, 8 do
    table.insert(budget, SM)
  end

  for i = 1, 12 do
    table.insert(budget, XS)
  end

  return budget
end

You can tweak the numbers as you see fit, what matters is we now have a table containing a bunch of different island sizes that we can pass to our physics simulation and get shuffling.

Tying them together

You’ll notice that the values in the island budget are rather small. This is because in my game, island sizes are measured in their Chunk size. Chunks are an optimisation method for large games (again, think Minecraft), and in my project they’re 32 tiles wide. Since we’re not actually making a game, we’ll just call a Chunk 32 pixels wide and high.

local CHUNK_SIZE = 32

local function generate()
  -- get the island budget
  local islands = getIslandBudget()

  -- Then turn them into physics objects
  for i, island in ipairs(islands) do
    physicsBody(
      love.graphics.getWidth() / 2,
      love.graphics.getHeight() / 2,
      island
      )
  end
end

So we can actually see our islands, lets draw them in our love.draw function. To make this easier, lets modify physicsBody to return a table, and we’ll store that in a file-scoped local to make it easier to access.

local world

local function physicsObject(x, y, chunkSize)
  local size = chunkSize * CHUNK_SIZE
  local body = love.physics.newBody(world, x, y)
  local shape = love.physics.newRectangleShape(size, size)
  local fixture = love.physics.newFixture(body, shape)

  -- make sure the collisions don't cause our squares to rotate
  body:setFixedRotation(true)

  return {
    body = body,
    shape = shape,
    fixture = fixture,
    chunkSize = chunkSize,
    size = size
  }
end

And then we modify generate as so:

local physicsObjects = {}

local function generate()
  -- ...
    for i, island in ipairs(islands) do
    local physObj = physicsObject(
      love.graphics.getWidth() / 2,
      love.graphics.getHeight() / 2,
      island
      )
    table.insert(physicsObjects, physObj)
  end
end

When we go to render them, physics Bodies have their position origin in the center of the body, so we’ll have to shift it to the top-left, which is what love.graphics.rectangle expects. While we’re here, we’ll also change the colour depending on island size:

function love.draw()
  for i, physObj in ipairs(physicsObjects) do
    local bx, by = physObj.body:getPosition()
    local x = bx - physObj.size / 2
    local y = by - physObj.size / 2
        love.graphics.setColor(
          physObj.chunkSize / XL,
          (physObj.chunkSize / XL) / 2,
          physObj.chunkSize / XL
          )
    love.graphics.rectangle(
      "fill", x, y, physObj.size, physObj.size
      )
  end
end

Running this, and we get… this:

By default, if everything is at the same position, box2d seems to resolve them to pop upwards. Sure, over time they spread out a little, but it’s not very “spread out”. Easiest solution to this is adding a little variation on the body’s position when we create it:

local physObj = physicsObject(
  (love.graphics.getWidth() / 2) + love.math.random(-16, 16),
  (love.graphics.getHeight() / 2) + love.math.random(-16, 16),
  island
  )

Nice.

It’s preferable to fuzzy up the initial position, rather than applying a force to a physics object, as we need to have our bodies ‘sleep’ as soon as possible. In my experiments, applying a linear impulse and messing with restitution, mass, friction etc. just ended up more pain than it was worth.

Sleeping

Now we have a little bit of a spread going on, we want to make sure they’re not overlapping one another before moving onto the next step of actually generating the height maps.

Thankfully, box2d has the notion of sleeping - if a physics object hasn’t moved in a few cycles, it’ll be put to “sleep”. If all our bodies are sleeping, then we know that all the collisions have been resolved, and we can proceed.

local sleeping = false

local function allSleeping(bodies)
  for i, body in ipairs(bodies) do
    if body:isAwake() then
      return false
    end
  end
  return true
end

function love.update(dt)
  world:update(dt)

  if allSleeping(world:getBodies()) then
    sleeping = true
  end
end

It’d also be wise to stop updating our physics simulation, as when we snap the grid, we don’t want it to start resolving again, sleeping again, resolving etc.

if not sleeping then
  world:update(dt)
end

Snapping

This step is probably dependent on your game design, but if you’re running a large simulated world, you may want to store this stuff as “chunks” - a grouping of tiles that can be loaded and unloaded depending on where players are, and save on CPU cycles keeping them updated in your game.

I wanted my islands to align themselves to the nearest chunk boundary, just to avoid any extra complications. To do this, I just wait for the simulation to sleep, then call a function on every body’s position to put them at the nearest chunkSize divisible position.

Here is the code I use:

local function round(n)
  -- Kinda wild that lua doesn't have math.round
  return math.floor(n + 0.5)
end

function love.update(dt)
  world:update(dt)

  if allSleeping(world:getBodies()) then
    sleeping = true
    for i, body in ipairs(world:getBodies()) do
      local bx, by = body:getPosition()
      body:setPosition(
        round(bx / CHUNK_SIZE) * CHUNK_SIZE,
        round(by / CHUNK_SIZE) * CHUNK_SIZE
        )
    end
  end
end

Noise Generation

After all this, we now have a spread of boxes, but they don’t make very good islands. The stock standard way to do this is with noise functions.

I think this subject has been done to death, and I’d probably butcher it, so instead I’d recommend taking a look at Sebastian Lague on Youtube. He has a tutorial series on noise generation in Unity, but the concepts can easily be brought over to Love2d.

Helpfully, Love does contain its own noise implementation, love.math.noise, so the following is my bog standard implementation of a layered noise function. We’ll put it in the skeleton for generateNoise we made earlier

I’ve seeded the values for octaves, persistence and scale as per what’ll look nice here, but play around with them in your own code to your liking.

local octaves = 8
local persistence = 0.5
local scale = 0.01

local function generateNoise(x, y)
  local sum = 0
  local amplitude = 1
  local frequency = scale

  local noise = 0
  for i = 1, octaves do
    noise = noise + love.math.noise(x * frequency, y * frequency) * amplitude
    sum = sum + amplitude
    amplitude = amplitude * persistence
    frequency = frequency * 2
  end

  return noise / sum
end

On the wiki page for love.math.noise it warns us that passing integer values will return the same results, but in our example, since frequency is less than one we’ll get that. You may want to add a little bit of random variation to the values you pass in, or just a + 0.1 if you wish.

Applying the noise

We’ll now want to use this generateNoise function on each of our islands, and generate a height map to use when it comes to drawing them.

For simplicity, I’m going to create a new table called islandHeightMaps, but in a game you might apply this noise to an underlying chunk implementation, or some global heightMap - depends on your game.

We’ll build out our generateHeightMaps function.

local islandHeightMaps = {}

local function generateHeightMaps()
  for i, physObj in ipairs(physicsObjects) do
    local size = physObj.size
    local result = {}
    for y = 0, size do
      if not result[y] then
        result[y] = {}
      end
      for x = 0, size do
        result[y][x] = generateNoise(x, y)
      end
    end

    table.insert(islandHeightMaps, result)
  end
end

Then call it when we find out if the simulation is sleeping:

  if allSleeping(world:getBodies()) then
    sleeping = true
    for i, body in ipairs(world:getBodies()) do
      local bx, by = body:getPosition()
      body:setPosition(
        round(bx / CHUNK_SIZE) * CHUNK_SIZE,
        round(by / CHUNK_SIZE) * CHUNK_SIZE
        )
    end
    -- We could spin this into the previous snapping loop, but for the sake
    -- of separation/explaining, we'll just loop over it again.
    generateHeightMaps()
  end
end

And so we can see whats happening, lets draw them in our love.draw function.

function love.draw()
  for i, physObj in ipairs(physicsObjects) do
    local bx, by = physObj.body:getPosition()
    local x = bx - physObj.size / 2
    local y = by - physObj.size / 2

    if not sleeping then
        love.graphics.setColor(
          physObj.chunkSize / XL,
          (physObj.chunkSize / XL) / 2,
          physObj.chunkSize / XL
          )
    love.graphics.rectangle(
      "fill", x, y, physObj.size, physObj.size
      )
    else
      local heightMap = islandHeightMaps[i]
      for hy = 0, physObj.size do
        for hx = 0, physObj.size do
          local height = heightMap[hy][hx]
          love.graphics.setColor(height, height, height)
          love.graphics.points(hx + x, hy + y)
        end
      end
    end
  end
end

Oh, err - that kinda looks all the same, don’t it? That would be because we’re passing it the same values every run. There are a ton of ways to fix this, but the easiest might be just setting a random x and y offset value, and passing that into generateNoise:

-- in generateHeightMaps:
local xOffset = love.math.random(-1024, 1024)
local yOffset = love.math.random(-1024, 1024)

for y = 0, size do
  if not result[y] then
    result[y] = {}
  end
  for x = 0, size do
    result[y][x] = generateNoise(x + xOffset, y + yOffset)
  end
end

That looks a little more varied. Lets add a little colour. This function will take in a height value, and return a colour that denotes a land feature.

local colours = {
  { 0.2, 0.46, 0.08, 1 }, -- deep water
  { 0.14, 0.28, 0.53 }, -- shallow water
  { 0.25, 0.39, 0.65, 1 }, -- beach
  { 0.96, 0.95, 0.65, 1 }, -- grass
}

local function determineColour(height)
  if height <= 0.20 then
    return colours[2]
  elseif height > 0.2 and height <= 0.3 then
    return colours[3]
  elseif height > 0.3 and height < 0.33 then
    return colours[4]
  else
    return colours[1]
  end
end

And where we draw the height, we change the code to this:

local height = heightMap[hy][hx]
love.graphics.setColor(determineColour(height))
love.graphics.points(hx + x, hy + y)

Which will give us something like this:

Falloff

These boxes still don’t quite look like islands, rather one great big square landmass. What we want to do is influence the height calculation to give us a smaller number the closer we’re to the edge of the island.

A simple approach is to just do it by the distance from the center of the island, which could work!

Lets define a stock standard distance function we’ll use in generateHeightMaps to apply a falloff to our islands.

The gist here is, we want to find out how far away our point is from the center of the island. We’ll convert it into a percentage, then multiply our noise by the inverse of that value: (1 - percentage)

Here’s the code:

local function distance(x1, y1, x2, y2)
  local dx = x2 - x1
  local dy = y2 - y1
  return math.sqrt(dx * dx + dy * dy)
end

local function generateHeightMaps()
  for i, physObj in ipairs(physicsObjects) do
    local size = physObj.size
    local result = {}
    local xOffset = love.math.random(-1024, 1024)
    local yOffset = love.math.random(-1024, 1024)
    for y = 0, size do
      if not result[y] then
        result[y] = {}
      end
      for x = 0, size do
        local noise = generateNoise(x + xOffset, y + yOffset)
        local dist = distance(size / 2, size / 2, x, y) / (size / 2)
        result[y][x] = noise * (1 - dist)
      end
    end
    table.insert(islandHeightMaps, result)
  end
end

Bada bing, bada boom:

You can make it look like they’re in a large ocean by putting a love.graphics.clear(colours[2]) at the top of love.draw

Those sure look like islands to me!

Different Falloff

Using distance definitely works! But it does often end up in pretty rounded island shapes, and quite a ways from the edge of our island. There are a couple ways to fix this (but we won’t go over in this post)

I often like option number 3

Conclusion

In this post I’ve described a way to generate islands using box2d and Love. You can find the full code as a gist here.

The approach is pretty naive, but a fun way to explore all the little bits of procedural generation in a pretty dumb manner that’s easy to understand and implement.

Ultimately I didn’t end up using this method for my world generation, instead I opted for various the random points method to allow for more interesting islands. Becoming pedantic over island budgets didn’t really work out from a game design perspective, but that’s a story for another time.

Here’s an early output of a method I was testing out:

But thanks for reading! Hopefully I can share more dumb programming tricks in the future!