Skip to content

Articles

Godot 3D grid building system: the runtime loop

Build a player-facing 3D grid building system in Godot 4: place, manipulate, demolish with refunds, and save the build so it reloads where it stood.

A 3D grid building system is four loops stacked on one grid: place, manipulate, demolish and refund, and save and restore. Grid Placement owns the grid half of all four — targeting, validation, occupancy, placement identity. Your game owns the consequences: what it costs, what gets unlocked, what the player keeps.

This page is the seams between those halves. It assumes the scene is already rigged; if it is not, do the 3D setup walkthrough first and come back.

What the plugin guarantees, so you don't rebuild it

The placement loop runs the same way for 2D and 3D:

select → target → preview → validate → confirm → validate again → commit

Two things in that line matter more than they look. The preview is a suggestion — validation runs again at commit, and the second check decides. And when commit fails, nothing changes: no object, no occupancy, no half-state. That is what makes it safe to build UI on top of the preview without writing your own rules.

1. Charge the player after the commit, never after the preview

Preview validity is not permission. A preview can look perfect and still be refused at commit, so anything that costs the player has to hang off a successful placement — inventory changes, progression, quests.

Costs themselves can live in the plugin: SpendMaterialsRuleById is the id-keyed cost rule, and it reads through your inventory bridge (get_count_by_id, try_remove_by_id). One cost source then feeds both the purchase and the refund, which is the only way those two numbers stay equal over time.

2. Single-shot placement: deselect through the host

Most building UIs place one object, then drop the selection. Do that with the host API:

if placement_succeeded:
    host.deselect_entry_for_session(session)

Not clear_preview(). Deselection also clears the authoritative selection, syncs the UI, restores brush state and leaves placement mode in a consistent state; clearing the preview only hides the ghost. A single-session game can use host.deselect_entry() instead.

3. Move, rotate and demolish through the manipulation path

It is tempting to position = snapped_cell from your UI, or to queue_free() a demolished building. Do not. The plugin's manipulation path keeps six things in sync: the transform, occupancy/mount state, validation, stable placement identity, save data and refunds. Move a node by hand and those drift apart quietly — you find out when a save reloads two buildings into one cell.

The guide with the split between service and parent ownership is Manipulation: Service vs Parent.

4. Demolish with a refund that cannot drift from the cost

Refunds reuse the same cost data as placement:

refund_service.refund_ratio = 0.5   # return half of the build cost on demolish

RefundCalculator applies the ratio to BuildCost, and RefundService hands the id and amount to your inventory bridge, which must implement:

func try_add_by_id(id: StringName, amount: int) -> int:
    return GameInventory.add_items(id, amount)

Return the amount actually added, not the amount requested — a full inventory is a real case, and the refund pipeline expects the truth. Terrain uses TilePlacementEntry.build_cost instead of the object rule. If you need to veto a demolition (a building with items inside, say), a Refunder runs as a pre-demolish hook.

The full pipeline, including the terrain branch, is in Refund on Demolish.

5. Save the build so it reloads where it stood

Every committed object carries a SceneEntryInstance component (the plugin adds one when instance tracking is on) holding the stable placement_instance_id. That component owns the plugin half of the save:

var instance: SceneEntryInstance = placed.get_node(SceneEntryInstance.default_name)
var record := instance.save(true)   # true = include a UID in the scene-entry load data
save["buildings"].append(record)

save() writes instance_name, transform, scene_entry and placement_instance_id, and carries any saved_game_data you attached. It does not write mount or placement_world — for 3D builds you must save those occupancy records too, or a reloaded wall can look right while its EDGE is free for the next placement to reuse.

Restoring is a sequence, not a loop of instantiations:

  1. clear the committed placement world,
  2. rebuild each object with SceneEntryInstance.instance_from_save(record, parent, duplicate_ids) — pass one shared duplicate detector for the whole batch,
  3. rebuild GRID/SMOOTH/mount/socket occupancy through PlacementWorldRestoreCoordinator3D (2D has the 2D twin),
  4. validate the restored world before the player can build again.

Two canonical copies of the same fact is how saves rot. Your game owns entities, inventory and progression; the plugin owns placement bookkeeping. Keep game data under placement_instance_id rather than teaching the plugin what a wall's hit points are.

Save and Load has the full key table and the restore contract.

6. What actually goes wrong

  • The UI eats the input. A panel holding focus blocks targeting; check that first when clicks do nothing.
  • Nothing is targetable. The targeting collision mask cannot see the surface you are aiming at.
  • A cell is refused that looks empty. It is occupied in the placement world even though nothing is visible — usually a demolished node freed with queue_free() instead of the manipulation path.
  • The save reloads into the wrong cell. The transform was restored without the occupancy record, or the object was moved by hand.
  • Refunds pay twice or not at all. The inventory bridge returned the requested amount instead of the added amount.

Read the failure report the plugin returns before changing anything; it names the rule that refused.

What this walkthrough does not cover

This is the runtime loop, not the setup: host nodes, snap profiles, terrain palettes and the catalog live in the setup walkthrough. Multiplayer and split-screen session ownership is its own guide, and terrain painting is 3D Terrain Painting.

Grid Placement is a commercial plugin for Godot 4 (use 6.2 or newer for everything above); the guides and API reference are public, and the browser demos on the Grid Placement page run the real plugin.