You have built twelve rooms by hand, as Godot scenes: a throne room, a library, a few corridors, a dead end with a chest. Each has doorways. Now you want the game to stitch them into a different floor every run. You instance a room next to another and the doorway lands in the middle of a wall, or the room arrives rotated a quarter turn the wrong way.
Premade rooms trade one problem for another. You no longer worry about rooms overlapping as much as about doors meeting. This is part 4 of a series on dungeon generation, and the one that leaves rectangles behind.
Doors are sockets
Think of each doorway as a socket: a point on the edge of a room, plus the direction it opens towards. Two rooms connect when an open socket on one meets a socket on the other that faces exactly the opposite way. An east door meets a west door. A north door meets a south door.
That gives you two rules to satisfy, in order:
- Facing. The new room needs a door that points back at the open one. If rooms can turn, you can usually make one. If they cannot, the room must already have a door on the right side.
- Space. Once lined up, the room must not overlap anything already placed.
Try both rules by hand first. The Hall's open door faces east. Turn the Bend and see which turns let it attach.
Turn the Bend until a door faces back
Interactive modelTurned 0 degrees, the Bend has a door facing west. Slide the Bend so that door sits just outside the Hall's door, and it fits. Attached.
Triangles are doors, pointing out of their room. The orange one on the Hall is the open door the generator is trying to fill. Green outline: the Bend attaches. Red dashed: it was turned the right way but lands on the Vault. Two of the four turns fail before position even matters.
A simplified model of the idea, for seeing how it behaves. It is not engine code.
At 0 degrees the Bend's west door faces the Hall, and it fits. At 90 degrees its south door has turned to face west, so it lines up too, but it would sit on top of the Vault. At 180 and 270 degrees no door faces west at all. Two of the four turns fail before position even matters.
Growing a whole floor
A full generator repeats that one move. Keep a list of open doors. Each try, pick one, pick a room template, pick a turn, and run both rules. A room that passes is placed, its door is removed from the open list, and its other doors are added. Stop at the number of rooms you want, or when you run out of tries.
A floor grown from premade rooms
Interactive modelTry 11: a Vault turned 90 degrees on the south door fits, so it is placed. Done: 8 rooms. 3 doors left open get walled up.
Four templates, marked by their first letter: H for Hall (four doors), C for Corridor (two), B for Bend (two, at a corner) and V for Vault (one, a dead end). Green bars are joined doors. Orange triangles are still open. Dark bars at the end are doors walled up because nothing was attached to them. Switch to No turning and far more tries fail, because a room can only attach where it already has a door facing the right way.
A simplified model of the idea, for seeing how it behaves. It is not engine code.
On seed 214, with rooms allowed to turn, 7 of 11 tries attach and the floor reaches 8 rooms. Three doors are left open and get walled up. Switch to No turning and the same seed only manages 5 rooms in 40 tries.
Can your rooms turn?
That switch is the biggest design question with premade rooms, and the answer comes from your art, not your code.
- Top-down rooms with even lighting can usually turn freely. Any door can be made to face any way.
- Side-view rooms cannot turn: floors stay floors. Neither can top-down art with a light direction painted in, or rooms drawn in perspective.
If rooms cannot turn, every open door needs a template with a door on the opposite side. Among the rooms the demo can attach, none has a door on its east side, so a west-facing open door can never be filled. Without turning, no seed from 1 to 200 reached 8 rooms. The fix is in your room library, not your generator: make sure every direction you build from has rooms that can answer it.
Where it goes wrong
Running out of doors. Seed 11 stops at 5 rooms with no open doors left, because dead ends like the Vault closed every branch. Place dead ends late, give them a lower chance than corridors, or keep trying from the oldest open door.
Doors drawn inconsistently. The whole method rests on one rule: every door marker points out of its room. One marker drawn pointing in, and that room attaches backwards. Check it in the editor: the red X axis of a Marker2D shows its facing.
Reading positions too early. A room scene you just instanced, but have not added to the tree, does not refresh its children's global positions when you move or turn it. In a test in Godot 4.7.2, a door kept its old global position after its room was turned a quarter turn. Add the room to the tree first, then align it.
Checking overlap with physics. An Area2D on each room looks like the natural overlap test. But the docs say its overlap list "is modified once during the physics step, not immediately after objects are moved". A room placed this frame always looks clear. Compare plain rectangles instead.
Forgetting the rooms behind walls. Walled-up doors are fine. A whole wing behind a door you walled by mistake is not. Run the reachability check from part 3 on the finished floor.
In Godot: attach, check, keep or discard
Each room is a Node2D scene with a Marker2D child per door. A Marker2D is a plain point in 2D space that the editor draws as a cross, so you can see and rotate it. The functions below expect the door markers to be direct children of the room's root node.
Attaching is two lines. Turn the room so the door's direction is the target's plus a half turn, then move it by the gap between the two doors:
func attach(new_room: Node2D, door: Marker2D, target_door: Marker2D) -> void: new_room.global_rotation = target_door.global_rotation + PI - door.rotation new_room.global_position += target_door.global_position - door.global_positionPI is half a turn in radians, which is what Godot measures rotation in. door.rotation is the door's angle inside its own room, so subtracting it turns the room just enough for that door to face the right way.
For rooms that must not turn, find the doors that already face back instead:
func doors_facing(new_room: Node2D, target_door: Marker2D) -> Array[Marker2D]: var wanted := target_door.global_rotation + PI var out: Array[Marker2D] = [] for child in new_room.get_children(): if child is Marker2D and is_zero_approx(wrapf(child.global_rotation - wanted, -PI, PI)): out.append(child) return outwrapf() folds an angle into the range minus half a turn to plus half a turn, so 360 degrees and 0 degrees compare as equal. is_zero_approx() allows for the tiny rounding errors angles pick up.
To check space, turn each room's rectangle into world space. This handles quarter turns, which is all a grid of rooms needs:
func world_bounds(room: Node2D, bounds: Rect2) -> Rect2: var xf := room.global_transform var corners: Array[Vector2] = [ xf * bounds.position, xf * Vector2(bounds.end.x, bounds.position.y), xf * bounds.end, xf * Vector2(bounds.position.x, bounds.end.y), ] var out := Rect2(corners[0], Vector2.ZERO) for corner in corners: out = out.expand(corner) return outfunc fits(candidate: Rect2, placed: Array[Rect2]) -> bool: for other in placed: if candidate.grow(-1.0).intersects(other): return false return trueRooms touch where their doors meet, so fits() shrinks the new rectangle by a pixel first. Touching passes. Real overlap still fails.
Putting it together, try_attach() instances a room, tries each of its doors against one open door, and keeps the first one that fits:
func try_attach(scene: PackedScene, target_door: Marker2D, parent: Node2D, placed: Array[Rect2], bounds: Rect2) -> Node2D: var room := scene.instantiate() as Node2D parent.add_child(room) # into the tree first, so global positions update for door in room.get_children(): if not door is Marker2D: continue attach(room, door, target_door) var box := world_bounds(room, bounds) if fits(box, placed): placed.append(box) return room room.queue_free() return nullYour generator loop calls it with a random open door and a random room scene. When it returns a room, remove the target door from your open list and add the new room's other doors. When it returns null, nothing was kept, so roll again. A seeded RandomNumberGenerator for those picks makes the floor repeatable, as part 7, on seeds, explains.
Build it next
2D Roguelike: Rooms & Dungeon
A quest on CodingQuests: 8 interactive lessons that build one part of a game in Godot, in your browser, checked as you go.
No CodingQuests quest teaches premade rooms joined at sockets yet, so here is the closest practice, described honestly. The 2D Roguelike's Rooms & Dungeon quest builds a room as a scene: a TileMapLayer floor and walls in lesson 1, and doors that lock and open in lesson 3. Both are free.
It then regenerates that one room in place each time you walk through a door. It does not join different rooms together. Build its room and door scenes, then use the functions above to stitch several of them into a floor.
- Lesson 1: The Room SceneFree
- Lesson 3: DoorsFree
Lessons 1 to 3 are free to open, no account needed. Guild Hall, the membership, opens the other 5.
FAQ
How do I line up doors between procedurally placed rooms in Godot?
Put a Marker2D at each doorway, pointing out of the room. Rotate the new room so its door faces the opposite way to the open door, then move it by the difference between the two doors' global positions. Add the room to the scene tree before you read any global positions.
Why is my generated room rotated the wrong way?
Usually a door marker points into its room instead of out, or the code adds the door's rotation instead of subtracting it. The room's rotation should be the target door's rotation, plus half a turn, minus the door's own rotation.
How do I stop premade rooms overlapping?
Give each room a rectangle in its own space, transform it into world space after placing the room, and compare it with every room already placed. Shrink it by a pixel first so rooms that only touch at the doorway still pass.
Sources and testing
- Godot docs: Marker2D, Node2D, Rect2, Area2D (overlap lists update during the physics step)
- Darius Kazemi, Spelunky Generator Lessons: rooms built from templates with guaranteed exits
- Developer questions this answers, on the Godot Forum: Best approach for connecting doors for procedural generated dungeon rooms?, Random dungeons with premade rooms, Line up Procedurally Generated Rooms
How this was tested. Every GDScript function on this page was run in Godot 4.7.2 (headless, on Windows) on 31 October 2026. The test turned and placed a real PackedScene room on a door, confirmed the two doors met with no gap and faced opposite ways, and confirmed try_attach() returns null when the space is blocked. The demo is a simplified grid model of the same rules, not the Godot code: its room templates, door lists and the 200-seed counts belong to the model.

