Skip to content

Grid Placement v6.3.0

3D Room Placement & Stacking

Place a whole room as one entry on a 3D GridMap and stand the same rooms on the storeys above.

Status
Current
Version
v6.3.0
Source updated
2026-10-29
Generated on
2026-10-29

6.3. These pages document the 6.3 release, where placeable is now entry (what changed) and PlacementOwner is deprecated (migration). Still on 6.2? Your version's guides ship inside the plugin itself, under addons/grid_placement/guides/; the 6.2 URLs on this site now redirect to this series.

Development docs. This page is part of the unreleased 6.3 guide set and is not on the published site yet. It assumes a project that has installed addons/grid_placement and nothing else — no template scenes, no configured session, no demo copy, and nothing pre-built to open. Every scene and resource below is authored in the step that needs it, so the walkthrough starts from a genuinely empty project.

Version this needs: 6.3 or newer — verified, not assumed. 6.3 is the first release that books 3D occupancy per storey. Run the proof below against anything older and it stops at step 7 ("place the same 2x1 room on the storey above"): 6.2.2, 6.2.1, 6.2.0, 6.1.2 and 6.1.1 all key 3D occupancy by column, so the second storey is refused with Cell (0, 0, 0) is already occupied. and Placement overlaps an existing object. Your copy is on the usable side of that floor when hovering a placed room puts the ghost one row up; if that preview turns red instead, the stacking steps (§9) do not apply to it yet. What has to exist for the page to hold: footprint_3d and snap_profile_3d on ScenePlacementEntry; snap_mode, family, occupy_columns, require_column_support, snaps_with on PlacementSnapProfile3D; the entry scripts under placement/scene_entry/; and 3D occupancy recorded per storey rather than per column. A release missing any of those is out of scope. To move the floor — up for a new requirement, down if someone proves an older release — re-run the proof against that release; it prints the plugin.cfg version it actually ran against, so the answer is read, not guessed.

Demo content is not evidence of plugin behaviour. demos/3d/rooms/ — the room scenes, room_doors.gd, the storefront bundle — is demo glue that ships alongside the plugin, not part of addons/grid_placement. A behaviour that only appears in a demo does not set the version floor for this guide, and nothing below is copied from, or depends on, that demo.

This guide builds rooms the way a base-building game does it: one room is one entry object, and the same object stacks. The player picks a room size, the plugin validates the whole footprint as a unit, one commit produces the slab, the walls and the shell together, and picking that same room again while pointing at a placed room stands it one storey up. There is no "lay the deck, then ring the walls" step, and there is no upstairs catalogue item, because a player of a room-building game has neither.

Storeys are grid rows, not a second system: a storey is one GridMap row, so the room above the ground floor is that same cell with y one higher. The plugin books occupancy per storey, which is what lets one column hold a room on the ground and another directly above it, while a second room on a row that already holds one is still refused.

Per-storey booking is the column-support rule, not a new global one: a piece that does not ask to stand on the piece below it (require_column_support off) still reserves its whole column, so 2D and 2.5D pieces and their neighbours keep blocking each other exactly as before. Turning require_column_support on is what buys the room its own storey.

The rooms in this guide are open on the side that faces the camera, so you can see the interior. That is art, not rules: the plugin never cares how many walls a room scene draws.

Open the camera on that side. A room game shows rooms from the front, because a view turned away from the open wall is three blank walls — so the pose you author is the pose the view opens at. Turning is worth having after that (a far side, a roof), but it must be a choice the player makes, not the default. The rooms demo and the tutorial use one script for it, RoomCamera3D (demos/3d/rooms/room_camera_3d.gd): pan, lift, zoom, and a right-drag turn around the point the authored pose already looks at.

Rooms in this guide also carry a StaticBody3D with one shape per wall and one for the floor. That collision is for characters and the cursor ray, not for placement: the plugin never asks a body whether a cell is free. Occupancy is a record of which cells each placed room claims, so the preview can answer before the click, the same answer survives a reload, and a refusal can say which cells belong to which piece. A room with no body is still refused where it overlaps — it just has no floor for a dweller to stand on.

Read Getting Started first if you have not placed a single object yet. The 3D Object Placement guide is the reference for mounts, footprints and snap families; this page only covers what changes when the thing you are placing is a room.

The short version

Setup — once, from a scene that has only a ground GridMap and a camera.

  1. Install addons/grid_placement, enable the plugin, run Project → Tools → Grid Placement → Setup Default Input Actions.
  2. Run Project → Tools → Grid Placement → Setup Scene and pick 3D. It adopts the GridMap you already have as the level surface and the camera you already have as the one the positioner aims through; it creates only what is missing — host, level context, owner, positioner — plus a session, bundle, catalog and terrain entry written beside the scene.
  3. Read the report it prints. created is the nodes it added — on a bare rig, GridPlacementHost, Objects, PlacementLevelContext, PlacementOwner, GridPositioner3D. preserved is what it adopted or reused instead of duplicating, and it also names the resources it wrote: root as World (3D scene owns its GridMap), TargetMap3D (reused existing GridMap: …), Camera3D (<your camera>), bundle (session), catalog (session), session, terrain palette. The two needs config lines are reminders, not outstanding work: it prints the owner-root and camera lines even when it has just wired both. The three links still deserve a look: PlacementLevelContext.target_map_3d → your GridMap, PlacementOwner.owner_root → the node the player steers, GridPositioner3D.camera → your camera.
  4. Fill the catalog: select GridPlacementHost and add the rooms under Session → Catalog → Scene Entries, then save the scene. Setup Scene writes that catalog to <scene folder>/grid_placement/catalog_3d.tres and the scene points at that file, so the inspector and the file edit one list. It starts with your terrain entries and an empty room list. One entry per room size — CELL snap mode, family room, footprint_3d 1x1 / 2x1 / 3x1, occupy_columns on, require_column_support on, snaps_with ["ground", "room"], and the room's own scene. Duplicating one entry and changing the footprint is the whole job.
  5. Press Setup UI so the room panel can select those entries, then play the scene.

Placing — every session.

  1. Hover shows the whole footprint before you commit; one click is one room.
  2. Hover a room you already placed with the same entry selected: the cursor resolves the row above it by itself, and the ghost stands one storey up. Click there and that is the second storey; hover it again for the third. Code that drives placement passes that cell.
  3. Expect refusals for a cell another room already claims on that storey, for a footprint past the plot edge, and for a room one storey up with no room underneath it.

Everything after this list is the reasoning behind those steps. Section 2 lists what Setup Scene created, so you can author the rig by hand instead, and section 1 lists the rest of the Tools submenu.

A room is one entry with a footprint

Piece snap_mode family footprint_3d require_column_support snaps_with
Room 1x1 CELL room Vector2i(1,1) on ["ground", "room"]
Room 2x1 CELL room Vector2i(2,1) on ["ground", "room"]
Room 3x1 CELL room Vector2i(3,1) on ["ground", "room"]

The size is data, not a different kind of object: three entries, one snap profile shape, and the plugin's multi-cell footprint machinery does the rest. One entry per size serves every storey. There is no upstairs entry and no room_upper family: require_column_support says "rest on what is underneath me", snaps_with lists both the plot (ground) and a room (room) as things that can be underneath, and occupy_columns stays on so the room books the cells it covers on its own row.

That last part is the whole trick, and section 9 takes it apart.

The room's scene is authored centred on its footprint. A 2x1 room's origin sits on the shared grid line between its two cells, which is why one try_place_report produces art that spans both of them.

1. Install and run the editor setup actions

  1. Copy addons/grid_placement into your project.
  2. Project Settings → Plugins → Grid Placement → Enable.
  3. Run Project → Tools → Grid Placement → Setup Default Input Actions.
  4. Restart the editor, then confirm the placement actions exist in Project Settings → Input Map.

Step 3 is the only input setup this version ships. The rest of the same Tools submenu, for reference:

Command What it does
Grid Placement → Setup Default Input Actions Creates the placement actions in the project Input Map.
Grid Placement → Setup Scene Asks for a level type, then adds that type's rig to the open scene.
Grid Placement → Setup Hierarchy/… The same rig for one type — Platformer 2D, Top-down 2D, Isometric 2D, Hex 2D, 3D — without the picker.
Grid Placement → Setup UI Adds the scene-entry selection panel and points it at the session.
Grid Placement → Validate Current Scene Setup Checks the open scene's host/session/level wiring.
Grid Placement → Open Getting Started Opens the getting-started guide.

