NS-23: Simplify path-follow — drop bump/step-down heuristics
Restore a straight Godot nav pattern: steer toward get_next_path_position until finish or final approach, then toward goal; horizontal-only arrival by xz. Removes footprint rays, bump-step group, and step-off vy layering that fought the engine. Slightly raise Player floor_snap_length for small ledges.pull/23/head
parent
25d21c0011
commit
ee6c4b2718
|
|
@ -201,7 +201,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5, 0.9, -5)
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
floor_max_angle = 0.872665
|
floor_max_angle = 0.872665
|
||||||
floor_snap_length = 0.28
|
floor_snap_length = 0.38
|
||||||
safe_margin = 0.045
|
safe_margin = 0.045
|
||||||
script = ExtResource("2_player")
|
script = ExtResource("2_player")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,39 +2,18 @@ extends CharacterBody3D
|
||||||
|
|
||||||
## NS-23: Follow server move target on walkable geometry.
|
## NS-23: Follow server move target on walkable geometry.
|
||||||
##
|
##
|
||||||
## Horizontal velocity toward nav waypoint or goal; `velocity.y` stays 0 except a short **step-off**
|
## Plain Godot pattern: horizontal `velocity` toward `NavigationAgent3D`’s next path point (or the
|
||||||
## nudge (see `_use_direct_step_down_steering`) so stepped bumps can drop without full gravity.
|
## goal when finishing / final approach). `velocity.y` stays 0; height comes from `move_and_slide`
|
||||||
## Move **legality** is server MoveCommand only — not reimplemented here.
|
## and floor handling. No custom step-down / bump heuristics — obstacle pathing is best-effort;
|
||||||
##
|
## small steps rely on the engine + scene (snap, collider shapes). Move **legality** is server-only.
|
||||||
## Nav for detours; short local step-down (feet vs goal Y, not body origin) may bypass waypoints.
|
|
||||||
|
|
||||||
const MOVE_SPEED: float = 5.0
|
const MOVE_SPEED: float = 5.0
|
||||||
const ARRIVE_EPS: float = 0.35
|
const ARRIVE_EPS: float = 0.35
|
||||||
const VERT_ARRIVE_EPS: float = 0.055
|
## Stop when close in xz only; server `y` may be surface or capsule center.
|
||||||
const DIRECT_APPROACH_RADIUS: float = 0.85
|
const DIRECT_APPROACH_RADIUS: float = 0.85
|
||||||
const FLOOR_PROBE_DOWN: float = 4.0
|
|
||||||
const STEP_OFF_FLOOR_MARGIN: float = 0.04
|
|
||||||
## Only small step-downs (not cliffs); allows a wider horiz cap without bee-lining big drops.
|
|
||||||
const STEP_OFF_MIN_DROP: float = 0.055
|
|
||||||
const STEP_OFF_MAX_DROP: float = 0.24
|
|
||||||
## Past this, only direct step-down when footprint still touches a bump-step collider (nav rim).
|
|
||||||
const STEP_OFF_CLOSE_HORIZ: float = 8.0
|
|
||||||
## Match server `MovementValidation.MaxHorizontalStep` default; no bee-line past this.
|
|
||||||
const STEP_OFF_MAX_HORIZ_AUTH: float = 18.0
|
|
||||||
const FLOOR_PROBE_BODY_Y_OFFSET: float = -0.55
|
|
||||||
## Extra xz rays for step-off; max Y keeps bump support when center ray sees floor first.
|
|
||||||
const STEP_FOOTPRINT_HALF: float = 0.2
|
|
||||||
## Downward vy only while step-off bypass is active (not global gravity).
|
|
||||||
const STEP_OFF_DESCEND_SPEED: float = 1.75
|
|
||||||
## If next waypoint is within this xz of us, steer to goal (rim / underfoot waypoints).
|
|
||||||
const NEXT_WAYPOINT_MIN_HORIZ_SQ: float = 0.01
|
|
||||||
## Editor group on stepped bump colliders; value must match `main.tscn`.
|
|
||||||
const WALK_BUMP_STEP_GROUP: StringName = &"ns19_bump"
|
|
||||||
|
|
||||||
var _has_walk_goal: bool = false
|
var _has_walk_goal: bool = false
|
||||||
var _auth_walk_goal: Vector3 = Vector3.ZERO
|
var _auth_walk_goal: Vector3 = Vector3.ZERO
|
||||||
var _footprint_cache_frame: int = -1
|
|
||||||
var _footprint_cache: Dictionary = {}
|
|
||||||
|
|
||||||
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
|
@onready var _nav_agent: NavigationAgent3D = $NavigationAgent3D
|
||||||
|
|
||||||
|
|
@ -62,128 +41,6 @@ func snap_to_server(world_pos: Vector3) -> void:
|
||||||
_nav_agent.set_target_position(world_pos)
|
_nav_agent.set_target_position(world_pos)
|
||||||
|
|
||||||
|
|
||||||
func _ray_floor_hit(from: Vector3) -> Dictionary:
|
|
||||||
var space: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
|
|
||||||
var to: Vector3 = from + Vector3(0.0, -FLOOR_PROBE_DOWN, 0.0)
|
|
||||||
var q: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, to)
|
|
||||||
q.collision_mask = 1
|
|
||||||
q.exclude = [get_rid()]
|
|
||||||
return space.intersect_ray(q)
|
|
||||||
|
|
||||||
|
|
||||||
func _ray_floor_y_at(from: Vector3) -> float:
|
|
||||||
var hit: Dictionary = _ray_floor_hit(from)
|
|
||||||
if hit.is_empty():
|
|
||||||
return from.y
|
|
||||||
var pv: Variant = hit.get("position")
|
|
||||||
if pv is Vector3:
|
|
||||||
return (pv as Vector3).y
|
|
||||||
return from.y
|
|
||||||
|
|
||||||
|
|
||||||
func _collider_in_bump_step_group(collider: Variant) -> bool:
|
|
||||||
var n: Node = collider as Node
|
|
||||||
while n:
|
|
||||||
if n.is_in_group(WALK_BUMP_STEP_GROUP):
|
|
||||||
return true
|
|
||||||
n = n.get_parent()
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _floor_y_under_body() -> float:
|
|
||||||
var y_center: float = _ray_floor_y_at(global_position)
|
|
||||||
var lower: Vector3 = global_position + Vector3(0.0, FLOOR_PROBE_BODY_Y_OFFSET, 0.0)
|
|
||||||
var y_lower: float = _ray_floor_y_at(lower)
|
|
||||||
return maxf(y_center, y_lower)
|
|
||||||
|
|
||||||
|
|
||||||
## One ray pass: max floor Y under footprint + whether any hit uses `WALK_BUMP_STEP_GROUP`.
|
|
||||||
func _compute_footprint_step_down_state() -> Dictionary:
|
|
||||||
var p: Vector3 = global_position
|
|
||||||
var h: float = STEP_FOOTPRINT_HALF
|
|
||||||
var oxz: Array[Vector3] = [
|
|
||||||
Vector3(h, 0.0, 0.0),
|
|
||||||
Vector3(-h, 0.0, 0.0),
|
|
||||||
Vector3(0.0, 0.0, h),
|
|
||||||
Vector3(0.0, 0.0, -h),
|
|
||||||
Vector3(h, 0.0, h),
|
|
||||||
Vector3(h, 0.0, -h),
|
|
||||||
Vector3(-h, 0.0, h),
|
|
||||||
Vector3(-h, 0.0, -h),
|
|
||||||
]
|
|
||||||
var best_y: float = 0.0
|
|
||||||
var got: bool = false
|
|
||||||
var on_bump: bool = false
|
|
||||||
for yoff: float in [0.0, FLOOR_PROBE_BODY_Y_OFFSET]:
|
|
||||||
var from_c: Vector3 = p + Vector3(0.0, yoff, 0.0)
|
|
||||||
var hit_c: Dictionary = _ray_floor_hit(from_c)
|
|
||||||
if not hit_c.is_empty():
|
|
||||||
var yc: float = (hit_c.get("position") as Vector3).y
|
|
||||||
if not got:
|
|
||||||
best_y = yc
|
|
||||||
got = true
|
|
||||||
else:
|
|
||||||
best_y = maxf(best_y, yc)
|
|
||||||
if not on_bump and _collider_in_bump_step_group(hit_c.get("collider")):
|
|
||||||
on_bump = true
|
|
||||||
for d: Vector3 in oxz:
|
|
||||||
for yoff2: float in [0.0, FLOOR_PROBE_BODY_Y_OFFSET]:
|
|
||||||
var from_o: Vector3 = p + d + Vector3(0.0, yoff2, 0.0)
|
|
||||||
var hit_o: Dictionary = _ray_floor_hit(from_o)
|
|
||||||
if not hit_o.is_empty():
|
|
||||||
var yo: float = (hit_o.get("position") as Vector3).y
|
|
||||||
if not got:
|
|
||||||
best_y = yo
|
|
||||||
got = true
|
|
||||||
else:
|
|
||||||
best_y = maxf(best_y, yo)
|
|
||||||
if not on_bump and _collider_in_bump_step_group(hit_o.get("collider")):
|
|
||||||
on_bump = true
|
|
||||||
if not got:
|
|
||||||
best_y = _floor_y_under_body()
|
|
||||||
return {"max_y": best_y, "touches_bump_step": on_bump}
|
|
||||||
|
|
||||||
|
|
||||||
func _footprint_step_down_state() -> Dictionary:
|
|
||||||
var f: int = Engine.get_physics_frames()
|
|
||||||
if f != _footprint_cache_frame:
|
|
||||||
_footprint_cache = _compute_footprint_step_down_state()
|
|
||||||
_footprint_cache_frame = f
|
|
||||||
return _footprint_cache
|
|
||||||
|
|
||||||
|
|
||||||
## Footing under goal xz (surface Y). Server goal.y may be capsule center (e.g. 0.9) or pick surface
|
|
||||||
## (e.g. 0) — do not compare that directly to `_floor_y_under_body()` (always surface).
|
|
||||||
func _destination_footing_y() -> float:
|
|
||||||
var g: Vector3 = _auth_walk_goal
|
|
||||||
var probe_top: float = maxf(global_position.y, g.y) + 4.0
|
|
||||||
var probe: Vector3 = Vector3(g.x, probe_top, g.z)
|
|
||||||
var y: float = _ray_floor_y_at(probe)
|
|
||||||
if y >= probe_top - 0.05:
|
|
||||||
return g.y
|
|
||||||
return y
|
|
||||||
|
|
||||||
|
|
||||||
func _use_direct_step_down_steering() -> bool:
|
|
||||||
var st: Dictionary = _footprint_step_down_state()
|
|
||||||
var floor_y: float = float(st["max_y"])
|
|
||||||
var on_bump: bool = bool(st["touches_bump_step"])
|
|
||||||
var dest_floor_y: float = _destination_footing_y()
|
|
||||||
if dest_floor_y >= floor_y - STEP_OFF_FLOOR_MARGIN:
|
|
||||||
return false
|
|
||||||
var drop: float = floor_y - dest_floor_y
|
|
||||||
if drop < STEP_OFF_MIN_DROP or drop > STEP_OFF_MAX_DROP:
|
|
||||||
return false
|
|
||||||
var hd: float = Vector2(
|
|
||||||
_auth_walk_goal.x - global_position.x, _auth_walk_goal.z - global_position.z
|
|
||||||
).length()
|
|
||||||
if hd > STEP_OFF_MAX_HORIZ_AUTH:
|
|
||||||
return false
|
|
||||||
if on_bump:
|
|
||||||
return true
|
|
||||||
return hd <= STEP_OFF_CLOSE_HORIZ
|
|
||||||
|
|
||||||
|
|
||||||
func _set_horizontal_velocity_toward(point: Vector3) -> void:
|
func _set_horizontal_velocity_toward(point: Vector3) -> void:
|
||||||
var pos := global_position
|
var pos := global_position
|
||||||
var dh: Vector3 = Vector3(point.x - pos.x, 0.0, point.z - pos.z)
|
var dh: Vector3 = Vector3(point.x - pos.x, 0.0, point.z - pos.z)
|
||||||
|
|
@ -201,14 +58,7 @@ func _physics_process(_delta: float) -> void:
|
||||||
|
|
||||||
var full_to_goal: Vector3 = _auth_walk_goal - global_position
|
var full_to_goal: Vector3 = _auth_walk_goal - global_position
|
||||||
var horiz_dist: float = Vector2(full_to_goal.x, full_to_goal.z).length()
|
var horiz_dist: float = Vector2(full_to_goal.x, full_to_goal.z).length()
|
||||||
var vert_err_capsule: float = absf(full_to_goal.y)
|
if horiz_dist <= ARRIVE_EPS:
|
||||||
var dest_foot_y: float = _destination_footing_y()
|
|
||||||
var fp: Dictionary = _footprint_step_down_state()
|
|
||||||
var max_foot_y: float = float(fp["max_y"])
|
|
||||||
# Same lip issue as step-off: `_floor_y_under_body()` can read floor early and match `dest_foot_y`
|
|
||||||
# while xz is within ARRIVE_EPS — false “arrived”, goal clears. Use footprint max_y.
|
|
||||||
var footing_match: bool = absf(max_foot_y - dest_foot_y) <= VERT_ARRIVE_EPS
|
|
||||||
if horiz_dist <= ARRIVE_EPS and (vert_err_capsule <= VERT_ARRIVE_EPS or footing_match):
|
|
||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
_has_walk_goal = false
|
_has_walk_goal = false
|
||||||
_nav_agent.set_target_position(global_position)
|
_nav_agent.set_target_position(global_position)
|
||||||
|
|
@ -221,22 +71,9 @@ func _physics_process(_delta: float) -> void:
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
return
|
return
|
||||||
|
|
||||||
if _use_direct_step_down_steering():
|
if _nav_agent.is_navigation_finished() or horiz_dist <= DIRECT_APPROACH_RADIUS:
|
||||||
_set_horizontal_velocity_toward(_auth_walk_goal)
|
|
||||||
velocity.y = -STEP_OFF_DESCEND_SPEED
|
|
||||||
move_and_slide()
|
|
||||||
return
|
|
||||||
|
|
||||||
if horiz_dist <= DIRECT_APPROACH_RADIUS:
|
|
||||||
_set_horizontal_velocity_toward(_auth_walk_goal)
|
_set_horizontal_velocity_toward(_auth_walk_goal)
|
||||||
else:
|
else:
|
||||||
var next_path_position: Vector3 = _nav_agent.get_next_path_position()
|
_set_horizontal_velocity_toward(_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() > NEXT_WAYPOINT_MIN_HORIZ_SQ:
|
|
||||||
_set_horizontal_velocity_toward(next_path_position)
|
|
||||||
else:
|
|
||||||
_set_horizontal_velocity_toward(_auth_walk_goal)
|
|
||||||
|
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue