NS-23: fix bump departure — stepped pick + goal-only steer
Ground pick: step the ray through steep walkable hits (0.09 < dot UP < 0.64) so a view that grazes bump sides still resolves to flat floor beyond; walls (dot ~0) stop the chain. Player: drop NavigationAgent3D waypoint following; steer only toward the server goal in xz so plateau paths are not dominated by vertical nav segments. move_and_slide handles slopes and obstacles.pull/23/head
parent
725bef1133
commit
05d7916cc7
|
|
@ -1,8 +1,8 @@
|
|||
extends Node3D
|
||||
|
||||
## NS-16: walkable ground ray pick. Emits world target for MoveCommand; camera wired from main.
|
||||
## NS-19: reject vertical faces (pedestal). Bump slopes ~39–45° (dot UP ~0.71–0.78).
|
||||
## 0.82 rejected slope-first picks when leaving plateau; use 0.64. Walls stay ~0.
|
||||
## NS-19: reject vertical faces (pedestal). Stepped ray: steep walkable hits (bump slopes) advance
|
||||
## along the ray so the next hit can be flat floor beyond — fixes “stuck” when leaving plateau.
|
||||
## (No `class_name` so headless/CI can load the project before `.godot` import exists.)
|
||||
|
||||
signal target_chosen(world: Vector3)
|
||||
|
|
@ -10,6 +10,15 @@ signal target_chosen(world: Vector3)
|
|||
## Minimum `hit_normal.dot(Vector3.UP)` to accept a pick (0 = vertical wall, 1 = flat floor).
|
||||
const MIN_WALKABLE_NORMAL_DOT_UP: float = 0.64
|
||||
|
||||
## Nudge past a steep walkable triangle so the ray can hit ground behind (meters along view ray).
|
||||
const STEEP_PICK_THROUGH: float = 0.14
|
||||
|
||||
## Max ray segments (steep walkable → advance) before giving up.
|
||||
const MAX_PICK_SEGMENTS: int = 24
|
||||
|
||||
## Below this dot, treat as wall — do not step through (pedestal sides stay non-pickable).
|
||||
const WALL_NORMAL_DOT_CUTOFF: float = 0.09
|
||||
|
||||
## Fallback when `Viewport.get_camera_3d()` is null (e.g. some editor setups). Set from `main.gd`
|
||||
## in `_ready`.
|
||||
var fallback_camera: Camera3D
|
||||
|
|
@ -29,27 +38,45 @@ func _try_pick(screen_pos: Vector2) -> void:
|
|||
if cam == null:
|
||||
return
|
||||
|
||||
var origin: Vector3 = cam.project_ray_origin(screen_pos)
|
||||
var ray_dir: Vector3 = cam.project_ray_normal(screen_pos)
|
||||
var to: Vector3 = origin + ray_dir * 2000.0
|
||||
var query := PhysicsRayQueryParameters3D.create(origin, to)
|
||||
query.collision_mask = 1
|
||||
if ray_dir.length_squared() < 1e-12:
|
||||
return
|
||||
ray_dir = ray_dir.normalized()
|
||||
var from: Vector3 = cam.project_ray_origin(screen_pos)
|
||||
var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
|
||||
var hit: Dictionary = space.intersect_ray(query)
|
||||
if hit.is_empty():
|
||||
return
|
||||
var collider: Variant = hit.get("collider")
|
||||
if not _collider_is_walkable(collider):
|
||||
return
|
||||
var hit_normal: Variant = hit.get("normal", Vector3.ZERO)
|
||||
if hit_normal is not Vector3:
|
||||
return
|
||||
var nrm: Vector3 = (hit_normal as Vector3).normalized()
|
||||
if nrm.dot(Vector3.UP) < MIN_WALKABLE_NORMAL_DOT_UP:
|
||||
return
|
||||
var hit_pos: Variant = hit.get("position")
|
||||
if hit_pos is Vector3:
|
||||
target_chosen.emit(hit_pos as Vector3)
|
||||
const RAY_LEN: float = 2000.0
|
||||
var chosen: Vector3 = Vector3.ZERO
|
||||
var have_chosen: bool = false
|
||||
|
||||
for _i in range(MAX_PICK_SEGMENTS):
|
||||
var to: Vector3 = from + ray_dir * RAY_LEN
|
||||
var query := PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.collision_mask = 1
|
||||
var hit: Dictionary = space.intersect_ray(query)
|
||||
if hit.is_empty():
|
||||
break
|
||||
if not _collider_is_walkable(hit.get("collider")):
|
||||
break
|
||||
var hit_normal: Variant = hit.get("normal", Vector3.ZERO)
|
||||
if hit_normal is not Vector3:
|
||||
break
|
||||
var nrm: Vector3 = (hit_normal as Vector3).normalized()
|
||||
var ndot: float = nrm.dot(Vector3.UP)
|
||||
if ndot >= MIN_WALKABLE_NORMAL_DOT_UP:
|
||||
var pos_var: Variant = hit.get("position")
|
||||
if pos_var is Vector3:
|
||||
chosen = pos_var as Vector3
|
||||
have_chosen = true
|
||||
break
|
||||
if ndot <= WALL_NORMAL_DOT_CUTOFF:
|
||||
break
|
||||
var hp_var: Variant = hit.get("position")
|
||||
if hp_var is not Vector3:
|
||||
break
|
||||
from = (hp_var as Vector3) + ray_dir * STEEP_PICK_THROUGH
|
||||
|
||||
if have_chosen:
|
||||
target_chosen.emit(chosen)
|
||||
|
||||
|
||||
func _collider_is_walkable(collider: Variant) -> bool:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ const ARRIVE_EPS: float = 0.35
|
|||
const VERT_ARRIVE_EPS: float = 0.055
|
||||
const MAX_CLIMB_SPEED: float = 2.6
|
||||
const MAX_DESCENT_SPEED: float = 2.2
|
||||
const DIRECT_APPROACH_RADIUS: float = 0.85
|
||||
|
||||
var _has_walk_goal: bool = false
|
||||
var _auth_walk_goal: Vector3 = Vector3.ZERO
|
||||
|
|
@ -85,22 +84,8 @@ func _physics_process(_delta: float) -> void:
|
|||
move_and_slide()
|
||||
return
|
||||
|
||||
var nav_map: RID = _nav_agent.get_navigation_map()
|
||||
if NavigationServer3D.map_get_iteration_id(nav_map) == 0:
|
||||
_steer_toward_world_point(_auth_walk_goal)
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
if horiz_dist <= DIRECT_APPROACH_RADIUS:
|
||||
_steer_toward_world_point(_auth_walk_goal)
|
||||
else:
|
||||
var next_path_position: Vector3 = _nav_agent.get_next_path_position()
|
||||
var to_next_h: Vector3 = Vector3(
|
||||
next_path_position.x - global_position.x, 0.0, next_path_position.z - global_position.z
|
||||
)
|
||||
if to_next_h.length_squared() > 0.0025:
|
||||
_steer_toward_world_point(next_path_position)
|
||||
else:
|
||||
_steer_toward_world_point(_auth_walk_goal)
|
||||
|
||||
# Do not follow NavigationAgent3D waypoints: from the bump plateau the first segment often
|
||||
# drops straight down the mesh, and the agent map can disagree with presentation. Steer only
|
||||
# toward the server goal in xz; move_and_slide + floor handles slopes and obstacles.
|
||||
_steer_toward_world_point(_auth_walk_goal)
|
||||
move_and_slide()
|
||||
|
|
|
|||
Loading…
Reference in New Issue