6.2.2 ships no migration command: the 5.x converter was removed, not deferred. Setup Scene builds the rig and reports what it created, preserved, and what still needs configuring; section 2 lists the minimum node set and the assignments, so you can let Setup Scene place the nodes and then match this section, or author them yourself. The panel is the separate Setup UI press. Use Validate Current Scene Setup after wiring to catch the usual mistakes.

2. Author the 3D scene

Minimum node set. Names are suggestions; the types and assignments are not.

Main (Node3D)
├─ Systems (Node)
│  ├─ GridPlacementHost (Node + GridPlacementHost script)
│  └─ GridPositioner3D (Node3D + GridPositioner3D script, session = your session)
├─ GridMap3D (GridMap — your floor surface + a MeshLibrary)
├─ PlacedObjects (Node3D)
├─ Level (Node + PlacementLevelContext script)
│  └─ LevelPersistence (Node + persistence script)   # only if you save
├─ Camera3D (current = true; RoomCamera3D drives it — no player body needed)
│  └─ PlacementOwner (Node + PlacementOwner script, owner_root = Camera3D)
└─ UI (CanvasLayer)
   └─ EntrySelectionUI (instance from templates/, or your own)

Then connect them:

# On node Set field To
1 GridPlacementHost session Your PlacementSession.
2 GridPlacementHost auto_configure true for a single-player scene.
3 GridPlacementHost grid_placement_bundle Must match the bundle used by the session.
4 Level (PlacementLevelContext) target_map_3d GridMap3D. This is the 3D surface selector.
5 Level (PlacementLevelContext) objects_parent PlacedObjects. Never place onto the GridMap itself.
6 PlacementOwner owner_root Camera3D (or your player body, if the scene has one).
7 GridPositioner3D session The same PlacementSession.
8 GridPositioner3D camera Camera3D.

Camera3D is not optional: GRID commits resolve the aim through the current camera projection and fail closed without one. Author the pose as the front view you want on open: square-on to the side the rooms open toward, looking slightly down, with the plot filling the frame. Camera height decides which storey you see into (section 9) while yaw decides whether you see into the room at all, which is why that pose is the default and not something the player has to restore. RoomCamera3D keeps it and works around it.

The room scene you hang off the positioner is authored in section 3.

Assign the 3D placement settings

The host composes the 3D object service itself, from the level context and the bundle, so an authored scene needs no setup script. It needs one resource from you: create an ObjectPlacementSettings3D (preview colors, presentation profile, support policy) and assign it to the bundle's object_placement_settings_3d slot.

With that assigned and the level context bound, the host builds the service and owns it — smooth placement, save/load and teardown all go through one authority. If your game already builds an ObjectPlacementService3D and hands it to the host, the host keeps yours instead.

The plot is the GridMap. A GridMap with cells holding a ground item is what the plugin reads as buildable ground; an empty GridMap refuses every room with a "no ground" style issue. That is the mechanism behind "you can only build on your base": author the slab you want buildable, and a room whose footprint runs past it is refused. The demo uses a 24×24 flat plot of a single Ground MeshLibrary item.

3. Author the room scene

No room ships with this guide, and the repository's test/fixtures/room_tutorial/ scenes are the automated proof's stage set (section 10) — test content, not a starting point. A walkthrough that began by opening them would skip the art it is meant to teach. Author one room here, then duplicate it per size in section 5.

