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
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.
- Grid Placement returns a stable
PlacementLifecycleResultwith the plugin-ownedattempt_idandplaced_instance_id, plus the opaquecorrelation_contextthat is echoed back untouched. - 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. PlaceableInstance.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.
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:
var session:= _host.get_session()
if session== null or not _host.select_placeable_for_session(session, entry):
building_rejected.emit("Placement is not ready.")
## 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
GridPlacementHost.select_placeable_for_session(), which updates the authoritative session and activates the correct GRID/SMOOTH placement path — 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. 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 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), 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.
ManipulationState.action_performedalso fires for demolitions. Whendata.action == PlacementEnums.Action.DEMOLISHand the status isFINISHED, read the canonical placement ID from the host's lifecycle result (_host.get_last_dispatch_result().get_lifecycle_result().placed_instance_id) and erase the matching game record.
Commit, move, and demolish results all populate placed_instance_id, so the lifecycle result is the stable handoff instead of a SceneTree lookup of the destroyed node. 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.
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_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_manipulation_action(data:ManipulationData)-> void:
if data.action!= PlacementEnums.Action.DEMOLISH:
return
if data.status!= PlacementEnums.Status.FINISHED:
return
# Canonical result: commit/move/demolish populate placed_instance_id.
var lifecycle:PlacementLifecycleResult = _host.get_last_dispatch_result().get_lifecycle_result()
var placement_id:StringName = lifecycle.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.