Manipulation is the part of Grid Placement that works on objects already placed in the world.
It covers:
- Move.
- Rotate.
- Flip.
- Demolish.
Version note: This guide is for Grid Placement 6.0.0. The recommended runtime model is
GridPlacementHost+PlacementSession+ services. The old "ManipulationSystem node" mental model is legacy.
The Core Split
Manipulation has two main responsibilities:
| Piece | Type | What it owns |
|---|---|---|
ManipulationService / ManipulationService2D |
RefCounted service | Business logic: start, validate, commit, cancel, move, demolish. |
ManipulationParent |
Node2D | Temporary visual transform parent for preview/ghost objects and indicators. |
Plain English:
- The service decides whether manipulation is allowed and what action is happening.
- The parent makes the preview look moved, rotated, or flipped in the scene tree.
This split keeps validation/test logic separate from visual transform behavior.
What an Object Needs
To support manipulation, the placed object should have:
- A
Manipulatablecomponent. ManipulatableSettingsconfigured for allowed operations.- Collision/targeting layers that allow
TargetingShapeCast2Dto detect it. - A sensible root assignment so the plugin knows which node is the real placed object.
If move/demolish does nothing, check targeting and ManipulatableSettings before rewriting manipulation code.
Scene Mental Model
During a move, the temporary hierarchy is conceptually:
GridPositioner2D
└── ManipulationParent
├── IndicatorManager / indicators
└── preview or ghost copy of the object
GridPositioner2D moves the whole assembly to the target grid cell. ManipulationParent applies rotation/flip transforms so the preview and indicators stay together.
Important Behavior
Movable validation
A move should be rejected if the active ManipulatableSettings says the object is not movable.
Check:
Manipulatable.settingsexists.ManipulatableSettings.movableis true.- The object can be targeted.
Transform preservation
When a move completes, the transform accumulated during the move should be applied to the placed result. If the result resets rotation or flip, check custom scripts on the placed object first. _ready() logic that rewrites transform can override the plugin's placement result.
Collision exclusions
During a move, the original source object should be excluded from collision checks so it does not block its own preview. If validation says the object collides with itself, inspect targeting/collision layers and stale preview cleanup.
Preview cleanup
Canceling or completing manipulation should clear temporary preview/parent state. If you change game modes, unload a level, kill a player, or remove the source object during manipulation, make sure the interaction is canceled cleanly through the plugin path instead of freeing random children by hand.
Signals to Prefer
For new code, prefer consolidated action signals where available.
ManipulationState exposes:
| Signal | Use |
|---|---|
action_performed(data: ManipulationData) |
New consumer path for started/finished/failed/canceled status changes. |
pre_demolish(data: DemolishData) |
Last chance to inspect or veto demolish before the target is freed. |
parent_changed(parent: Node) |
Connect to the active ManipulationParent for transform callbacks. |
Older started, finished, failed, and canceled signals still exist as compatibility signals, but new integrations should prefer action_performed when they can.
Rotation and Flip Callbacks
ManipulationParent emits transform callbacks:
| Signal | Payload |
|---|---|
rotated(new_rotation_degrees: float, cardinal: PlacementGridRotationUtils.CardinalDirection) |
New angle and nearest cardinal direction. |
flipped(axis: PlacementEnums.FlipAxis) |
HORIZONTAL or VERTICAL. |
Connect after the manipulation parent is available:
func _ready() -> void:
manipulation_state.parent_changed.connect(_on_parent_changed)
func _on_parent_changed(parent: Node) -> void:
if parent is ManipulationParent:
parent.rotated.connect(_on_rotated)
parent.flipped.connect(_on_flipped)
Use these callbacks when your art needs special handling, such as swapping directional sprite frames instead of visually spinning the sprite.
Directional Sprite Pattern
For top-down 4-direction art, you can let the plugin rotate the footprint while your sprite swaps frames.
func _on_rotated(_degrees: float, cardinal: PlacementGridRotationUtils.CardinalDirection) -> void:
sprite.texture = directional_frames[cardinal]
sprite.rotation = -sprite.get_parent().rotation
Why the plugin still rotates the parent: the rotation affects both the preview art and the collision/indicator footprint. A 2×1 object needs to become a 1×2 footprint when rotated.
Demolish and Refunds
For object demolish refunds, prefer the dedicated refund path instead of ad-hoc signal code.
Use:
RefundServicefor refund calculation/application.BuildCost/ id-keyed cost data for the value to refund.ManipulationState.pre_demolishfor veto/inspection paths.
See Refund on Demolish for the full spend/refund contract.
Only write a manual action_performed or pre_demolish handler when your game needs custom behavior the refund service does not cover.
Common Pitfalls
- Looking for an old
ManipulationSystemscene node instead of using host/session services. - Missing
Manipulatableon the placed object. ManipulatableSettings.movableis false.- Targeting shapecast cannot see the object because collision layers/masks do not match.
- Rotating the child preview directly instead of using
ManipulationParenttransform methods. - UI/camera code consumes rotation/flip input before placement receives it.
- Saving temporary manipulation preview nodes as real placed objects.
- Implementing manual refunds in multiple places and double-refunding.
Related Guides
Source
docs/v6-0/guides/manipulation-system-vs-parent.md
Plugin docs root:gdscript/plugins/grid_placement_dev/docs