Cells are 1 m. The root is a plain Node3D named for the size, and its origin is the centre of the footprint, not a corner: a 2x1 room's origin sits on the grid line between its two cells, so art authored around it spans both.

  1. Floor. MeshInstance3D with a BoxMesh of size = Vector3(2, 0.08, 1) at y = 0.04, so its top face is the grid plane and it covers cells -1 and 0.
  2. Back wall. A box one cell tall on the -z side, inset a little so neighbours do not z-fight: size = Vector3(1.84, 1, 0.08) at z = -0.46.
  3. End walls, with a doorway. On each x end, two stubs either side of the opening plus a lintel above it (the rooms demo uses 0.3 m stubs and a 0.4 m wide, 0.62 m tall opening). Leave the +z side fully open — that is the side the fixed camera looks through.
  4. Door panels. One MeshInstance3D per shared side, named DoorWest and DoorEast, filling the opening. They are visible by default, so a lone room reads as a closed room rather than a hole. Section 7 opens the ones that have a neighbour.
  5. Collision. A StaticBody3D with one CollisionShape3D + BoxShape3D per floor and wall. It is for characters and the cursor ray, not for placement: the plugin validates against its own cell records and never asks a body.
  6. Materials. A StandardMaterial3D each for floor, wall and door, the door a distinct shade so an open doorway reads at a glance.
  7. Save as res://rooms/room_2x1.tscn. Duplicate for 1x1 and 3x1 and widen the floor, back wall and end-wall offsets; keep the root at the footprint centre.
  8. A Manipulatable component carrying a ManipulatableSettings resource with demolishable on. This is what makes the room a piece the player can take back: a demolish click acquires its target by finding that component under the aimed piece, so a room scene without one cannot be acquired at all and the click is refused with "Room 2x1 can't be demolished: it's not marked as demolishable." — a scene authoring gap, never an aiming bug. movable decides whether Move mode can grab the room; leave rotatable and the flip flags off, because a room's orientation comes from its entry's footprint.

The room on the storey above is this same scene and the same entry: section 9 adds no art.

What makes a cutaway room read

