NS-23: Fix nav movement (sync bake, map guard, path/fallback steering)

pull/23/head
VinPropane 2026-04-05 00:29:58 -04:00
parent 0ac059fa3b
commit f49c8b88d2
3 changed files with 33 additions and 8 deletions

View File

@ -14,7 +14,7 @@ Do not grow an all-in-one **`main.gd`**. The main scene root should **compose**
With the game server running ([`server/README.md`](../server/README.md)), each valid floor click sends a **`MoveCommand`** (**`POST`**) and a follow-up **`GET`** for **`PositionState`**. The server still **snaps** authority to the target (NS-16/19); the client **walks** along a **baked navigation mesh** (`NavigationRegion3D` + **`NavigationAgent3D`** on the player) toward that verified position instead of teleporting on the **`GET`** (NS-23). **Boot** `sync_from_server()` **snaps** once so spawn matches the server. With the game server running ([`server/README.md`](../server/README.md)), each valid floor click sends a **`MoveCommand`** (**`POST`**) and a follow-up **`GET`** for **`PositionState`**. The server still **snaps** authority to the target (NS-16/19); the client **walks** along a **baked navigation mesh** (`NavigationRegion3D` + **`NavigationAgent3D`** on the player) toward that verified position instead of teleporting on the **`GET`** (NS-23). **Boot** `sync_from_server()` **snaps** once so spawn matches the server.
- **Scripts:** `scripts/ground_pick.gd` (walkable pick + `target_chosen`), `scripts/position_authority_client.gd` (`PositionAuthorityClient`: POST move, GET verify; second signal arg = boot snap vs nav goal), `scripts/player.gd` (path-follow), thin `scripts/main.gd` (nav bake on first frame, then wiring). - **Scripts:** `scripts/ground_pick.gd` (walkable pick + `target_chosen`), `scripts/position_authority_client.gd` (`PositionAuthorityClient`: POST move, GET verify; second signal arg = boot snap vs nav goal), `scripts/player.gd` (path-follow), thin `scripts/main.gd` (nav bake on first frame, then wiring).
- **Scene:** `scenes/main.tscn` — walkable **`StaticBody3D`** geometry lives under **`World/NavigationRegion3D`** so **`bake_navigation_mesh()`** (called from `main.gd` after one **`process_frame`**) picks up floor, NS-19 props, and the **`Obstacle`** cube. After moving floor or obstacles, open the scene in the editor and **Bake NavigationMesh** on the region if you need to refresh cached mesh data. - **Scene:** `scenes/main.tscn` — walkable **`StaticBody3D`** geometry lives under **`World/NavigationRegion3D`**. `main.gd` calls **`bake_navigation_mesh(false)`** (main-thread bake) after one **`process_frame`**, then waits two **`physics_frame`**s so **`NavigationServer3D`** has a map before agents query paths. After moving floor or obstacles, re-bake in the editor if needed.
- **Inspector:** select **`PositionAuthorityClient`** on the main scene to set **`base_url`** (default `http://127.0.0.1:5253`) and **`dev_player_id`** (default `dev-local-1`, must match server `Game:DevPlayerId`). - **Inspector:** select **`PositionAuthorityClient`** on the main scene to set **`base_url`** (default `http://127.0.0.1:5253`) and **`dev_player_id`** (default `dev-local-1`, must match server `Game:DevPlayerId`).
### Manual check (NS-16 + NS-23) ### Manual check (NS-16 + NS-23)

View File

