diff --git a/client/README.md b/client/README.md index 6b1f6a6..c349f57 100644 --- a/client/README.md +++ b/client/README.md @@ -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. - **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`). ### Manual check (NS-16 + NS-23) diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 92537e9..e7b9bf2 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -20,7 +20,11 @@ var _move_reject_msg_token: int = 0 func _ready() -> void: 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.connect("target_chosen", Callable(self, "_on_target_chosen")) _authority.connect( diff --git a/client/scripts/player.gd b/client/scripts/player.gd index f2b5e2d..d323ebb 100644 --- a/client/scripts/player.gd +++ b/client/scripts/player.gd @@ -4,43 +4,64 @@ extends CharacterBody3D ## `MoveCommand` (NS-16, NS-19); boot snap vs post-verify nav goal from `main.gd`. 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 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. 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. func clear_nav_goal() -> void: 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. func snap_to_server(world_pos: Vector3) -> void: global_position = world_pos velocity = Vector3.ZERO - _nav_agent.target_position = world_pos + _nav_agent.set_target_position(world_pos) 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 move_and_slide() 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 to_next: Vector3 = next_path_position - global_position to_next.y = 0.0 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: velocity = to_next.normalized() * MOVE_SPEED velocity.y = 0.0