Skip to content

Grid Placement v6.3.0

Game Bridge Recommendation

Recommendations for bridging Grid Placement 6.3 with game-owned systems.

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.

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_id to 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 state

Grid 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:

  1. The player proposes a placement through Grid Placement.
  2. Grid Placement computes the candidate transform and placement validity.
  3. The host publishes a PlacementLifecycleResult through its lifecycle bus. It includes the plugin-owned attempt_id; a successful object placement also carries placed_instance_id when the committed object has a SceneEntryInstance. Any correlation_context is carried through as an opaque value that Grid Placement does not inspect.
  4. 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.
  5. Presentation can be created or updated from game state.
  6. Later move/demolish operations pass through the same boundary: ManipulationState.pre_move and ManipulationState.pre_demolish are veto windows, and consequences such as refunds run after FINISHED through Refunder.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_performed for the 2D object path shown here. The typed PlacementActionData.report gives you the placed node, success/failure, and lifecycle result. For 3D GRID events, use ObjectPlacementService3D.object_placed; use GridPlacementHost.get_lifecycle_bus() for dimension-neutral lifecycle IDs and outcomes.
  • ID correlation maps the plugin's placed_instance_id (stable, unique) to your own building_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 call PlacementRuntime.new(host).execute(command). The target matches the session's coordinate mode: a Vector2i/Vector3i cell in GRID, a Vector2/Vector3 world position in SMOOTH. The returned HostDispatchResult carries the terminal PlacementLifecycleResult with your correlation data. Scene-free backends use SMOOTH; 2D GRID runs the session's rule pipeline and needs its positioner and target map (see placement/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 id

Persist 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:

  1. Gate (optional). ManipulationState.pre_demolish fires before anything is destroyed. Call data.veto() to cancel game-side (a building under construction, a protected plot); the node is preserved and the attempt reports CANCELED.
  2. Cleanup. Listen to GridPlacementHost.get_lifecycle_bus(). When the result has operation PlacementLifecycleResult.OP_REMOVE and outcome PlacementLifecycleResult.OUTCOME_REMOVED, erase the game record keyed by placed_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.

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.