Birdoggydog's Builds

Dressing dungeon rooms with generated furniture in Godot

Eleven furniture pieces built from numbers in Blender, and a placer that reads a room's note and never blocks the party or a monster.

I built two things. The first is a furniture kit: a desk, chair, bunk, stacked bunk, locker, lab bench, book shelf, store shelf, counter, table and cot, each made from numbers in four themes and four states of decay. The second is a rule called DungeonDresser that reads what kind of room a room is from the note I wrote in it on the dungeon’s map, and puts furniture against its walls without cutting any floor off from the party or from a monster.

58 seconds, captioned, of the dresser at work.

On the Breakout Site’s first floor it gives 15 of 28 rooms a kind and places 123 pieces. But none of it is installed in the game yet. My model pipeline has a step that asks which existing model a new one replaces, and it refuses the furniture until I say the family replaces nothing. Once I answer that and install the models, nothing needs re-importing, because the rule runs when the dungeon is built.

Why furniture?

The rooms were empty boxes, and the barrel was the only themed prop in the game. My notes on the map already say what the rooms are: “This is the head scientists office. Desk chairs etc….”, “Scientist bunks”, “Kitchen”, “Library”, “The 5 similar rooms here are rooms for experiment subjects.” I wanted those notes to be enough.

Building eleven pieces from a table

Each piece is a spec over a table of defaults plus its own kind’s numbers. One function per kind builds it from boxes and lofted sections, and every part is a child of the part it sits on. A variant is data. tiers: 2 is the stacked bunk, load: "boxes" is the store shelf, and tipped and pulled give a tipped chair and pulled-out drawers.

Eleven pieces of furniture in a row, repeated in four rows. The top row is clean white and black with cyan lit strips. The second and third rows are increasingly stained and broken. The bottom row is dark rubble.
The cyberpunk theme. Each column is one piece, and the rows go from new at the top to dead at the bottom.

Each model goes through the same theme pipeline the barrel uses, which assigns materials by role and makes four states: peak, declining, decayed and dead. Edges are chamfered 1.2 cm where a character would touch them, and each piece has one 256 px bake. The total is 176 variants, or 185 with nine alternative builds, at 140 to 976 triangles each. The target for this class is 2,000 triangles and the cap is 5,000.

I compared one variable at a time on review sheets:

  • I couldn’t tell a hard edge, a 1.2 cm bevel and a 3 cm bevel apart at 12 m, and at 5 m only the 3 cm one showed.
  • A 256 px bake and a 512 px bake looked alike everywhere. The theme’s shared swatches passed on new pieces and failed on worn ones, so each piece gets a bake.
  • Missing drawers and a lean look worn, so a state is data and not a second shape. The one exception is a dead chair, which is rubble unless it’s built tipped.
  • A chair made of parts is 218 triangles and a chair as one shell is 26. The parts looked like a chair at 5 and 12 m and the shell didn’t.
Eight views of a bunk in a small white cell with an orange block figure beside it. The top row is in bright light and the bottom row by a lamp, each at close range, 2 m, 5 m and 12 m.
The cyberpunk bunk, 308 triangles, close up and at 2, 5 and 12 m in both lights.

Removing the faces nobody sees

Furniture has a lot of faces the player can’t see: backs against the wall, undersides, and whatever is inside a cabinet. A pass removes them. A face is kept if a ray from it reaches an eye that’s 1.0 to 1.9 m up and 0.4 to 14 m away, and rings of eyes put back any face they can see. Nothing is collapsed.

That took out 36% of the triangles, from 120,744 built to 77,320.

PieceBuiltUnseenAfter
desk392136256
chair26850218
bunk484176308
bunk, stacked748255493
locker43288344
bench1012261751
shelf1592709883
store1000353647
counter744169575
table32062258
cot30080220

How does a note become a kind of room?

Everything the dresser knows is in one file, content/dressing.json, so a new kind, word or piece is a new row and not new code.

