Manipulation: System vs Parent
The Core Split
In 5.0.4, manipulation is divided into two distinct responsibilities:
- ManipulationSystem: The “Brain”. Owns business logic, state transitions, and validation.
- ManipulationParent: The “Body”. Owns visual transforms, scene hierarchy, and input handling for rotation/flip.
Why This Split Exists
The architecture intentionally separates orchestration from presentation.
- Testability: The
ManipulationSystemcan be tested (mostly) without needing a complex scene tree, as it operates on state and signals. - Clarity: The node hierarchy remains understandable. You know exactly where to look for rotation logic (Parent) vs validation logic (System).
- Stability: By isolating transform logic, we prevent “drift” where an object slowly moves off-grid due to floating point errors in repeated calculations.
Responsibilities
| Component | Role | Owns | Does NOT Own |
|---|---|---|---|
ManipulationSystem | Business Logic | Lifecycle (start/commit), validation (can I move this?), state transitions (is_moving), API (try_move). | Direct visual transforms, local rotation math, scene tree parentage. |
ManipulationParent | Visual Layer | Rotation/Flip/Scale containers, transform input handling, holding the ghost/preview object. | Validation rules, resource consumption, grid occupancy checks. |
Scene Hierarchy (Mental Model)
When you pick up an object, the hierarchy temporarily looks like this:
| |
The GridPositioner2D moves the entire assembly to the target grid cell. The ManipulationParent rotates the assembly around that center point.
Critical 5.0.4 Behaviors
1. Movable Validation
ManipulationSystem.try_move() strictly enforces the is_movable() check on the source object.
- Behavior: The system will reject the move with a failure message if
ManipulatableSettings.movableis false.
2. Transform Preservation
When a move completes, the accumulated transform (rotations applied during the move) must be preserved.
- Flow: Move Start → Copy original transform → User rotates/flips → Place → Apply final transform to new instance.
- Bug Watch: If your objects reset rotation after placement, check that you aren’t overwriting the transform in
_ready().
3. Stale Move Copy Protection (5.0.4)
\n5.0.4 update: Previous manipulations are now properly cancelled before starting new ones. This prevents stale collision shapes from persisting and interfering with new indicators.
4. Source Deletion Safety (5.0.4)
\n5.0.4 update: Fixed hard crash when the manipulation source is deleted during an active move. Indicators parented in the scene tree are now properly remove_child()’d before freeing, and collision exclusions are cleared on cancel/finish.
5. State Reset Fix (5.0.4)
\n5.0.4 update: Fixed a critical bug where _states.manipulation.data == null was a comparison (discarding the result) instead of an assignment. Stale manipulation data is now properly cleared.
6. Visual Desyncs
Because the parent handles the visual transform, if you manually set the node.rotation of the object inside the parent, you might get double rotations or unexpected offsets.
- Fix: Always rotate the
ManipulationParentusingapply_rotation()orapply_grid_rotation_clockwise(), never the child object directly.
Glossary
- ManipulationSystem: The singleton that orchestrates move/rotate logic.
- ManipulationParent: The
Node2Dthat holds the preview object. - Source Object: The original object in the world being moved.
- Preview Object: The temporary visual copy attached to the mouse cursor.
- GridPositioner2D: The component that snaps the preview to the grid cell center.
Common Pitfalls
- Script Access: Do not try to
get_node("ManipulationSystem")from inside a random scene script. Use dependency injection or a global singleton reference if your architecture supports it. - Input Consumption: The
ManipulationParentoften handles input for rotation. If your camera controller consumes all input, the rotation keys might not trigger. Ensure input propagation is handled correctly (e.g.,_unhandled_input). - Orphaned Previews: If the system is interrupted (e.g., the player dies while building), ensure
cancel_interaction()is called to clean up theManipulationParentand its preview child.
\n5.0.4 update: Orphaned preview leaks are now much less likely due to improved cleanup guards, but explicit cancel_interaction() calls are still recommended on state transitions.
Example: Listening to Manipulation State
| |