OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / foot_walk_gate.gd

195 lines · gd ref:
extends Node3D
## Does the SWING foot clear ground that is rising to meet it?
##
## foot_slope_gate holds a STANDING character, where swing is zero -- which is why
## standing is provably unchanged by the gait work and equally why that gate
## cannot see the feature at all. This one walks.
##
## WHAT IT MEASURES, over a real walk cycle on a real slope:
##   - PEAK CLEARANCE: how high above the surface the swing foot gets. On a slope
##     it must be HIGHER than on the flat, because the ground is coming up to meet
##     it -- that is the whole of the gait change.
##   - WORST PENETRATION: how far the lowest foot goes BELOW the surface. Ground
##     adaptation that clears the swing at the cost of sinking the stance foot has
##     traded one artifact for another.
##   - Whether the cycle ran at all, since none of it means anything at a standstill.
##
## Run:
##   "$HYPER_TPS_GODOT" --headless res://scenes/test/foot_walk_gate.tscn

const SLOPES := [0.0, 20.0, 35.0]
const SETTLE := 40         ## frames to land and reach walking speed
const WALK := 150          ## frames of walking to sample

var _plan: Array = []
var _idx := 0
var _frames := 0
var _player: Node = null
var _ground: StaticBody3D = null
var _skel: Node = null
var _peak := [-1e9, -1e9]
var _worst := [1e9, 1e9]
var _speed_sum := 0.0
var _speed_n := 0
var _phase_seen := 0.0
var _rows: Array = []
var _done := false


func _ready() -> void:
	process_priority = 100
	for s in SLOPES:
		_plan.append(s)
	_build()


func _build() -> void:
	var deg: float = float(_plan[_idx])
	var s: float = tan(deg_to_rad(deg))
	_ground = StaticBody3D.new()
	var cs := CollisionShape3D.new()
	var f := PackedVector3Array()
	var i := 0
	while i < 80:
		var j := 0
		while j < 80:
			var x0 := float(i) * 1.5 - 60.0
			var z0 := float(j) * 1.5 - 60.0
			var x1 := x0 + 1.5
			var z1 := z0 + 1.5
			var y0 := x0 * s
			var y1 := x1 * s
			# CW so the normal points UP. [[feedback_godot-cw-winding]]
			f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z1)); f.push_back(Vector3(x0, y0, z1))
			f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z0)); f.push_back(Vector3(x1, y1, z1))
			j += 1
		i += 1
	var tri := ConcavePolygonShape3D.new()
	tri.set_faces(f)
	cs.shape = tri
	_ground.add_child(cs)
	add_child(_ground)

	var scene: PackedScene = load("res://scenes/player/player_procedural.tscn")
	_player = scene.instantiate()
	add_child(_player)
	_player.global_position = Vector3(0.0, 0.6, 0.0)
	# Walk ACROSS the slope: the traverse holds a steady speed without climbing,
	# so the cycle runs at a constant rate and the samples are comparable between
	# slopes. Climbing would confound clearance with a changing speed.
	_player.camera_head.global_transform = Transform3D(Basis.IDENTITY, Vector3.ZERO)
	_player.input_sync.input_direction = Vector2(0.0, -1.0)
	_skel = null
	_frames = 0
	_peak = [-1e9, -1e9]
	_worst = [1e9, 1e9]
	_speed_sum = 0.0
	_speed_n = 0
	_phase_seen = 0.0


func _physics_process(_d: float) -> void:
	if _done or _player == null:
		return
	if _skel == null:
		_skel = _find_skel(_player)
		if _skel != null and _skel.has_signal("skeleton_updated"):
			_skel.skeleton_updated.connect(_on_skeleton_updated)
	# Hold the input: the controller reads it every frame.
	_player.input_sync.input_direction = Vector2(0.0, -1.0)
	_frames += 1


func _on_skeleton_updated() -> void:
	if _done or _skel == null or _frames < SETTLE:
		return
	var deg: float = float(_plan[_idx])
	var s: float = tan(deg_to_rad(deg))
	var c: float = cos(deg_to_rad(deg))
	var names := ["DEF-foot.L", "DEF-foot.R"]
	for i in range(2):
		var bi: int = _skel.find_bone(names[i])
		if bi < 0:
			continue
		var p: Vector3 = (_skel.global_transform * _skel.get_bone_global_pose(bi)).origin
		# Perpendicular height above the slope plane y = s*x.
		var h: float = (p.y - s * p.x) * c
		if h > _peak[i]:
			_peak[i] = h
		if h < _worst[i]:
			_worst[i] = h
	var anim: Node = _find(_player, "ProceduralAnimator")
	if anim != null:
		_speed_sum += float(anim.get("horizontal_speed"))
		_speed_n += 1
		var ph: float = float(anim.get("_cycle_phase"))
		if ph > _phase_seen:
			_phase_seen = ph


func _process(_d: float) -> void:
	if _done or _player == null or _frames < SETTLE + WALK:
		return
	var deg: float = float(_plan[_idx])
	var spd: float = (_speed_sum / float(_speed_n)) if _speed_n > 0 else 0.0
	_rows.append({"slope": deg, "peak": maxf(_peak[0], _peak[1]),
			"worst": minf(_worst[0], _worst[1]), "speed": spd,
			"phase": _phase_seen, "samples": _speed_n})
	_player.queue_free(); _player = null
	_ground.queue_free(); _ground = null
	_idx += 1
	if _idx >= _plan.size():
		_report()
		_done = true
		get_tree().quit(0)
		return
	_build()


func _report() -> void:
	print("")
	print("slope   avg speed   cycle ran   peak clearance   worst penetration")
	print("---------------------------------------------------------------------")
	var flat_peak := 0.0
	for r in _rows:
		if r["slope"] == 0.0:
			flat_peak = r["peak"]
	for r in _rows:
		var ran: String = "yes" if r["phase"] > 0.01 else "NO -- standing"
		print("%5.0f   %7.2f m/s   %-13s %8.4f m      %+9.4f m"
				% [r["slope"], r["speed"], ran, r["peak"], r["worst"]])
	print("")
	print("Peak clearance is measured against the FLAT row (%.4f m): on a slope the" % flat_peak)
	print("swing foot must get HIGHER, because the ground is rising to meet it.")
	for r in _rows:
		if r["slope"] > 0.0:
			print("   %2.0f deg: %+.4f m vs flat" % [r["slope"], r["peak"] - flat_peak])
	var any_standing := false
	for r in _rows:
		if r["phase"] <= 0.01:
			any_standing = true
	if any_standing:
		print("")
		print("RIG FAILED: the walk cycle never advanced in at least one row, so those")
		print("rows measure a standing character and say nothing about gait.")


func _find(n: Node, nm: String) -> Node:
	if n.name == nm:
		return n
	for c in n.get_children():
		var f: Node = _find(c, nm)
		if f != null:
			return f
	return null


func _find_skel(n: Node) -> Node:
	if n.get_class() == "Skeleton3D":
		return n
	for c in n.get_children():
		var f: Node = _find_skel(c)
		if f != null:
			return f
	return null