@ -20,7 +20,11 @@ var _move_reject_msg_token: int = 0
func _ready() -> void: func _ready() -> void:
await get_tree().process_frame await get_tree().process_frame
_nav_region.bake_navigation_mesh() # Default `bake_navigation_mesh()` bakes on a background thread; sync bake ensures the map
# exists before any `NavigationAgent3D` queries (see Godot navigation_using_navigationagents).
_nav_region.bake_navigation_mesh(false)
await get_tree().physics_frame
await get_tree().physics_frame
_ground_pick.set("fallback_camera", _camera) _ground_pick.set("fallback_camera", _camera)
_ground_pick.connect("target_chosen", Callable(self, "_on_target_chosen")) _ground_pick.connect("target_chosen", Callable(self, "_on_target_chosen"))
_authority.connect( _authority.connect(

View File

@ -4,43 +4,64 @@ extends CharacterBody3D
## `MoveCommand` (NS-16, NS-19); boot snap vs post-verify nav goal from `main.gd`. ## `MoveCommand` (NS-16, NS-19); boot snap vs post-verify nav goal from `main.gd`.
const MOVE_SPEED: float = 5.0 const MOVE_SPEED: float = 5.0
## If the nav mesh is missing or the agent gives up, steer on XZ toward `target_position`.
const FALLBACK_ARRIVE_EPS: float = 0.4
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D @onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void: func _ready() -> void:
_nav_agent.target_position = global_position _nav_agent.set_target_position(global_position)
## After successful move POST + GET: walk to server target without teleporting. ## After successful move POST + GET: walk to server target without teleporting.
func set_authoritative_nav_goal(world_pos: Vector3) -> void: func set_authoritative_nav_goal(world_pos: Vector3) -> void:
_nav_agent.target_position = world_pos _nav_agent.set_target_position(world_pos)
## NS-19 rejection or cancel pending path without moving the body. ## NS-19 rejection or cancel pending path without moving the body.
func clear_nav_goal() -> void: func clear_nav_goal() -> void:
velocity = Vector3.ZERO velocity = Vector3.ZERO
_nav_agent.target_position = global_position _nav_agent.set_target_position(global_position)
## Boot (and any hard reconcile): teleport to server position and reset agent. ## Boot (and any hard reconcile): teleport to server position and reset agent.
func snap_to_server(world_pos: Vector3) -> void: func snap_to_server(world_pos: Vector3) -> void:
global_position = world_pos global_position = world_pos
velocity = Vector3.ZERO velocity = Vector3.ZERO
_nav_agent.target_position = world_pos _nav_agent.set_target_position(world_pos)
func _physics_process(_delta: float) -> void: func _physics_process(_delta: float) -> void:
if _nav_agent.is_navigation_finished(): var nav_map: RID = _nav_agent.get_navigation_map()
if NavigationServer3D.map_get_iteration_id(nav_map) == 0:
velocity = Vector3.ZERO velocity = Vector3.ZERO
move_and_slide() move_and_slide()
return return
if _nav_agent.is_navigation_finished():
var target: Vector3 = _nav_agent.target_position
var flat_target := Vector3(target.x, global_position.y, target.z)
var to_target: Vector3 = flat_target - global_position
to_target.y = 0.0
if to_target.length_squared() > FALLBACK_ARRIVE_EPS * FALLBACK_ARRIVE_EPS:
velocity = to_target.normalized() * MOVE_SPEED
velocity.y = 0.0
else:
velocity = Vector3.ZERO
move_and_slide()
return
var next_path_position: Vector3 = _nav_agent.get_next_path_position() var next_path_position: Vector3 = _nav_agent.get_next_path_position()
var to_next: Vector3 = next_path_position - global_position var to_next: Vector3 = next_path_position - global_position
to_next.y = 0.0 to_next.y = 0.0
if to_next.length_squared() < 0.0001: if to_next.length_squared() < 0.0001:
velocity = Vector3.ZERO var to_final: Vector3 = _nav_agent.target_position - global_position
to_final.y = 0.0
if to_final.length_squared() > 0.0001:
velocity = to_final.normalized() * MOVE_SPEED
velocity.y = 0.0
else:
velocity = Vector3.ZERO
else: else:
velocity = to_next.normalized() * MOVE_SPEED velocity = to_next.normalized() * MOVE_SPEED
velocity.y = 0.0 velocity.y = 0.0