> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/core-engine-concepts/navmesh-and-collision.md).

# Navmesh & Collision

Navmeshes define walkable space for pathfinding and movement constraints.

Navmeshes define the **walkable areas** in your world. Use them to:

* Snap spawned items onto reachable ground
* Lock players/NPCs to walkable regions

Colliders can also be used to carve navmeshes in the editor.

### Components (quick overview)

* **`Navmesh`**: the baked navigation mesh you query and rebuild
* **`Navmesh_Loop`**: polygon loops that define walkable boundaries (and holes)
* **Colliders**: `Box_Collider`, `Circle_Collider`, `Edge_Collider`, `Polygon_Collider`

{% hint style="info" %}
For movement/pathfinding (`Movement_Agent`) see [Movement Agents/NPCs](/core-engine-concepts/movement-agents-npcs.md).
{% endhint %}

### Navmesh API reference

```go
Navmesh :: class : Component {
    // Project a point onto this navmesh.
    // triangle_hint is an acceleration hint you can reuse for nearby queries.
    try_find_closest_point_on_navmesh :: method(
        to_point: v2,
        result: ref v2,
        triangle_hint: ref s64
    ) -> bool;

    // Cast through the navmesh. Returns the endpoint and whether the query succeeded.
    try_raycast :: method(position: v2, direction: v2) -> (v2, bool);

    // Queue a rebuild for the normal navmesh update.
    mark_for_rebuild :: method();

    // Rebuild now when a same-frame query needs the new geometry.
    rebuild_immediately :: method() -> bool;

    // Include only colliders on this entity and its descendants.
    child_colliders_only: bool;

    // Detect input changes and rebuild automatically.
    enable_automatic_rebuilds: bool;
}
```

### Editor setup (building a navmesh)

1. Create an entity and add a `Navmesh` component.
2. Create child entities with `Navmesh_Loop` components to define walkable polygons.
3. For each `Navmesh_Loop`:
   * Add points to define the loop shape
   * Order the outer boundary points counter-clockwise
   * To reverse an existing loop, right-click the `Navmesh_Loop` component header and select **Flip Winding Order**
   * Toggle **Flip Inside Outside** to make a hole/obstacle instead of walkable space
4. Use the navmesh debug options in the inspector to visualize the triangles.

{% hint style="info" %}
By default, a `Navmesh_Loop` defines a walkable area. Flip it to "punch holes" (unwalkable islands) into an existing navmesh.
{% endhint %}

{% hint style="warning" %}
Loop winding determines which side of every edge is treated as the inside. Counter-clockwise is the normal winding for a walkable outer boundary. Clockwise winding reverses that result. If a loop was authored backwards, right-click its component header and select **Flip Winding Order**. Use **Flip Inside Outside** deliberately for holes instead of relying on an accidentally reversed point order.
{% endhint %}

### Colliders and navmesh loops

Colliders can also contribute loops via collider inspector options (for example "Make Navmesh Loop" / "Flip Navmesh Loop").

Important caveats:

* With **Enable Automatic Rebuilds**, navmeshes detect loop, collider, tilemap, and child-navmesh input changes and queue a rebuild.
* Turn on **Child Colliders Only** to ignore unrelated colliders elsewhere in the scene.
* With automatic rebuilds disabled, call `mark_for_rebuild()` after changing an input.
* Call `rebuild_immediately()` only before a same-frame query that requires the changed geometry.

### Spawning on navmesh (clamp to reachable ground)

Use `try_find_closest_point_on_navmesh` to project a desired position onto the closest valid point on a navmesh:

```go
spawn_on_navmesh :: proc(navmesh: Navmesh, desired_position: v2) -> Entity {
    spawn_pos: v2;
    triangle_hint: s64; // 0 = not set yet

    if navmesh.try_find_closest_point_on_navmesh(desired_position, ref spawn_pos, ref triangle_hint) {
        e := Scene.create_entity();
        e.set_local_position(spawn_pos);
        return e;
    }

    return null;
}
```

{% hint style="info" %}
Reuse `triangle_hint` for repeated queries in the same area (loot spawners, wave spawns, etc). It can significantly speed up projection.
{% endhint %}

### Rebuilding navmeshes immediately

Most geometry changes are picked up automatically. Use this only when you need to force timing:

* **`rebuild_immediately()`**: rebuilds now (use only if you must query the updated mesh in the same frame)

```go
ok := navmesh.rebuild_immediately();
if !ok {
    log_info("Navmesh rebuild failed", {});
}
```

{% hint style="info" %}
Parent navmeshes refresh after child navmeshes, so stitched parent meshes automatically pick up child mesh input changes.
{% endhint %}

