Targeting Flow

This guide explains how targeting works in 5.0.8, including the relationship between GridTargetingSystem, GridPositioner2D, TargetingShapeCast2D, and GridTargetingState.

Version note: This guide is validated for Grid Building 5.0.8.

In the current 5.0.8 runtime, GridPositioner2D owns the main cursor/grid input handling. It:

  1. Receives input events.
  2. Converts screen coordinates into world coordinates.
  3. Converts world coordinates into target tiles.
  4. Moves itself to the active tile center.
  5. Updates visibility based on mode/settings/input state.
1
2
3
func _input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        _handle_mouse_motion_event(event, _mode_state.current)

GridTargetingSystem still matters, but it is not the node that directly owns mouse-follow behavior.


TargetingShapeCast2D

TargetingShapeCast2D is the collision-query layer. Its job is to update GridTargetingState.target from the current shapecast result.

1
2
func _physics_process(_delta: float) -> void:
    update_target()
  • If GridTargetingState.is_manual_targeting_active is true, automatic target updates are skipped.
  • When a collider is found, the raw collider may be promoted to a targetable root Area2D.
  • When no collider is found, the current target is cleared.

GridPositioner2D

GridPositioner2D is responsible for:

  • Mouse movement.
  • Keyboard tile movement.
  • Recenter behavior.
  • Visibility behavior.
  • Assigning itself into GridTargetingState.positioner.

It does not own:

  • Shapecast collision targeting.
  • Rotation/flip behavior.
  • Manipulation transforms.

5.0.4 update: _on_mode_changed now has an explicit -> void return type (fixed parse warning).


Target Highlighting

TargetHighlighter is the visual feedback layer for targeting. It does not move the cursor or decide which object is targetable. Its job is to read the current targeting result and highlight the target so players can see what the placement or manipulation flow is acting on.

Use it when you want feedback for:

  • Build mode selection.
  • Move-mode target hover.
  • Demolish or inspect previews.

Practical guidance:

  • Keep highlighting separate from cursor movement so targeting stays easy to reason about.
  • Wire the highlighter as a visual/UI concern, not as the source of targeting truth.
  • If highlighting looks wrong, check the targetable object setup and the visual settings used by the highlighter.

5.0.4 update: Fixed null dereference crashes in TargetHighlighter.current_target setter and _on_started(). These now guard against null before accessing properties.


GridTargetingState

The targeting state is the shared contract other systems consume. In practice it includes references such as:

1
2
3
4
5
6
var target: Node2D
var target_map: TileMapLayer
var maps: Array[TileMapLayer]
var positioner: Node2D
var is_manual_targeting_active: bool
var collision_exclusions: Array[Node]

Processing Flow

1
2
3
4
5
6
7
8
9
Mouse / keyboard input
    ->
GridPositioner2D moves to tile center
    ->
TargetingShapeCast2D queries collisions
    ->
GridTargetingState.target and positioner references are updated
    ->
Building / manipulation / UI systems consume targeting state

Recenter and Visibility Behavior

GridPositioner2D also owns several user-visible behaviors:

  • Recenter on enable.
  • Manual recenter action.
  • Visibility changes when mode changes.
  • Hide/show behavior based on input and targeting settings.
  • OFF-mode gating via remain_active_in_off_mode.

Essential Setup for Plugin Users

If targeting looks broken, check these first:

  • GBLevelContext.target_map is assigned.
  • GridPositioner2D was injected successfully.
  • TargetingShapeCast2D was injected successfully.
  • Your shapecast collision mask matches the targetable layers.
  • The mode allows the positioner to stay active/visible.
  • Your UI is not consuming the input path you expect.

Collision Layer vs Collision Mask for Targeting

TargetingShapeCast2D uses Godot’s collision system to detect objects:

PropertyWhat it meansUsed by Shapecast2D to…
collision_mask on TargetingShapeCast2DWhich layers to checkFind objects
collision_layer on target objectsWhich layers the object is onMust overlap with shapecast mask to be detected

How detection works:

  1. TargetingShapeCast2D.collision_mask (e.g., 10) determines which physics layers the shapecast looks for
  2. Placed objects are detected if their collision_layer includes that same layer (e.g., layer 10)

