Carving out caverns¶
In this section of the tutorial we’ll create a more interesting place to kick kobolds, and put some in the game world to start.
Getting started on a map¶
Let’s create a new file in the root of the project called levelgen.lua
. We’re gonna want to
return a function from this module that takes a few parameters.
--- @param rng RNG
--- @param player Actor
--- @param width integer
--- @param height integer
return function(rng, player, width, height)
local builder = prism.MapBuilder(prism.cells.Wall)
return builder
end
We give the level building function an RNG which will be exclusive to it, the player we want to place, and the width and height of the map we want generated. Inside we create a MapBuilder and return it.
Populating the void¶
The first step is going to be filling the void with a width * height initialization of pits and walls. To decide where to put what we’re going to use perlin noise.
-- Fill the map with random noise of pits and walls.
local nox, noy = rng:getUniformInt(1, 10000), rng:getUniformInt(1, 10000)
for x = 1, width do
for y = 1, height do
local noise = love.math.perlinNoise(x / 5 + nox, y / 5 + noy)
local cell = noise > 0.5 and prism.cells.Wall or prism.cells.Pit
builder:set(x, y, cell())
end
end
First we generate an offset for the noise using our RNG. This ensures every map gets a unique pattern of pits and walls.
Then we loop through each tile in the map, grab our result from the perlinNoise which we scale a little bit, and finally check if the noise is > 0.5. If it is we put a wall there, and if it’s not we place a pit.
Making room¶
-- Create rooms in each of the partitions.
--- @type table<number, Rectangle>
local rooms = {}
local missing = prism.Vector2(rng:getUniformInt(0, PARTITIONS - 1), rng:getUniformInt(0, PARTITIONS - 1))
local pw, ph = math.floor(width / PARTITIONS), math.floor(height / PARTITIONS)
local minrw, minrh = math.floor(pw / 3), math.floor(ph / 3)
local maxrw, maxrh = pw - 2, ph - 2 -- Subtract 2 to ensure there's a margin.
for px = 0, PARTITIONS - 1 do
for py = 0, PARTITIONS - 1 do
if missing ~= prism.Vector2(px, py) then
local rw, rh = rng:getUniformInt(minrw, maxrw), rng:getUniformInt(minrh, maxrh)
local x = rng:getUniformInt(px * pw + 1, (px + 1) * pw - rw - 1)
local y = rng:getUniformInt(py * ph + 1, (py + 1) * ph - rh - 1)
local roomRect = prism.Rectangle(x, y, rw, rh)
rooms[prism.Vector2._hash(px, py)] = roomRect
builder:drawRectangle(x, y, x + rw, y + rh, prism.cells.Floor)
end
end
end
Okay so this is a little chunky, but we’re gonna break it down. The first thing we do is decide which one of the partitions we’re going to omit a room in.
local missing = prism.Vector2(rng:getUniformInt(0, PARTITIONS - 1), rng:getUniformInt(0, PARTITIONS - 1))
Then we’re going to calculate the total width and height of our patitions.
local pw, ph = math.floor(width / PARTITIONS), math.floor(height / PARTITIONS)
After that let’s set some reasonable limits on the minimum and maximum room width and height.
local minrw, minrh = math.floor(pw / 3), math.floor(ph / 3)
local maxrw, maxrh = pw - 2, ph - 2 -- Subtract 2 to ensure there's a margin.
Next we loop through each of our partitions and build a room so long as it’s not the one we’re omitting. We build our room rectangle, hash it’s partition coordinates, and put it into a dictionary of rooms. Finally we draw the room onto our map.
for px = 0, PARTITIONS - 1 do
for py = 0, PARTITIONS - 1 do
if missing ~= prism.Vector2(px, py) then
local rw, rh = rng:getUniformInt(minrw, maxrw), rng:getUniformInt(minrh, maxrh)
local x = rng:getUniformInt(px * pw + 1, (px + 1) * pw - rw - 1)
local y = rng:getUniformInt(py * ph + 1, (py + 1) * ph - rh - 1)
local roomRect = prism.Rectangle(x, y, rw, rh)
rooms[prism.Vector2._hash(px, py)] = roomRect
builder:drawRectangle(x, y, x + rw, y + rh, prism.cells.Floor)
end
end
end
Carving hallways¶
Next we’re gonna create a function local function (yes, that’s correct) to draw the classic Rogue style L shaped hallways between rooms. It accepts two Rectangles representing the rooms, and if both a and b exist we draw a hallway between them. We use the level generator’s RNG to figure out if we should start vertically or horizontally for a little bit of spice.
-- Helper function to connect two points with an L-shaped hallway.
--- @param a Rectangle
--- @param b Rectangle
local function createLShapedHallway(a, b)
if not a or not b then return end
local ax, ay = a:center():floor():decompose()
local bx, by = b:center():floor():decompose()
-- Randomly choose one of two L-shaped tunnel patterns for variety.
if rng:getUniform() > 0.5 then
builder:drawLine(ax, ay, bx, ay, prism.cells.Floor)
builder:drawLine(bx, ay, bx, by, prism.cells.Floor)
else
builder:drawLine(ax, ay, ax, by, prism.cells.Floor)
builder:drawLine(ax, by, bx, by, prism.cells.Floor)
end
end
Now that we’ve got this little helper function done let’s go through each room and try to connect it to the one to the right, and the one to the bottom. If either doesn’t exist the hallway helper won’t get past the guard and nothing will happen.
for hash, currentRoom in pairs(rooms) do
local px, py = prism.Vector2._unhash(hash)
createLShapedHallway(currentRoom, rooms[prism.Vector2._hash(px + 1, py)])
createLShapedHallway(currentRoom, rooms[prism.Vector2._hash(px, py + 1)])
end
Spawning people¶
Now to place the player. We’ll select a random room and put the player on the center tile.
-- Choose the first room (top-left partition) to place the player.
local startRoom
while not startRoom do
local x, y = rng:getUniformInt(0, PARTITIONS - 1), rng:getUniformInt(0, PARTITIONS - 1)
startRoom = rooms[prism.Vector2._hash(x, y)]
end
local playerPos = startRoom:center():floor()
builder:addActor(player, playerPos.x, playerPos.y)
We’re getting close now, but we need some kobolds to kick. Let’s go through every room that’s not the starting room and spawn a kobold there.
for _, room in pairs(rooms) do
if room ~= startRoom then
local cx, cy = room:center():floor():decompose()
builder:addActor(prism.actors.Kobold(), cx, cy)
end
end
Okay, we’ve got the player and the kobolds spawning. Time to wrap things up.
Sending it back¶
builder:addPadding(1, prism.cells.Wall)
return builder
Finally we’ll wrap the entire map in some walls and return the finished MapBuilder.
Complete levelgen.lua
local PARTITIONS = 3
--- @param rng RNG
--- @param player Actor
--- @param width integer
--- @param height integer
return function(rng, player, width, height)
local builder = prism.MapBuilder(prism.cells.Wall)
-- Fill the map with random noise of pits and walls.
local nox, noy = rng:getUniformInt(1, 10000), rng:getUniformInt(1, 10000)
for x = 1, width do
for y = 1, height do
local noise = love.math.perlinNoise(x / 5 + nox, y / 5 + noy)
local cell = noise > 0.5 and prism.cells.Wall or prism.cells.Pit
builder:set(x, y, cell())
end
end
-- Create rooms in each of the partitions.
--- @type table<number, Rectangle>
local rooms = {}
local missing = prism.Vector2(rng:getUniformInt(0, PARTITIONS - 1), rng:getUniformInt(0, PARTITIONS - 1))
local pw, ph = math.floor(width / PARTITIONS), math.floor(height / PARTITIONS)
local minrw, minrh = math.floor(pw / 3), math.floor(ph / 3)
local maxrw, maxrh = pw - 2, ph - 2 -- Subtract 2 to ensure there's a margin.
for px = 0, PARTITIONS - 1 do
for py = 0, PARTITIONS - 1 do
if missing ~= prism.Vector2(px, py) then
local rw, rh = rng:getUniformInt(minrw, maxrw), rng:getUniformInt(minrh, maxrh)
local x = rng:getUniformInt(px * pw + 1, (px + 1) * pw - rw - 1)
local y = rng:getUniformInt(py * ph + 1, (py + 1) * ph - rh - 1)
local roomRect = prism.Rectangle(x, y, rw, rh)
rooms[prism.Vector2._hash(px, py)] = roomRect
builder:drawRectangle(x, y, x + rw, y + rh, prism.cells.Floor)
end
end
end
-- Helper function to connect two points with an L-shaped hallway.
--- @param a Rectangle
--- @param b Rectangle
local function createLShapedHallway(a, b)
if not a or not b then return end
local ax, ay = a:center():floor():decompose()
local bx, by = b:center():floor():decompose()
-- Randomly choose one of two L-shaped tunnel patterns for variety.
if rng:getUniform() > 0.5 then
builder:drawLine(ax, ay, bx, ay, prism.cells.Floor)
builder:drawLine(bx, ay, bx, by, prism.cells.Floor)
else
builder:drawLine(ax, ay, ax, by, prism.cells.Floor)
builder:drawLine(ax, by, bx, by, prism.cells.Floor)
end
end
for hash, currentRoom in pairs(rooms) do
local px, py = prism.Vector2._unhash(hash)
createLShapedHallway(currentRoom, rooms[prism.Vector2._hash(px + 1, py)])
createLShapedHallway(currentRoom, rooms[prism.Vector2._hash(px, py + 1)])
end
-- Choose the first room (top-left partition) to place the player.
local startRoom
while not startRoom do
local x, y = rng:getUniformInt(0, PARTITIONS - 1), rng:getUniformInt(0, PARTITIONS - 1)
startRoom = rooms[prism.Vector2._hash(x, y)]
end
local playerPos = startRoom:center():floor()
builder:addActor(player, playerPos.x, playerPos.y)
for _, room in pairs(rooms) do
if room ~= startRoom then
local cx, cy = room:center():floor():decompose()
builder:addActor(prism.actors.Kobold(), cx, cy)
end
end
builder:addPadding(1, prism.cells.Wall)
return builder
end
Updating MyGameLevelState¶
Head back to gamestates/MyGamelevelstate.lua
and add the following line to the top of the file.
local levelgen = require "levelgen"
Then we’re going to change it’s constructor. Head to MyGameLevelState:__new
and let’s replace the map
builder code there with this:
local seed = tostring(os.time())
local mapbuilder = levelgen(prism.RNG(seed), prism.actors.Player(), 60, 30)
Now run the game! You’ll be exploring a totally sick classic Rogue style map with some caverns and pits all around the room!
Descending to the next part¶
In the next section of the tutorial we’ll add a set of stairs and let the player descend deeper into the dungeon!