words is a list of rows, each with a kind and its words. A note’s text is lowercased and cut down to letters. The rows are walked in order, and the first row with a whole word from the note wins. A row with no kind means the room gets nothing, which is how bathrooms, the gym, security and the vat chambers stay empty. There are 10 kinds: office, lab, bunks, kitchen, library, cell, rec, meeting, staging and storage.

A note with a word like “similar”, “identical” or “each” also applies to every room of the same size that has no note. That’s how five cells in a row share one note. A corridor, a room with no note, or a note with no known word gets nothing.

Each kind has a fill (the share of floor its furniture can cover), a most (a piece count) and a lay list of pieces that are placed in turn, and again from the top, until the room is full. An entry can have a with and a count, so a desk comes with a chair and a table with four. States are drawn by weight: peak 2, declining 5, decayed 2. Every one of those numbers is a first guess.

Placing pieces on a half-meter grid

The room and the squares around it are covered by a grid of half-meter cells. Each cell has three flags: solid, kept clear, and taken.

A wall piece can go anywhere along the four walls where its box has rock behind it the whole way, with a 12 cm gap. 80% of the time it prefers a place where one end meets a corner or another piece, which makes the room look tidy. A free-standing piece can go at spots a meter apart with 1.5 m clear around it. A seat goes at its host’s front, facing it.

Some squares are off limits to every piece: any square a way in opens onto (so a door’s square and the one before it), any square the map put a thing on, and the squares around a pack of monsters. Up to 6 places are tried for a piece before the dresser gives up on it. The layout is seeded from a hash of the dungeon’s id and the room’s id, so the same map always gives the same rooms.

Making sure nothing gets cut off

My rule for the game is that monsters can follow you anywhere they can fit. Furniture can’t break that, so the reach check is made per body width.

Before anything is placed, for the party (0.8 m wide) and for every monster that can spawn, the dresser computes the grid’s clearance. For each cell, that’s the side of the largest free square starting there, worked out with a dynamic program from the far corner. A body N cells wide can be anywhere the clearance is at least N. A flood fill over those cells groups the floor into parts, and each part has the set of squares it reaches.

After each piece is placed, the same thing is computed again. The piece is kept only if every old part is still contained in some new part. If not, the cells are freed and another place is tried. A body is treated as a square block of whole cells, never narrower than it really is.

The same function, keeps_reach, answers the question for any set of boxes. The test uses it on a finished layout, and on a box put across a doorway, where it has to say no.

Putting props in the importer

ImportedDungeon._build_dressing() adds the pieces as ThemedProp nodes, and only the ones whose models are built and installed. With none installed, the game is exactly as it was.

ThemedProp got a furniture mode. Its old collider was a cylinder of at most 0.75 m radius, which is wrong for a 2.4 m counter, so furniture gets a box collider from its footprint. Furniture also has no light and can’t be interacted with. The barrel hasn’t changed.

The dresser’s test has 69 checks covering the words (using the notes I actually wrote), two five by five rooms on either side of a door, nothing placed on a way in, no floor cut off for the party or the widest monster, and the Breakout Site’s rooms. With the dungeon, model pipeline, model gate and themed prop tests that’s 12 tests, and none failed. I didn’t run the whole suite.

What’s still wrong?

  • I haven’t seen any of it in the game, because nothing is installed.
  • The dresser places no dead pieces. A dead piece’s footprint is bigger than its spec’s. The steampunk theme’s death also tips a part up on an edge, which suits a barrel, but a bunk, counter or shelf ends up on its corner.
  • The theme pipeline crashes when something riding on a broken part has its own rider. It removes a rider, which removes that rider’s riders, and then reads one of them. The kit keeps fittings off shell parts to avoid it. I know the one-line fix and haven’t applied it yet.
  • The chair at 12 m is a pale square over a dark seat.
  • Fantasy and eldritch give most pieces no lit surface.
  • Two build commands run at the same time destroy each other’s files.