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.
4. Links
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
- Rooms, portals and line of sight - the rooms the ground is cut from.
- Hearing for stealth gameplay - where a guard decides to go.
HammeriteNavigation,HammeriteNavigationBakeReport, and the hammerite-nav README.