Manipulation: System vs Parent (5.0.3)
GridBuilding 5.0.3 introduces a strict separation of concerns for object manipulation (moving, rotating, flipping placed objects). This guide explains why this split exists and how it affects your code.## The Core SplitIn 5.0.3, manipulation is divided into two distinct responsibilities:1. ManipulationSystem: The “Brain”. Owns business logic, state transitions, and validation.2. ManipulationParent: The “Body”. Owns visual transforms, scene hierarchy, and input handling for rotation/flip.### Why this split existsThe 5.0.3 architecture intentionally separates orchestration from presentation.- Testability: The ManipulationSystem can 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:textGridPositioner2D (The cursor/grid snapper) \-- ManipulationParent (The rotator/flipper) +-- IndicatorManager (Visual feedback) \-- Preview / ghost object (The object being moved)The GridPositioner2D moves the entire assembly to the target grid cell. The ManipulationParent rotates the assembly around that center point.## Critical 5.0.3 Behaviors### 1. Movable ValidationIn 5.0.3, ManipulationSystem.try_move() strictly enforces the is_movable() check on the source object.- Old Behavior: You could sometimes force-move static objects via script.- New Behavior: The system will silently reject the move if Manipulatable.is_movable is false.### 2. Transform PreservationWhen 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().## Related Guides- Placement Workflow- Troubleshooting### 3. Visual DesyncsBecause 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 ManipulationParent (or use the rotate_90() API), never the child object directly.## Glossary- ManipulationSystem: The singleton that orchestrates move/rotate logic.- ManipulationParent: The Node2D that 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 ManipulationParent often 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 the ManipulationParent and its preview child.gdscript# Example: Listening to manipulation state# Note: This guide shows the pattern - actual implementation variesfunc _ready(): var sys = get_node("/root/ManipulationSystem") sys.manipulation_started.connect(_on_manipulation_started)func _on_manipulation_started(context: ManipulationContext): # The System tells us WHAT happened print("Started moving: ", context.source_object.name) # The Parent handles HOW it looks var parent = context.preview_parent parent.modulate = Color(1, 1, 1, 0.5) # Make it semi-transparent