### Raycasting across a navmesh

`try_raycast` follows `direction` to the navmesh boundary. The second result reports whether the query succeeded.

```go
endpoint, ok := navmesh.try_raycast(start, direction);
if ok {
    // endpoint is on the navmesh boundary.
}
```

### Collider triggers

A collider with `is_trigger == true` can report when another collider enters, remains inside, or exits. Trigger callbacks work on stationary and moving colliders; a `Movement_Agent` is not required.

{% hint style="warning" %}
**Use colliders sparingly.** Only add them to gameplay-critical surfaces: map boundaries, platforms the player must land on, key obstacles, and deliberate trigger volumes. Do **not** add colliders to decorative entities (trees, bushes, background props, etc.). Most entities in a scene should have no collider.
{% endhint %}

```go
Trigger_Listener :: class : Component {
    ao_start :: method() {
        trigger_collider := entity.get_component(Circle_Collider);
        trigger_collider.is_trigger = true;

        trigger_collider.on_trigger_start = proc(self: Collider, other: Collider) {
            log("OVERLAP START: %", {other.entity.get_name()});
        };

        trigger_collider.on_trigger_stay = proc(self: Collider, other: Collider) {
            log("OVERLAP STAY: %", {other.entity.get_name()});
        };

        trigger_collider.on_trigger_end = proc(self: Collider, other: Collider) {
            log("OVERLAP END: %", {other.entity.get_name()});
        };
    }
}
```

### Proximity checks and custom hit logic

For trigger zones that use range queries instead of collider callbacks, pickups, projectile checks, or other custom hit logic, use:

* `Scene.get_all_components_in_range` / `Scene.get_closest_component_in_range`
* Simple distance checks (`in_range`) to decide "inside", "picked up", "hit", etc.

#### Scene query helpers

```go
Scene :: struct {
    get_all_components_in_range :: proc(position: v2, range: float, results: ref [..]$T);
    get_closest_component_in_range :: proc(position: v2, range: float, $T: typeid) -> (T, bool);
}
```

{% hint style="info" %}
Run damage, pickups, and scoring in the normal shared gameplay path. Do not guard predicted gameplay with `Game.is_server()`.
{% endhint %}

#### Trigger volume (enter / stay / exit)

To simulate a trigger zone, keep a list of who was inside last frame and diff it against the current frame’s results.

```go
contains_id :: proc(list: []u64, id: u64) -> bool {
    for x: list if x == id return true;
    return false;
}

Trigger_Zone :: class : Component {
    @ao_serialize radius: float;
    last_inside: [..]u64;

    ao_update :: method(dt: float) {
        center := entity.world_position;

        players: [..]Player;
        Scene.get_all_components_in_range(center, radius, ref players);

        current_inside: [..]u64;

        for p: players {
            if in_range(p.entity.world_position, center, radius) {
                current_inside.append(p.entity.id);

                if !contains_id(last_inside, p.entity.id) {
                    // on_enter
                    Notifier.notify(p, "Entered zone!");
                }
                else {
                    // on_stay
                }
            }
        }

        for id: last_inside {
            if !contains_id(current_inside, id) {
                // on_exit (you may want your own id.player lookup)
            }
        }

        last_inside.clear();
        for id: current_inside {
            last_inside.append(id);
        }
    }
}
```

{% hint style="warning" %}
This manual trigger pattern is still useful when you do not want to add a collider trigger volume. If something is teleported/despawned between updates, you may not get a clean "exit" unless you handle cleanup.
{% endhint %}

#### Pickups (closest in range)

```go
try_pickup_near_player :: proc(player: Player) {
    pos := player.entity.world_position;

    pickup, ok := Scene.get_closest_component_in_range(pos, 1.5, Pickup);
    if ok && pickup != null {
        if in_range(pickup.entity.world_position, pos, 1.5) {
            pickup.claim(player); // your own logic (grant + destroy)
        }
    }
}
```

#### Fast movement "hits" (simple sub-stepping)

If you move quickly (dash, projectile), you can miss narrow targets when checking only the final position. A simple workaround is sub-stepping: sample a few points between last and new position and run the same range queries.

```go
hit_check_move :: proc(last_pos: v2, new_pos: v2) {
    steps := 4;
    for i := 1; i <= steps; i += 1 {
        t := (i.(float)) / (steps.(float));
        p := lerp(last_pos, new_pos, t);

        enemies: [..]Enemy;
        Scene.get_all_components_in_range(p, 1.0, ref enemies);
        for e: enemies {
            if in_range(e.entity.world_position, p, 1.0) {
                // apply hit once, start cooldown, etc.
            }
        }
    }
}
```
