Grid Placement

Manipulation: System vs Parent (5.0.2)

Architectural breakdown of the 5.0.2 manipulation system, explaining the separation of concerns between business logic (System) and visual transforms (Parent).

GridBuilding 5.0.2 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 Split

In 5.0.2, 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 exists

The 5.0.2 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

ComponentRoleOwnsDoes NOT own
ManipulationSystemBusiness LogicLifecycle (start/commit), validation (can I move this?), state transitions (is_moving), API (try_move).Direct visual transforms, local rotation math, scene tree parentage.
ManipulationParentVisual LayerRotation/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:

1
2
3
4
GridPositioner2D (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.2 Behaviors

1. Movable Validation

In 5.0.2, 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 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().

Validated By

The separation of concerns and the specific behaviors of the manipulation system are verified by the following test suites:

  • System Logic: res://addons/grid_building/systems/manipulation/manipulation_system.gd — Core implementation of the manipulation “brain”.
  • Rotation Handling: res://addons/grid_building/test/integration/grid_positioner_rotation_integration_test.gd — Validates the interaction between GridPositioner2D and ManipulationParent.
  • Workflow Integration: res://addons/grid_building/test/e2e/all_systems_integration_tests.gd — Validates the complete manipulation lifecycle from start to commit.
  • Rotation Reset: res://addons/grid_building/test/unit/rotation_reset_on_placeable_switch_test.gd — Tests that rotation state is correctly managed when switching placeables.

3. 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 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.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Example: Listening to manipulation state
# Note: This guide shows the pattern - actual implementation varies
func _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