A floor and three walls under a camera that looks down are two flat bands until something tells the eye which band is horizontal. The demo's rooms add four cheap cues, and any of them can be dropped without affecting placement:

  • Wall caps. A thin, lighter box on top of every wall, sunk 7 mm into it. The top face is what the camera sees; without it the wall top is the wall colour edge-on and the room has no outline.
  • Baseboard and floor lip. A dark strip where the back wall meets the floor and a lip on the open edge: the two edges that otherwise dissolve into the ground behind the room. Sink both into the surface they sit on, or the coplanar faces z-fight into a dotted line.
  • Floor seams. The plot's own world-space grid shader on a plane just above the floor, so the floor shows the cells the room covers.
  • One interior light per room. A shadowless warm OmniLight3D near the ceiling with its falloff doing the shading: it is what separates floor from wall, and it is cheaper and steadier than a shadow-casting sun at this scale. Anything standing in the room (the demo's crate, the light itself) is what finally tells the eye which band is the floor.

All of it is art and none of it is a plugin rule: a room with none of these cues places exactly the same, and a room whose open side faces another way needs the mirror of it.

4. Create the session and bundle

  1. Right-click in FileSystem → New Resource… → GridPlacementBundle. Leave its defaults; it creates its settings/templates/actions subresources.
  2. New Resource… → PlacementSession. Assign the bundle to grid_placement_bundle. Leave settings and actions empty to use the bundle's; assign catalog in section 5.
  3. Put the session on GridPlacementHost.session and GridPositioner3D.session.

One session per player. Do not share a session between split-screen players.

5. Create the room entries

For each size you ship, create a ScenePlacementEntry:

  1. packed_scene — the room scene from section 3. Required.
  2. display_name — what the player sees (Room 2x1).
  3. icon — the catalog thumbnail.
  4. profiles — at least one PlacementProfile, or the shipped selection UI shows no category tab and the entry cannot be selected.
  5. footprint_3d — the room's size in cells, e.g. Vector2i(2, 1).
  6. snap_profile_3d — a PlacementSnapProfile3D with:
    • snap_mode = CELL (rests on the plot and claims whole cells),
    • family = "room",
    • occupy_columns on — the room books every cell of its footprint on the row it stands on,
    • require_column_support on — it needs something accepted underneath every covered cell, on that same column,
    • snaps_with = ["ground", "room"] — the plot counts as underneath, and so does another room, which is what makes the next storey legal,
    • attachment_requirement = Optional so the first room of a row can be placed with nothing next to it.

Those fields make the entry stackable and grounded by the same rules, so the first room of the scene is placed on the plot and the one above it is placed on the room below, with no second entry and no per-storey variant.

Which flags you need — and which you do not

Three fields decide this, and each one is only needed for a job it names. Setting one "just in case" changes what the piece is allowed to do.

You are building require_column_support snaps_with occupy_columns Why
Rooms that stack (this guide) on ["ground", "room"] on Ground floor rests on the plot, upper floors on the room below, and each row is still claimed.
A room on the plot, never upstairs off [] on Nothing can float, nothing can stack. The safest default.
Free floor slots, no support rule off [] on Any row is legal, including one over nothing. Overlap on a row is still refused.
A floor, roof or deck sharing a cell with the piece below off [] off Two pieces in one column is the point: it is an overlay, not a claim.
A prop that stacks on a crate on ["crate"] on Same rule as a room; only the accepted family differs.

The two mistakes worth naming:

  • require_column_support on a piece that should never stack. It lifts the grounding rule, so the piece may anchor above the terrain — that is exactly what lets the third storey be refused only when nothing is underneath, instead of always.
  • occupy_columns off on a room. The room stops claiming its row, so nothing refuses a second room that overlaps it there. Overlays are the one case that wants that; a room is not.

None of the three is set by the plugin for you, and none is inferred from the scene. They are authored, per entry, in the snap profile.

Add the sizes to the session's catalog in ascending order so the UI reads as 1x1 → 2x1 → 3x1. That is the whole catalogue: three entries, every storey.

6. Build a row

  1. Select a size. Hover a free cell: the preview must be green before the click, and it must cover the whole footprint, not just the anchor cell.
  2. Click. One commit produces the room — slab, walls and shell.
  3. Select the next size and click the cell next to the first room. Rooms butt up on the grid line without any extra rule; the second room's own shell is what you see at the boundary.

That is the whole placement workflow. There is no second step to "finish" a room, and nothing to clean up afterwards. Section 9 adds the upstairs.

7. Shared walls between adjacent rooms

Each room ships its own complete shell, so two adjacent rooms produce two thin walls with a hand's width between them rather than one shared wall. Pick one of these, in order of cost:

  1. Accept the seam. Two inset walls read as separate pods, which is fine for a bunker and needs no art work.
  2. Author half-thickness edges. Walls on the shared sides sit just inside the footprint, so neighbours' walls meet with a hairline gap instead of a visible double wall.
  3. Move walls into a neighbour-aware piece. That is a different model — see the appendix.

There is no automatic "merge these two rooms" step in 6.2, and a tutorial that promised one would be lying.

Optional doorways: the demo's neighbour hook

Not part of placing or stacking, and nothing below is needed to finish this guide. The rooms demo authors a door panel on each side of a room scene, visible by default, and a demo-owned node opens only the doors that have a neighbour on the same storey:

func _on_object_placed(_cell:Vector3i, p_node:Node3D, _entry:ScenePlacementEntry)-> void:
	p_node.tree_exiting.connect(refresh,CONNECT_ONE_SHOT)
	refresh()

refresh() reads each placed room's floor-plate AABB and toggles DoorWest / DoorEast for the pairs whose plates touch on one storey. The storey test is not decoration: the room above shares x and z with the room below, so an x/z-only test would open a doorway through the floor slab. object_placed is the hook, not a plugin rule — the service still decides only which cells a room claims.

The implementation is demos/3d/rooms/room_doors.gd, its regression suite is test/demos/integration/3d/room_doors_test.gd, and Custom Placement Rules covers the hook itself. Copy it if your rooms need neighbour-aware art; skip it otherwise.

8. What the plugin refuses

Attempt Why it is refused
Room on cells another room covers on that row CELL occupancy, booked per storey: every footprint cell of that row is claimed.
Room overlapping part of a room on the same row Same rule — the overlap is per cell, not per anchor.
Room whose footprint runs past the GridMap No ground under one or more footprint cells.
Room one storey up with no room underneath it require_column_support: every covered cell of that column must hold room, or the plot.
The same room again at the same cell and rotation It is already placed there.

Note the first rule's row: the same cells one storey up are free. That is not an exception to occupancy — it is occupancy, booked per row.

Every one of those is the plugin talking, not the demo. A room demo must surface the refusal rather than draw the room anyway — that is the difference between a tutorial and a mock-up.

Rules the plugin cannot own

A rule only your game knows — rooms on the front row, a piece with one facing, a reserved band — is a PlacementCellPolicy3D you write and attach to the entry's cell_policy_3d. That is game code, so it lives in your project, not in the plugin and not in this tutorial: the rig below attaches none, because everything it demonstrates is the plugin's own refusal.

The demo project keeps a working one beside its rooms, demos/3d/rooms/row_band_policy.gd, as a starting point. It is inert there — no entry in the demo attaches it — so copy it, change the numbers, attach it to the entry. See Custom placement rules.

What the status line reads on this rig, copied from the proof run rather than written by hand:

  • 1x1 room over cells a room covers on that row — Cell (0, 0, 0) is already occupied. then Placement overlaps an existing object.
  • 3x1 room running past the plot edge — No ground under this spot.
  • Room one storey up over bare plot — Cell (0, 1, 2) floats above the terrain: CELL placement rests on the ground; mounting above ground needs an edge/face/top/corner socket. then 'Room 2x1' (family 'room') needs the ground or a piece underneath every covered tile: the column below each cell must hold ["room"], or the level's ground.

The coordinates follow the proof's own cells; the reasons are the same ones you will read in your project. Both lines of a two-line refusal come from the same report, so a HUD that shows only the first one hides why the placement failed. The second line is what tells a room builder what is missing: the first is the plugin saying the row is unsupported, the second says by what.

9. Storeys: the same room one row up

A storey is the same placement on the row above. There is no upstairs entry and no storey switch to fit: the room you already picked stands on the room you already placed, and the cell handed to the service is y + 1.

What makes that legal is authored on the entry, not on a second one:

Field Value What it does here
family room One name for every storey: it hosts itself, safely.
require_column_support on The row must rest on something accepted underneath.
snaps_with ["ground", "room"] The plot holds the ground floor; a room holds the next.
occupy_columns on The room books its cells on that row.
footprint_3d (2,1) The shape must be covered below, cell by cell.
cell y 0, 1, 2 … One GridMap row per storey.

Three separate rules fall out of those five fields, and each is a rule the plugin already enforced — none of it is demo code:

  • Occupancy is per storey — for pieces that ask to stack. A CELL entry with require_column_support on books its cells at (x, y, z), so a room on row 0 and a room on row 1 of the same column are two claims, not one conflict. (An entry with it off keeps counting every row of its column, exactly as before — per-storey booking is what the support rule buys, not a global swap.) That is what makes the stack legal while occupy_columns stays on: the room still claims its own row, so a second room that overlaps it on that row is refused.
  • Support is per column. require_column_support asks the cell directly below to hold a family in snaps_with. Listing ground lets the first room rest on the plot; listing room lets every room above rest on the one below. A room one storey up over bare plot has neither, so it is refused twice over: the row floats, and the column holds nothing.
  • The room below is load-bearing. Removing it while a room stands on it is refused, because the same counterfactual that validates a fresh placement finds nothing to hold the storey.

The cursor does the climbing

Nothing in a game script lifts the piece. With require_column_support on, the cursor resolves the cell above an accepted provider itself: hover a placed room and the ghost, the preview report and the click all name y + 1 on that column. Hover the room you just stacked and it resolves y + 2. A third storey is the same entry one row higher — no extra resource, no UI.

That is also why this guide has no storey selector. An earlier revision of the rooms demo shipped one: a HUD panel that offset the hovered cell by a chosen row, because the pieces then relied on a separate room_upper entry. With occupancy booked per storey and snaps_with naming the room below, the row is the cursor's job, and a selector on top of it would offset twice.

A game that wants free floor slots rather than support-chained rooms (build anywhere, no structural rule) is a different policy: drop require_column_support and every row is legal. The plugin still refuses overlap on a row; it just stops asking what is underneath.

Each room carries its own floor plate, and the plate of the room above is the ceiling of the room below. With the camera fixed to the front, the plate is what you see when you look at the upstairs, and the room underneath is what you see when you pan down one storey — which is the whole point of the locked rig. The rooms proof captures that pair as 04-stacked-rooms and 05-lower-storey, and 04b-three-storeys for the third.

The row is the cursor's job, and so is the column. A piece snapped onto a room stands on the column you pointed at, not on the anchor of the room underneath: aim the middle of a 3x1 and the 3x1 above it sits one cell right of the room below, not pulled back over it. What the piece stands on is the room under the cursor; where it goes is the cell under the cursor. 04c-aimed-off-the-anchor captures that offset storey, and the positioner's pick evidence reports both cells (ground_cell, volume_owner_cell) so a placement log can show the same thing in numbers.

10. Verify

Run the editor check on the scene you wired:

  1. Project → Tools → Grid Placement → Validate Current Scene Setup.
  2. Enter the scene, select a room, hover a cell: the preview must be green and must cover the room's full footprint before you click.
  3. Place a 2x1 room, then a 3x1 beside it, then a 1x1 beside that.
  4. With the same 2x1 entry still selected, hover the room you placed: the preview must appear one row up and be green; click it. Hover the room you just stacked: the preview must appear one row above that. Nothing in the scene changes between those clicks.
  5. Hover bare plot with a room selected: green on the ground row, and red one row up — a room with nothing underneath is refused, not drawn.
  6. Hover a cell a room already claims on the row you are aiming at: red. The room one storey up over it is still legal, which is the difference between per-storey occupancy and a column rule.

Steps 4–6 are the whole stacking contract, and each one is the plugin talking: the preview, the commit and the refusal all come from the same validation.

The endpoint is proven outside the demos

test/smoke/room_tutorial_endpoint_proof.gd boots test/fixtures/room_tutorial/room_tutorial.tscn — a scene built only from addons/grid_placement plus its own primitive boxes, with no res://demos/ dependency — and walks this guide's build steps in the order a player meets them: scene wiring (§2), session (§4), entries (§5), a row (§6), a storey (§9), and then the refusals listed in §8. The fixture is the proof's own stage set: it exists so the steps can be checked unattended, and it is not the project you build in sections 2–5.

xvfb-run -a -s "-screen 0 1920x1080x24" godot --path godot \
  -s res://test/smoke/room_tutorial_endpoint_proof.gd \
  -- --out /tmp/room-tutorial-proof --size 1600x900

A pass prints RESULT: PASS in order plus the plugin.cfg version it ran against, and writes start, one-room, row, stacked and end-state screenshots, so the tutorial's before/after is captured from the same run that checked it.

That is how this page's version floor was set: the same fixture copied next to 6.2.2, 6.2.1, 6.2.0, 6.1.2 and 6.1.1 still fails at step 7 — "place the same 2x1 room on the storey above" is refused with Cell (0, 0, 0) is already occupied. there, because those releases book 3D occupancy per column. Running it against a release is the only thing that moves the floor — a release that merely looks newer is not evidence.

If you have the repository checkout, the same workflow is also pinned by the rooms demo's suites, which drive the shipped room entries through the demo scene — one for the service path, one for the real cursor path (hover → preview → click, and the climb onto the storey above):

python3 scripts/testing/run_test_tree.py \
  --target test/demos/integration/3d/room_build_e2e_test.gd

python3 scripts/testing/run_test_tree.py \
  --target test/demos/integration/3d/room_cursor_e2e_test.gd

11. The same room model in 2D

Nothing in sections 5–8 is 3D-specific. A 2D room is one ScenePlacementEntry with a multi-cell footprint_2d, placed as a single object on a TileMapLayer:

  • a 1x1 room keeps the default CELL_CENTER anchor;
  • centered art an even number of cells wide uses CELL_CENTER_LEFT (or CELL_TOP_LEFT for tall art), so the room covers whole cells instead of straddling half of each neighbour;
  • the same occupancy rule applies: one commit produces the whole room, its cells are claimed, and a room over those cells is refused.

test/grid_placement/integration/placement/room_2d_feasibility_test.gd pins that contract on the 2D object service, so the workflow the rooms demo teaches is verified in both dimensions rather than assumed.

Appendix: building a room out of parts

The 3D demo also ships a modular kit — floors, edge walls, upper decks, story walls — for games where the player designs the shell. That is a different model with different rules (each piece is its own placement and save record), and it is covered by 3D Object Placement, not by this page. Do not mix the two in one room: a room object plus loose wall pieces will double up on the shell.

This page ships with the plugin's 6.3 guide set and moves to the published site when 6.3 ships. The rooms demo is the worked example; its scenes and scripts are under demos/3d/rooms/ in the repository.

Using this in a game?

Grid Placement v6.3.0 — full source, playable demos, commercial license

The guide above is the same setup the shipped demos use. Try the build in your browser before you buy it.