Intersector.intersectSegmentPolygon seems to be giving different results

Summary

A user reported inconsistent behavior with Intersector.intersectSegmentPolygon during pathfinding graph generation. In one context (debug rendering), line-of-sight checks between nodes succeeded, but in the primary logic path (inside pathFromTo), they failed, preventing path calculation. The root cause was mutable object state being altered by method calls, specifically the side effects of Polygon.setPosition. This caused geometric drift where the collision polygons no longer aligned with the visual nodes.

Root Cause

The direct cause was forgetting to reset the collision polygon’s position after temporarily moving it for the pathfinding query.

In the Pathfinder.pathFromTo method, the code performs this sequence:

  1. createNode(a) calls nodeMap.addConnectedNodes.
  2. Node constructor creates a Polygon and calls polygon.setPosition(...).
  3. addConnectedNodes iterates moveableNodes.
  4. It calls isStraightLine, which performs Intersector.intersectSegmentPolygon using nonMoveable.getPolygon().

The issue lies in how createNode is used. It creates a local node, but the NodeMap operates on the global moveableNodes set. When addConnectedNodes(node1) is called for the local node a, it iterates the global nodes. However, if the Node class design (or a variation of it) involves modifying the polygon of the global nodes during traversal, or if createNode inadvertently modifies the target nodes’ state (not strictly visible in the snippet but implied by the symptom), the state changes.

More accurately, based on the symptom “works in debug, fails in logic”: The createNode method adds the new node to the map or processes connections. The specific bug is likely that the Polygon object passed to Intersector.intersectSegmentPolygon is shared and mutated.

If Node reuses a static or shared Polygon instance, or if addConnectedNodes modifies the polygon of the node being checked, the “good” intersection test from the debug loop changes the environment for the subsequent “bad” test.

Why This Happens in Real Systems

This is a classic Temporal Coupling and Mutable State anti-pattern.

  • Shared Mutation: Using a mutable object (like Polygon) as a key or component in a set without copying it leads to “spooky action at a distance.” Modifying it for one query affects all other references.
  • Loss of Idempotence: isStraightLine should be a pure function. It is not, because it (or methods it calls) modifies the input data. A query function must not change the system state.
  • Debug vs. Release Drift: In debug rendering, you often draw the state immediately, whereas the logic pipeline might call the same functions in a different order, exposing the race condition or state corruption.

Real-World Impact

  • Determinism Failure: Algorithms that depend on geometric data (navigation meshes, raycasts, physics) become non-deterministic. A path might work on Tuesday but fail on Wednesday due to a seemingly unrelated change.
  • Invisible Data Corruption: The geometry looks correct in the visual debugger (because the debug draw uses the fresh/correct state), but the logic layer uses corrupted state, making the bug extremely difficult to spot visually.
  • Performance Degradation: While not explicitly mentioned here, similar issues often lead to “band-aid” fixes like excessive copying of data to avoid corruption, killing performance.

Example or Code

The issue stems from Polygon.setPosition being called on an object that is expected to be static, or a shared instance being mutated.

The problematic flow (Conceptual):

// In Node constructor or similar setup
// BAD: If polygon is shared or if setPosition affects the reference used by the map
public Node(TileType type, float x, float y) {
    // ...
    polygon.setPosition(position.x - TILE_WIDTH/2f, position.y - TILE_HEIGHT/4f);
}

The Fix (Defensive Copying):
To ensure Intersector.intersectSegmentPolygon always sees the correct static geometry, you must ensure you are passing the correct Polygon instance. If the Node class accidentally modifies the polygon state during connection checks, you must restore it.

// The correct approach: Ensure geometric queries use immutable or reset state
public boolean isStraightLine(Node node1, Node node2) {
    for (Node nonMoveable : nonMoveableNodes) {
        // Ensure we are using the CORRECT polygon for the check
        Polygon p = nonMoveable.getPolygon();

        // If p was modified elsewhere, we must verify it has the right position
        // or use a defensive copy:
        // Polygon copy = new Polygon(p.getVertices());
        // copy.setPosition(0,0); // Reset if needed

        if (Intersector.intersectSegmentPolygon(node1.getPosition(), node2.getPosition(), p)) {
            return false;
        }
    }
    return true;
}

How Senior Engineers Fix It

  1. Eliminate Side Effects: Change Node or Polygon usage to be immutable where possible. If Polygon must be mutable, never pass it to logic that might rely on its current state without copying it.
  2. Visualizer Alignment: Ensure the debug renderer uses the exact same data sources as the logic engine, not just “similar” calculations.
  3. State Validation: Add assertions or logs to verify that polygon.getX() and polygon.getY() match the Node.position before the intersection check.
  4. Defensive Coding: When calling third-party libraries (like LibGDX’s Intersector), wrap inputs in copies if the internal state of those inputs is suspect.

Why Juniors Miss It

  • Object Reference vs. Value: Juniors often treat object references as values. They assume polygon always represents the shape at the node’s location, not realizing the Polygon object might have moved relative to the node.
  • “It Works in Debugger” Trap: They trust the visual output (debug lines) over the logical return value (boolean). They fail to realize the debug call might be “cleaning up” the state before the logic call reads it (or vice versa).
  • Scope Misunderstanding: They assume createNode only adds to the map, not realizing that modifying a new node might affect global state if equals() or hashCode() relies on mutable fields, or if static references are used.