Chris' Tutorials
Docs/Grid Placement

Grid Placement v5.0.8

Drag Multi-Building

Use drag multi-building to place multiple objects in a single drag operation.

StatusCurrent
Versionv5.0.8
UpdatedActive v5.0.8 guide line from Grid Placement repo
Source note:This page rendered plugin-owned docs or generated metadata inside unified Astro docs shell.

Overview

Drag multi-building allows players to place multiple objects by pressing and holding the confirm action while dragging across tiles. This is useful for quickly building walls, fences, roads, or any repeating structures.

New in v5.0.0: DragManager is now a self-contained scene component that manages its own lifecycle, rather than being created on-demand by BuildingSystem.

Architecture (v5.0.0)

Component Responsibilities

DragManager (Scene Component)

  • Handles confirm_build input action (press/release)
  • Manages drag lifecycle (start/stop)
  • Detects tile changes via _physics_process() synchronized with physics updates
  • Emits signals when targeting new tiles (gated to once per physics frame)
  • Self-contained: Independent node that can be added to any scene
  • Physics-synchronized: Updates every physics frame to align with collision detection
  • No longer controlled by BuildingSystem

BuildingSystem (System)

  • Listens to DragManager signals
  • Responds to targeting_new_tile by calling try_build()
  • Tracks which tiles have been built during drag session
  • Prevents duplicate builds on the same tile

Key Changes from v4.x

v4.x (Old) v5.0.0 (New)
BuildingSystem creates DragManager lazily DragManager added to scene at editor time
BuildingSystem handles input and calls start_drag()/stop_drag() DragManager handles its own input
drag_multi_build setting required Drag enabled by presence of DragManager component
BuildingSystem owns drag lifecycle DragManager owns its own lifecycle

Setup

When to Use DragManager

DragManager is an optional component that enables drag-to-place functionality. Add it when you want:

Use DragManager for:

  • Placing multiple objects by dragging (walls, fences, roads)
  • Quick repetitive building without clicking each tile
  • Painting-style placement mechanics
  • Any tile-based drag operations (building, selection, movement, etc.)

Don't need DragManager for:

  • Single-click placement only
  • Building systems without drag support
  • Non-tile-based placement
  • Custom drag logic outside Grid Building plugin

Key Point: Drag functionality is enabled by the presence of a DragManager component in your scene. No settings required - if DragManager exists and is connected, drag building works automatically.

1. Add DragManager to Your Scene

Add DragManager as a child of your World or Systems node:

World/
├── GridPositioner2D
├── DragManager          # ← Add this
└── (other nodes)

Systems/
├── BuildingSystem
├── GridTargetingSystem
└── (other systems)

In Code:

# Create DragManager
var drag_manager = DragManager.new()
add_child(drag_manager)

# Inject dependencies (typically done by GBInjectorSystem)
drag_manager.resolve_gb_dependencies(composition_container)

# Connect to BuildingSystem
building_system.connect_drag_manager(drag_manager)

In Scene Editor:

  1. Right-click World node → "Add Child Node"
  2. Search for "DragManager"
  3. Add it as a child
  4. Dependencies will be injected automatically by GBInjectorSystem

2. Connect DragManager to BuildingSystem

After both systems have resolved their dependencies:

# In GBInjectorSystem or your initialization code
func _ready():
    # After all systems are initialized
    var drag_manager = get_node("World/DragManager")
    var building_system = get_node("Systems/BuildingSystem")

    building_system.connect_drag_manager(drag_manager)

3. Configure Input Actions

DragManager responds to the confirm_build action (defaults to "confirm"). Ensure your InputMap has this action configured:

Project → Project Settings → Input Map

  • Add action: confirm (or your custom action name)
  • Bind to: Mouse Button Left, Enter, or other inputs

Usage

Basic Drag Building

Once set up, drag building works automatically:

  1. Player enters BUILD mode
  2. Player selects a placeable object
  3. Player presses and holds confirm_build action (e.g., left mouse button)
  4. Player drags across tiles
  5. Objects are placed at each new tile
  6. Player releases confirm_build to stop dragging

Programmatic Control

For tests or custom behavior, you can disable input and control DragManager manually:

# Disable automatic input handling
drag_manager.input_enabled = false

# Manually control drag lifecycle
drag_manager.start_drag()  # Start dragging
# ... move positioner to different tiles ...
drag_manager.stop_drag()   # Stop dragging

