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_buildinput 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_tileby callingtry_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:
- Right-click World node → "Add Child Node"
- Search for "DragManager"
- Add it as a child
- 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:
- Player enters BUILD mode
- Player selects a placeable object
- Player presses and holds
confirm_buildaction (e.g., left mouse button) - Player drags across tiles
- Objects are placed at each new tile
- Player releases
confirm_buildto 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:
- Player drags quickly across multiple tiles
- Frame 1: Positioner moves from tile (0,0) → (1,0) → (2,0) within same frame
- Without gating: 3 signals emitted → 3 builds attempt → race condition
- 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 activestart_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:
- Is DragManager added to the scene?
- Has
resolve_gb_dependencies()been called? - Is
input_enabledset totrue? - Is the
confirm_buildaction configured in InputMap? - 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:
- Physics frame gating is working (see
_last_signal_physics_frame) - Tile deduplication is working (see
last_attempted_tile) - BuildingSystem is properly connected to DragManager signals
Builds Not Occurring
If no builds happen during drag:
Check:
targeting_new_tilesignal has listeners- BuildingSystem is in BUILD mode
- Placeable is selected
- 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