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.
Startup is not the bridge's job
A bridge translates game meaning. It does not have to own startup.
Keep startup in the host/session setup. A single-session scene can use the authored host path; a controller should configure and register sessions when the game owns their lifetime. See Composition & Injection for the wiring steps and Session Ownership for choosing a session lifetime.
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
Grid Placement is authoritative for placement mechanics, not for the consuming game's simulation. It decides placement-session state, candidate transforms and placement validity. The consuming game remains authoritative for gameplay entities and consequences such as resources, construction, ownership, persistence and destruction.
A bridge translates between game logic and Grid Placement without creating 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.
Game-owned state typically includes:
- 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.
External / game-authority path
A simulation-heavy game can keep Grid Placement as the placement UX/rules layer while its own world model remains authoritative. The supported flow is:
- The player proposes a placement through Grid Placement.
- Grid Placement computes the candidate transform and placement validity.
- The host publishes a
PlacementLifecycleResultthrough its lifecycle bus. It includes the plugin-ownedattempt_id; a successful object placement also carriesplaced_instance_idwhen the committed object has aSceneEntryInstance. Anycorrelation_contextis carried through as an opaque value that Grid Placement does not inspect. - The integration can decide and apply its own consequences and keep its own entity ID in its own save or a side table keyed by the plugin's placement ID. Plugin IDs and game entity IDs are separate namespaces.
- Presentation can be created or updated from game state.
- Later move/demolish operations pass through the same boundary:
ManipulationState.pre_moveandManipulationState.pre_demolishare veto windows, and consequences such as refunds run afterFINISHEDthroughRefunder.on_demolish_confirmed.
The simple built-in path remains fully supported: the plugin instantiates scenes and manages placement bookkeeping while the game reacts to the results. SceneEntryInstance.saved_game_data carries game-owned save fields through plugin round trips.
Occupancy state in a bridge
The full addon's occupancy registry answers placement queries. A bridge-side
_occupied dictionary that mirrors it duplicates that fact. An integration
that keeps its own placement authority can keep its own occupancy data as a
separate namespace.
Game-specific operations
A bridge is most useful when it exposes game actions instead of wrapping every plugin method.
For example, request_build(owner, entry, target) can check game policy,
submit through the supported placement API, and update the game record from
the result. Keep the method focused on that workflow instead of forwarding
every plugin call.
The 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: map 2D placement IDs
This small 2D object-placement example maps the plugin's placement ID to a game-owned building record. It is a starting point, not a drop-in class: wire the authored session and host, then connect the record and save hooks to your game.
class_name BuildingAuthority
extends Node
@export var session:PlacementSession
## 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:
assert(session!= null,"Assign the host's PlacementSession here too.")
# This signal covers the 2D object-placement path shown in this example.
session.get_states().building.action_performed.connect(_on_placement_action)
## Player chooses what to place. Preview follows the cursor automatically.
func select_entry(entry:ScenePlacementEntry)-> void:
if not _host.select_entry_for_session(session, entry):
building_rejected.emit("Placement is not ready.")
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 _new_building_record(id:StringName, node:Node, lifecycle:PlacementLifecycleResult)-> Dictionary:
return {"building_id": id,"node": node,"kind": lifecycle.definition_key}This example does not enforce build costs. A check before selecting an entry can help the UI, but funds may change before the player commits. Use a rule to check current affordability during validation and apply the cost after success. See Custom Placement Rules and Placement World Facts Provider.
What this example demonstrates
- Selection goes through
GridPlacementHost.select_entry_for_session(), which updates the authoritative session and activates the correct GRID/SMOOTH placement path — no manual preview management. - Commit observation uses
PlacementState.action_performedfor the 2D object path shown here. The typedPlacementActionData.reportgives you the placed node, success/failure, and lifecycle result. For 3D GRID events, useObjectPlacementService3D.object_placed; useGridPlacementHost.get_lifecycle_bus()for dimension-neutral lifecycle IDs and outcomes. - ID correlation maps the plugin's
placed_instance_id(stable, unique) to your ownbuilding_id. The two identities are separate namespaces and can be correlated without merging. - 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 host uses the authored session; the bridge selects through the host API and observes results. For controller-owned sessions, use the setup described in Composition & Injection.
- The bridge reacts to placements; it does not drive this example's input loop. The host dispatches the 2D build input.
- If you need programmatic placement (AI builders, scripted spawns), build a
PlacementCommand.commit(entry, target, correlation)and callPlacementRuntime.new(host).execute(command). The target matches the session's coordinate mode: aVector2i/Vector3icell in GRID, aVector2/Vector3world position in SMOOTH. The returnedHostDispatchResultcarries the terminalPlacementLifecycleResultwith your correlation data. Scene-free backends use SMOOTH; 2D GRID runs the session's rule pipeline and needs its positioner and target map (seeplacement/PLACEMENT_LIFECYCLE_CONTRACT.md). - For read-only game facts consumed during validation (tech unlocks, protected zones), a custom placement rule or world-facts provider can replace a full bridge and keep validation logic declarative.
Providers and 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.
A bridge is useful when a larger game lifecycle coordinates 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. Listen to
GridPlacementHost.get_lifecycle_bus(). When the result has operationPlacementLifecycleResult.OP_REMOVEand outcomePlacementLifecycleResult.OUTCOME_REMOVED, erase the game record keyed byplaced_instance_id. The host publishes this result after the demolish dispatch completes.
When a placed object has a SceneEntryInstance, successful commit, move, and demolish results carry its placed_instance_id. Check that the ID is not empty before updating the game record. Refunder.on_pre_demolish is veto-only; apply no consequences there. Game consequences such as refunds belong in Refunder.on_demolish_confirmed, which runs only after the demolition is FINISHED.
# Add these connections to the earlier _ready() method.
session.get_states().manipulation.pre_demolish.connect(_on_pre_demolish)
_host.get_lifecycle_bus().lifecycle_result_emitted.connect(_on_lifecycle_result)
func _on_pre_demolish(data:DemolishData)-> void:
var placement_id:= _placement_id_for_node(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_lifecycle_result(result:PlacementLifecycleResult)-> void:
if result.operation!= PlacementLifecycleResult.OP_REMOVE:
return
if result.outcome!= PlacementLifecycleResult.OUTCOME_REMOVED:
return
var placement_id:StringName = result.placed_instance_id
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_for_node(node:Node)-> StringName:
# Game-owned bookkeeping: the bridge stored the placed node per placement ID.
for placement_id:StringName in _buildings:
if _buildings[placement_id].get("node")== node:
return placement_id
return &""If 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.
Related guides
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.