Signals

DragManager emits three signals:

drag_started(drag_data: DragPathData)

Emitted when drag begins (confirm_build pressed).

drag_manager.drag_started.connect(func(drag_data):
    print("Drag started at tile: ", drag_data.target_tile)
)

drag_stopped(drag_data: DragPathData)

Emitted when drag ends (confirm_build released).

drag_manager.drag_stopped.connect(func(drag_data):
    print("Drag stopped")
)

targeting_new_tile(drag_data: DragPathData, new_tile: Vector2i, old_tile: Vector2i)

Emitted when the drag position moves to a different tile during drag.

Important: This signal is gated to emit at most once per physics frame to prevent race conditions.

drag_manager.targeting_new_tile.connect(func(drag_data, new_tile, old_tile):
    print("Moved from %s to %s" % [old_tile, new_tile])
    # BuildingSystem uses this signal to call try_build()
)

Race Condition Prevention

DragManager includes built-in protection against race conditions where multiple builds could occur before collision detection updates. This is achieved through physics frame synchronization and signal gating.

Physics Frame Synchronization

Why _physics_process() instead of _process()?

DragManager uses _physics_process() rather than _process() to synchronize drag detection with the physics engine's collision detection updates:

# DragManager updates in physics frame
func _physics_process(delta: float) -> void:
    if not is_dragging() or drag_data == null:
        return

    var old_tile = drag_data.target_tile
    drag_data.update(delta)

    if drag_data.target_tile != old_tile:
        # Tile changed - emit signal (with gating)
        emit_targeting_new_tile(drag_data, drag_data.target_tile, old_tile)

Benefits:

  • Consistent timing: Drag events align with physics updates (60 FPS by default)
  • Accurate collision detection: Buildings are placed after collision state updates
  • Prevents race conditions: No multiple builds within same physics frame

Physics Frame Gating

To further prevent race conditions, DragManager gates signal emissions to once per physics frame:

# Internal DragManager signal gating
var current_physics_frame := Engine.get_physics_frames()
if current_physics_frame == _last_signal_physics_frame:
    if _logger and _logger.is_trace_enabled():
        _logger.log_trace("[DragManager] GATE: Blocked duplicate in frame %d" % current_physics_frame)
    return  # Already emitted this frame, skip

_last_signal_physics_frame = current_physics_frame
targeting_new_tile.emit(drag_data, new_tile, old_tile)

This ensures targeting_new_tile emits at most once per physics frame, preventing multiple rapid builds before physics/collision state updates.

Example Scenario Without Gating:

  1. Player drags quickly across multiple tiles
  2. Frame 1: Positioner moves from tile (0,0) → (1,0) → (2,0) within same frame
  3. Without gating: 3 signals emitted → 3 builds attempt → race condition
  4. With gating: 1 signal emitted → 1 build → collision updates → next frame

See: CHANGES/2025-10-02-drag-building-race-condition.md for detailed analysis.

Tile Deduplication

BuildingSystem tracks which tiles have been built during the current drag session:

# Internal BuildingSystem code
if new_tile == drag_data.last_attempted_tile:
    return  # Already attempted this tile

This prevents rebuilding the same tile multiple times if the player hovers over it.

Testing

Unit Testing DragManager

extends GdUnitTestSuite

var drag_manager: DragManager
var container: GBCompositionContainer

func before_test():
    # Create and setup
    drag_manager = DragManager.new()
    add_child(drag_manager)
    drag_manager.resolve_gb_dependencies(container)

    # Disable input for manual control
    drag_manager.input_enabled = false

func test_drag_lifecycle():
    # Start drag
    var drag_data = drag_manager.start_drag()
    assert_object(drag_data).is_not_null()
    assert_bool(drag_manager.is_dragging()).is_true()

    # Stop drag
    drag_manager.stop_drag()
    assert_bool(drag_manager.is_dragging()).is_false()

Integration Testing with BuildingSystem

func before_test():
    # Setup both systems
    drag_manager = DragManager.new()
    building_system = BuildingSystem.new()

    add_child(drag_manager)
    add_child(building_system)

    drag_manager.resolve_gb_dependencies(container)
    building_system.resolve_gb_dependencies(container)

    # Connect them
    building_system.connect_drag_manager(drag_manager)

    # Disable input for manual control
    drag_manager.input_enabled = false