For placed objects to be detected by TargetingShapeCast2D:

  1. Set TargetingShapeCast2D.collision_mask to your targeting layer (e.g., 10)
  2. Set ONLY the root scene node’s collision_layer to include that layer (e.g., 1, 10)
  3. Child physics bodies (StaticBody2D, Area2D) should NOT have the targeting layer

IMPORTANT: Only the scene root should own the targeting layer. If multiple nodes in the hierarchy have the targeting layer, the shapecast may detect the wrong one (e.g., a child collider instead of the intended root target).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Correct: Only root has targeting layer
GeyserPump (root, Node2D):
  collision_layer: 1, 10    # <- Root owns layer 10
  StaticBody2D:
    collision_layer: 1        # <- Child does NOT have layer 10

# Wrong: Multiple nodes have targeting layer (causes wrong target detection)
GeyserPump (root, Node2D):
  collision_layer: 1, 10    # <- Root has layer 10
  StaticBody2D:
    collision_layer: 1, 10    # <- WRONG: Child also has layer 10

When the shapecast detects the scene, it will find whichever node is closest/first - if a child has the targeting layer, the shapecast may target that child instead of the root you intended.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# TargetingShapeCast2D:
ShapeCast2D:
  collision_mask: 10        # ← Shapecast looks for layer 10
  collide_with_areas: true   # ← Enable Area2D detection
  collide_with_bodies: true  # ← Enable StaticBody2D detection (required for most placed objects)

# Placed object (e.g., GeyserPump):
StaticBody2D:
  collision_layer: 1, 10    # ← MUST include layer 10 to be detected
  collision_mask: 10        # ← Doesn't matter for Shapecast2D detection

Common mistake: Setting only collision_mask = 10 on placed objects. This allows the object to detect layer 10 things, but Shapecast2D cannot find the object because the object’s collision_layer doesn’t include 10.

CRITICAL: Shapecast Collision Flags

TargetingShapeCast2D will NOT detect any objects unless you set the collision flags. Godot ShapeCast2D defaults to collide_with_areas = false and collide_with_bodies = false.

In your GridPositioner scene (e.g., grid_positioner_stack_2d.tscn), set BOTH:

1
2
3
4
# TargetingShapeCast2D node in your scene:
TargetingShapeCast2D:
  collide_with_areas: true   # ← Enable Area2D detection (e.g., ColTargetHighlighter)
  collide_with_bodies: true  # ← Enable StaticBody2D detection (e.g., GeyserPump)

If only collide_with_areas = true is set, the shapecast will detect Area2D nodes but NOT StaticBody2D nodes. Most placed game objects use StaticBody2D, so you need collide_with_bodies = true as well.

Quick diagnostic: Add this to your scene and check the Output panel when running:

1
2
func _ready() -> void:
  print("TargetingShapeCast2D: collide_with_areas=" + str($TargetingShapeCast2D.collide_with_areas) + ", collide_with_bodies=" + str($TargetingShapeCast2D.collide_with_bodies))

If either shows false, that type of object will not be detected.


Preview Stability and Manual Targeting

During active building/manipulation flows:

  • TargetingShapeCast2D intentionally stops auto-updating when manual targeting is active.
  • Collision exclusions can be applied so previews do not block their own placement validation.
  • Manipulation visuals are handled by ManipulationParent, not the positioner.

Common Targeting Problems

  • Positioner never appears
    • Check mode/state injection.
    • Check targeting settings visibility rules.
  • Target object never updates / Move/Demolish/Highlight not working
    • FIRST: Check collide_with_areas and collide_with_bodies - see “CRITICAL: Shapecast Collision Flags” above. This is the most common cause.
    • Check TargetingShapeCast2D.collision_mask matches your targeting layer.
    • Check that the targetable object has collision_layer including that same layer.
    • Enable TargetingShapeCast2D.debug_log_collisions = true to see what the shapecast is detecting.
  • Cursor does not move
    • Check GridPositioner2D input settings and target map wiring.
  • Move/build targeting acts frozen
    • Check whether manual targeting is intentionally active.