Your generator places ten rooms, and three of them land on top of each other. Or they don't quite overlap, but two sit wall to wall with no rock between them, so in game they read as one strange L-shaped room. Both come from the same missing rule.
This is part 2 of a series that builds one dungeon generator, pass by pass. Here is the room pass.
Why overlap happens at all
A room generator rolls a size and a position, then carves floor. Nothing in "roll a random rectangle" knows about the rectangles rolled before it. With 11 rooms on a 22 by 22 grid, collisions are not rare bad luck. They are most of the rolls.
So the fix is not to roll more carefully. It is to check, and to be willing to throw a roll away.
The test: a box around the room
Two rectangles overlap when they share area. Godot's Rect2i.intersects() answers exactly that, and its documentation is clear on one detail: "The edges of both rectangles are excluded." Rectangles that only touch along an edge do not count as intersecting.
That detail is why a plain overlap test is not enough. Two rooms that touch pass it, and you get a double room with no wall between the halves. So you test a bigger box instead. Grow the new room by one cell on every side, and test the grown box against the rooms already there. If the grown box hits a room, the real room would have touched it.
The demo below steps through every rectangle the generator tried on seed 214.
Every room the generator tried
Interactive, engine-accurateTry 66: a 3 by 4 room at (9, 6). Its box grown by 1 overlaps room 1, so it is thrown away. Finished: 9 rooms kept, 57 tries rejected.
A solid outline is the room the generator rolled. The dotted outline around it is the same box grown by the padding, and that grown box is what gets tested. Green means it fit. Red and dashed means it hit the room shown in red, so it was thrown away. Press Reset, then Play to watch it fill up. With No gap, rooms can sit wall to wall and merge into one blob.
This runs the GDScript from this article, ported to the browser. The same seed gives the same result in Godot 4.7.2.
A worked example: seed 214, try by try
On seed 214 the generator is allowed 66 tries and keeps 9 rooms. Here are the first five tries:
- A 5 by 3 room at (3, 9). Nothing exists yet, so it is kept as room 0.
- A 3 by 6 room at (10, 2). Its grown box touches nothing, so it becomes room 1.
- A 4 by 6 room at (12, 11). Kept as room 2.
- A 6 by 5 room at (5, 3). Its grown box overlaps room 1, so it is thrown away.
- A 5 by 4 room at (10, 17). Its grown box overlaps room 2. Thrown away.
The pattern holds for the rest of the run. By the end, 57 of the 66 tries have been thrown away. Across seeds 1 to 500 the average is 58 rejections, and the generator ends with 7.85 rooms on average, never fewer than 5 and never more than 11. Throwing most rolls away is how a simple generator spreads rooms out, not a sign that something is broken.
How much padding?
Padding is a design choice, and the demo lets you try three settings on the same seed:
| Padding | Rooms kept on seed 214 | Tries thrown away | What it looks like |
|---|---|---|---|
| No gap | 11 | 37 | Rooms can sit wall to wall and merge |
| 1 cell | 9 | 57 | One cell of rock between rooms, room for a wall |
| 2 cells | 7 | 59 | Spacious, fewer rooms, longer corridors |
One cell is the smallest gap that still leaves space for a wall tile. Two cells leaves space for a wall on each side, which matters if your walls are thick or your art needs a margin. The 3D dungeon course uses one cell, and its lesson gives the same reason: the gap leaves room for walls and rock.
Where it still goes wrong
The map is too crowded for the rooms you want. Seed 75 keeps only 5 rooms, after 61 rejected tries. If your design needs at least 6, you need a rule for that. Retry the whole floor with the next seed, shrink the room sizes, or grow the map. Do not loop until it fits: a map that cannot fit 11 rooms will loop forever. The try cap, 6 tries per room wanted, is what stops that.
You checked the room, but not the corridor. Rooms that never overlap can still be joined by corridors that run through other rooms. That is fine for floor, but it can carve a doorway into a room you meant to keep closed. Check corridors against rooms too if your design needs closed rooms.
You used physics to test the overlap. A common Godot approach is to give each room an Area2D and ask it for overlapping areas right after placing it. The list is empty, every time. The docs explain why: the overlap list "is modified once during the physics step, not immediately after objects are moved". Rectangles in plain data have no such delay, which is one more reason to plan the layout as data and build nodes afterwards.
Big rooms crowd out small ones. Rooms are placed in the order they are rolled, so a few large early rooms can leave no space later. If you need a mix of sizes, place the large rooms first on purpose, then fill in.
Other ways to keep rooms apart
- Split the space first. Binary space partitioning cuts the map into pieces and puts one room in each, so rooms cannot overlap by construction. See RogueBasin's BSP walkthrough.
- Push them apart. TinyKeep's well-known generator scatters overlapping rooms, then nudges them apart until none overlap, as A Adonaac's write-up on Game Developer describes. It packs rooms tighter, at the cost of a slower, fiddlier step.
- Use slots. Lay out a grid of room slots and put at most one room in each.
Rejection, the method on this page, is the simplest of the four and the easiest to debug, which makes it the right first generator.
In Godot: grow() and intersects()
Rect2i is Godot's rectangle on a grid of whole numbers: a position and a size. The room pass rolls one, tests it, and either keeps it or moves on:
func place_rooms() -> void: for _attempt in max_rooms * 6: if rooms.size() >= max_rooms: break var w := rng.randi_range(room_min, room_max) var h := rng.randi_range(room_min, room_max) var x := rng.randi_range(1, grid_width - w - 1) var y := rng.randi_range(1, grid_height - h - 1) var rect := Rect2i(x, y, w, h) if overlaps_any(rect): continue # rejected, roll a new rectangle rooms.append(rect) carve_room(rect)The position is rolled between 1 and grid_width - w - 1, so every room keeps one cell of rock between itself and the edge of the map, too. The test itself is three lines:
func overlaps_any(rect: Rect2i) -> bool: for other in rooms: if rect.grow(padding).intersects(other): return true return falsegrow(padding) returns a copy of the rectangle, bigger by padding cells on every side. It does not change rect, so the room you keep is still the size you rolled. Carving the room is a double loop over its cells. rect.end is the corner just past the bottom right, so range() stops in the right place:
func carve_room(rect: Rect2i) -> void: for y in range(rect.position.y, rect.end.y): for x in range(rect.position.x, rect.end.x): set_floor(Vector2i(x, y))The complete generator is dungeon_layout.gd.
Build it next
Procedural Dungeon & Run
A quest on CodingQuests: 12 interactive lessons that build one part of a game in Godot, in your browser, checked as you go.
Lesson 2 of Procedural Dungeon & Run is this pass, in a 3D game. You carve rooms and corridors into a grid, and one exercise hands you a generator with the overlap bug. Your job is to find it and fix it with rect.grow(1).intersects(r), the same test as above.
The course does not set a try cap or a minimum room count in that lesson. Add both from this article if your design needs them.
- Lesson 2: Rooms & CorridorsFree
Lessons 1 to 3 are free to open, no account needed. Guild Hall, the membership, opens the other 9.
FAQ
How do I check if two rooms overlap in Godot?
Store each room as a Rect2i and call a.intersects(b). It returns true only when the rooms share area. To also stop rooms touching, test a grown copy instead: a.grow(1).intersects(b).
Why are my procedural rooms touching even though I check for overlap?
Because Rect2i.intersects() ignores shared edges, so two rooms that only touch do not count as overlapping. Grow the new room by one cell before you test it. That leaves at least one cell of rock between rooms, which is where the wall goes.
Why does my Area2D overlap check say nothing overlaps?
Area2D overlap lists only update during the physics step, not the moment you move or add a node. Checking straight after placing a room always sees the old, empty list. Test plain rectangles instead, before you create any nodes.
Sources and testing
- Godot docs: Rect2i (grow, intersects, end) and Area2D (overlap lists update during the physics step)
- RogueBasin, Basic BSP Dungeon generation
- A Adonaac, Procedural Dungeon Generation Algorithm, Game Developer (2015)
- Developer questions this answers: Procedural Dungeon Generation (Detect Room Intersection) and Ways to make a procedural dungeon on the Godot Forum
How this was tested. The GDScript on this page was run in Godot 4.7.2 (headless, on Windows) on 31 October 2026. The demo runs a browser port of the same script, checked against Godot's output on 40 seeds. The averages for seeds 1 to 500 come from that port.