func test_drag_building():
    building_system.enter_build_mode(placeable)
    drag_manager.start_drag()

    # Move positioner to trigger builds
    position_at_tile(Vector2i(0, 0))
    await get_tree().physics_frame

    position_at_tile(Vector2i(1, 0))
    await get_tree().physics_frame

    # Verify builds occurred
    assert_int(build_count).is_equal(2)

    drag_manager.stop_drag()

Migration from v4.x

Old Pattern (v4.x)

# BuildingSystem automatically created DragManager
building_system.start_drag()  # Called by BuildingSystem input handler
# ... drag happens ...
building_system.stop_drag()   # Called by BuildingSystem input handler

New Pattern (v5.0.0)

# Add DragManager to scene
var drag_manager = DragManager.new()
world.add_child(drag_manager)
drag_manager.resolve_gb_dependencies(container)

# Connect to BuildingSystem
building_system.connect_drag_manager(drag_manager)

# DragManager handles its own lifecycle from input
# No manual start_drag()/stop_drag() calls needed

Settings Migration

Old (v4.x):

# Required setting to enable drag building
composition_container.get_settings().building.drag_multi_build = true

New (v5.0.0):

# No setting needed - drag enabled by presence of DragManager in scene
# If you don't want drag building, simply don't add DragManager component

Advanced Usage

DragManager Lifecycle

DragManager manages its own lifecycle through physics-synchronized updates:

# Lifecycle flow
func _ready():
    # Enable physics processing immediately
    set_physics_process(true)

func _physics_process(delta: float):
    if not is_dragging():
        return  # No active drag

    # Update drag state
    drag_data.update(delta)

    # Check for tile changes
    if drag_data.target_tile != old_tile:
        # Emit gated signal
        _emit_targeting_new_tile_gated(drag_data, new_tile, old_tile)

func start_drag() -> DragPathData:
    drag_data = DragPathData.new(_targeting_state)
    drag_started.emit(drag_data)
    return drag_data

func stop_drag():
    if drag_data:
        drag_stopped.emit(drag_data)
        drag_data = null

Key Methods:

  • is_dragging() -> bool: Check if drag is currently active
  • start_drag() -> DragPathData: Begin drag session (usually called from input)
  • stop_drag() -> void: End drag session (usually called from input)
  • _can_continue_dragging() -> bool: Validate drag can continue (checks targeting state)

Custom Drag Behavior

You can connect other systems to DragManager for non-building drag operations:

# Example: Drag-to-select multiple units
class_name SelectionSystem
extends Node

func _ready():
    var drag_manager = get_node("../World/DragManager")
    drag_manager.targeting_new_tile.connect(_on_tile_targeted)

func _on_tile_targeted(drag_data, new_tile, old_tile):
    # Select all units in the new tile
    select_units_at_tile(new_tile)

Disabling Drag in Specific Modes

# Disable drag manager in certain modes
func _on_mode_changed(mode: GBEnums.Mode):
    match mode:
        GBEnums.Mode.MANIPULATION:
            drag_manager.input_enabled = false  # No dragging in manipulation mode
        GBEnums.Mode.BUILD:
            drag_manager.input_enabled = true   # Enable for building

Troubleshooting

Drag Not Working

Check:

  1. Is DragManager added to the scene?
  2. Has resolve_gb_dependencies() been called?
  3. Is input_enabled set to true?
  4. Is the confirm_build action configured in InputMap?
  5. Has connect_drag_manager() been called on BuildingSystem?

Debug:

print("DragManager in tree: ", drag_manager.is_inside_tree())
print("Input enabled: ", drag_manager.input_enabled)
print("Is dragging: ", drag_manager.is_dragging())

Multiple Builds Per Tile

If the same tile is being built multiple times:

Check:

  1. Physics frame gating is working (see _last_signal_physics_frame)
  2. Tile deduplication is working (see last_attempted_tile)
  3. BuildingSystem is properly connected to DragManager signals

Builds Not Occurring

If no builds happen during drag:

Check:

  1. targeting_new_tile signal has listeners
  2. BuildingSystem is in BUILD mode
  3. Placeable is selected
  4. Tiles being targeted are valid (within map bounds, pass placement rules)

Enable trace logging:

var debug_settings = container.get_debug_settings()
debug_settings.level = GBDebugSettings.LogLevel.TRACE

See Also

Source

docs/v5-0/guides/drag-multi-building.md

Plugin docs root:gdscript/plugins/grid_placement_dev/docs