A game bridge is useful when one build action must coordinate Grid Placement with several game systems, such as ownership, economy, construction progress, faction rules, AI tasks, or save IDs.
It is optional. A simple game can call Grid Placement directly.
Use a bridge when it removes repeated integration code
Good reasons:
- several game systems need one game-specific build operation;
- a successful placement must create or update a game entity;
- faction or player ownership must be attached to committed placement;
- construction, cost, or progression rules belong to the game;
- your save system maps plugin
placement_instance_idto a separate game building/entity ID; - you want plugin-specific types isolated from a large simulation module.
Do not add a bridge because of an arbitrary call-count or file-length threshold.
Keep placement state in one place
A bridge translates between game logic and Grid Placement. It should not create a second placement database.
game request/policy
→ game bridge/BuildingAuthority
→ Grid Placement preview/validate/commit
→ typed placement result
→ game updates game-owned stateGrid Placement tracks reusable placement facts:
- target and placement validity;
- committed placement identity;
- GRID and SMOOTH occupancy;
- CELL/EDGE/FACE/CORNER/TOP mount occupancy and SMOOTH socket occupancy;
- placement transform, mount, and support data;
- placement-world restore.
Your game tracks game-specific state:
- ownership and factions;
- construction progress;
- costs and resources beyond the placement integration;
- game entity/building IDs;
- AI and pathfinding consequences;
- game save schema and progression.
Avoid parallel occupancy state
Do not keep a bridge-side _occupied dictionary that mirrors Grid Placement occupancy.
If the game needs its own spatial data for gameplay, define what that data represents and reconnect it through stable placement IDs or placement results. Do not copy plugin occupancy internals into another registry.
Prefer game-specific operations
A bridge is most useful when it exposes game actions instead of wrapping every plugin method.
Example shape:
class_name BuildingAuthority
extends Node
signal building_committed(building_id:int, placement_id:StringName)
signal building_rejected(reason:String)
func request_build(owner_id:int, entry_id:StringName, target)-> void:
# 1. Check game-owned permissions/cost policy.
# 2. Submit through the supported Grid Placement API.
# 3. On commit, create/update the game-owned building record.
passThe target and result types can differ between 2D GRID, 3D GRID mounts, and SMOOTH placement. Do not force every dimension through an invented dictionary wrapper only to make the bridge interface look uniform.
Concrete example
The following is a complete, minimal BuildingAuthority that coordinates game-owned building state with Grid Placement. It shows the full lifecycle: select, react to commit, correlate IDs, and save/load mapping.
class_name BuildingAuthority
extends Node
## Maps a plugin placement ID to the game-owned building record.
var _buildings:Dictionary = {}
signal building_committed(building_id:StringName, placed_node:Node)
signal building_rejected(reason:String)
@onready var _host:GridPlacementHost = $GridPlacementHost
func _ready()-> void:
var session:= PlacementSession.new()
session.grid_placement_bundle= preload("res://config/grid_placement_bundle.tres")
_host.configure(session)
# React to every committed/rejected placement.
session.get_building_state().action_performed.connect(_on_placement_action)
## Player chooses what to place. Preview follows the cursor automatically.
func select_placeable(entry:ScenePlacementEntry)-> void:
_host.get_session().selected_placeable= entry
## Optional: game-owned gate before allowing a build request.
func request_build(entry:ScenePlacementEntry, player:Node)-> void:
if not _can_afford(entry, player):
building_rejected.emit("Not enough resources.")
return
select_placeable(entry)
func _on_placement_action(data:PlacementActionData)-> void:
if data== null or data.report== null:
return
if not data.report.is_successful():
var issues:Array[String]= data.report.get_issues()
building_rejected.emit("; ".join(issues)if issues.is_empty()== false else "Placement failed.")
return
var placed_node:Node = data.report.placed
var lifecycle:PlacementLifecycleResult = data.report.lifecycle_result
var placement_id:StringName = lifecycle.placed_instance_id
var building_id:StringName = _make_building_id(lifecycle)
_buildings[placement_id]= _new_building_record(building_id, placed_node, lifecycle)
building_committed.emit(building_id, placed_node)
func _make_building_id(lifecycle:PlacementLifecycleResult)-> StringName:
# Your own ID scheme; independent of the plugin's placement ID.
return &"building_%s" % lifecycle.attempt_id
func save_state()-> Dictionary:
# Persist only game-owned fields keyed by placement ID.
var out:Dictionary = {}
for placement_id:StringName in _buildings:
var rec:Dictionary = _buildings[placement_id]
out[placement_id]= {"building_id": rec["building_id"],"kind": rec["kind"]}
return out
func load_state(saved:Dictionary, placement_id_to_node:Dictionary)-> void:
# Reconnect after Grid Placement restores placement records.
for placement_id:StringName in saved:
var node:Node = placement_id_to_node.get(placement_idas String)
if node== null:
continue
_buildings[placement_id]= saved[placement_id].duplicate()
_buildings[placement_id]["node"]= node
func _can_afford(entry:ScenePlacementEntry, player:Node)-> bool:
# Game-owned economy check.
return true
func _new_building_record(id:StringName, node:Node, lifecycle:PlacementLifecycleResult)-> Dictionary:
return {"building_id": id,"node": node,"kind": lifecycle.definition_key}What this example demonstrates
- Selection goes through
session.selected_placeable, which auto-creates the preview — no manual preview management. - Commit observation uses
PlacementState.action_performed, the canonical signal for "a placement just happened." The typedPlacementActionData.reportgives you the placed node, success/failure, and the lifecycle result. - ID correlation maps the plugin's
placed_instance_id(stable, unique) to your ownbuilding_id. Keep both — never overwrite the plugin's identity with yours. - Save/load persists only your game-owned fields, then reconnects them to restored placement records through the stable ID mapping. Grid Placement restores placement transforms; you restore game meaning.
Key choices
- The bridge reacts to placements; it does not drive the input loop. Let the host's
_unhandled_inputdispatch builds. - If you need programmatic placement (AI builders, scripted spawns), call
GridPlacementHost.dispatch_smooth_placement(correlation)and read the returnedPlacementReport. - For read-only game facts consumed during validation (tech unlocks, protected zones), prefer a custom placement rule or world-facts provider instead of a full bridge — that keeps validation logic declarative.
Use providers or rules for read-only facts
If Grid Placement only needs to read a game fact during validation, a custom placement rule or world-facts provider is often smaller than a full bridge.
Examples:
- protected build zone;
- technology unlocked;
- 2D reserved cell;
- inventory availability.
Use a bridge when the game must coordinate a larger lifecycle around committed placement.
Save/load mapping
Keep plugin placement identity and game entity identity distinct:
placement_instance_id ↔ game building/entity idPersist that mapping in your game save layer. Let Grid Placement restore placement records, then reconnect your game-owned fields through the stable mapping.
Remove / demolish flow
Demolition follows the same single-authority split: the plugin destroys the placed node, the game drops its own building record. Never free the placed node from game code — observe and react instead.
Two hooks, in order:
- Gate (optional).
ManipulationState.pre_demolishfires before anything is destroyed. Calldata.veto()to cancel game-side (a building under construction, a protected plot); the node is preserved and the attempt reportsCANCELED. - Cleanup.
ManipulationState.action_performedalso fires for demolitions. Whendata.action == PlacementEnums.Action.DEMOLISHand the status isFINISHED, read the placement ID back off the destroyed node'sPlaceableInstancecomponent and erase the matching game record.
func _ready()-> void:
var manipulation:= _host.get_session().get_manipulation_state()
manipulation.pre_demolish.connect(_on_pre_demolish)
manipulation.action_performed.connect(_on_manipulation_action)
func _on_pre_demolish(data:DemolishData)-> void:
var placement_id:= _placement_id_of(data.target.root)
if placement_id.is_empty():
return
# Game-owned gate: construction in progress cannot be demolished.
if _is_under_construction(placement_id):
data.veto()
func _on_manipulation_action(data:ManipulationData)-> void:
if data.action!= PlacementEnums.Action.DEMOLISH:
return
if data.status!= PlacementEnums.Status.FINISHED:
return
var placement_id:= _placement_id_of(data.target.root)
if placement_id.is_empty():
return
_buildings.erase(placement_id)
# Notify your own game systems (construction tasks, build-cap counts, save-dirty flag) here.
func _placement_id_of(node:Node)-> StringName:
if node== null:
return &""
# PlaceableInstance is auto-added to placed roots when
# PlacementSettings.add_placeable_instance is true.
var found:Array[Node]= node.find_children("","PlaceableInstance",true,false)
if found.is_empty():
return &""
return (found[0]as PlaceableInstance).placement_instance_idIf the attempt was vetoed or failed, the node still exists and the game record stays untouched — there is no half-demolished state to reconcile.
Anti-patterns
Avoid:
- bypassing Grid Placement validation or occupancy from the bridge;
- re-exporting private/internal service objects as your game's public API;
- duplicating placement records in unsynchronized dictionaries;
- putting faction, economy, or construction types into the reusable addon;
- wrapping every plugin method one-for-one without adding a useful game-specific operation.