Navigation for guards

hammerite-nav derives a level's walkable ground from the carved rooms - no voxels, no cell size - and hands it to Godot's navigation server as ordinary regions and links. A guard then walks it with a NavigationAgent3D as in any Godot game. What Hammerite adds is measurement: every patch of ground knows its headroom and width, every windowsill and drop knows its height, and your game's agent profiles decide who fits where. That is how a thief climbs through the scullery window and the guard chasing them has to go round.

1. Say who walks

An HammeriteNavigationAgentProfile is one kind of body: how tall, how wide, how high it can climb and how far it will drop. Each profile gets a navigation layer of its own, and the ground is baked onto the layers of every profile that fits it.

var guard := HammeriteNavigationAgentProfile.new()
guard.profile_name = &"guard"
guard.navigation_layer = 1
guard.height = 96.0
guard.climb_height = 32.0

var thief := HammeriteNavigationAgentProfile.new()
thief.profile_name = &"thief"
thief.navigation_layer = 2
thief.height = 72.0
thief.crouch_height = 40.0
thief.climb_height = 96.0

Normally the table is a .tres HammeriteNavigationAgentProfiles edited in the inspector. Distances are in map units, the same as the brushes. Two profiles on one layer are a mistake that would otherwise be silent - validate() says so.

2. Bake

Set the profiles before any map is assigned, then hand each HammeriteMap3D to the addon:

HammeriteNavigation.profiles = load("res://ai/agent_profiles.tres")
HammeriteNavigation.integrate(map_3d)
map_3d.map = map

Navigation is built as the map is, and again whenever it changes or another variant is made active; HammeriteNavigation.navigation_built reports each build. A level's ground is derived in a fraction of a second, and a map saved as a resource keeps a cache beside it (save_cache(), which the editor does on every save) so a shipped game loads it instead.

3. Walk

The regions are ordinary Godot navigation regions, so a guard uses an ordinary agent, on its profile's layer:

@onready var _agent: NavigationAgent3D = $NavigationAgent3D

func _ready() -> void:
    _agent.navigation_layers = HammeriteNavigation.layer_bit(&"guard")

func go_to(point: Vector3) -> void:
    _agent.target_position = point

func _physics_process(_delta: float) -> void:
    if _agent.is_navigation_finished():
        return
    velocity = global_position.direction_to(_agent.get_next_path_position()) * walk_speed
    move_and_slide()

Keep the 3D direction rather than flattening it: a route through a window climbs.

Before walking to a point that came from somewhere else - a sound's apparent position in a doorway, mid-air - snap it to the ground, and check the route arrives: the server returns the nearest it could manage rather than nothing.

var nav_map: RID = get_world_3d().navigation_map
var goal: Vector3 = NavigationServer3D.map_get_closest_point(nav_map, heard_at)
var path: PackedVector3Array = NavigationServer3D.map_get_path(nav_map, global_position, goal, true,
    HammeriteNavigation.layer_bit(&"guard"))
var reachable: bool = path.size() >= 2 and path[path.size() - 1].distance_to(goal) < 40.0

A large level may need NavigationAgent3D.path_search_max_polygons raised from its default of 4096, or long routes come up short.

Two rooms that abut join on their own: an ordinary doorway is not a link. A link is where the ground jumps - a step, a windowsill, a drop, a gap - and those are derived from the geometry and costed per profile, so a guard who cannot climb a sill never routes through the window.

What geometry cannot show - a ladder, a rope - an author places as a nav_link entity, aimed at its far end with nav_link_target.

A link can be switched while the game runs, by name. A window or a drop takes the name of the opening it goes through, the same name audio reads, so one name shuts both:

HammeriteAudio.set_portal_occlusion("scullery_window", 1.0, self)
HammeriteNavigation.set_link_enabled("scullery_window", false)

A doorway is not a link, so shutting a door does not close the route through it. Whether a guard opens a door, or waits, or picks a lock, is your AI's to decide; navigation only says the floor goes on.

5. What a room costs

An author can make a room dearer to cross - a lit hall a thief would rather go round - with the Navigation section of an air brush (nav/travel_cost, a multiplier on distance). A game that changes it while running rebuilds:

brush.set_property(HammeriteNavigationBrushProperties.KEY_TRAVEL_COST, 3.0)
HammeriteNavigation.rebuild(map_3d)

rebuild() is also the call after any other change navigation should notice at runtime - a door that has become a wall. Nothing is rebuilt behind your back.

Props are inert to navigation until marked: a prop_model's nav_role says whether it is an obstacle, something to stand on, or both, and the bake warns how many were left unmarked.

See also

Hammerite 1.0 · built from a6dd634, 2026-10-10