extends Node3D
## MINIMAL REPRO. A bare CharacterBody3D on the same planar ramp, driven with a
## constant horizontal velocity and nothing else -- no controller, no animator, no
## foot IK. If this loses the floor, the bounce is Godot's grounded move on a
## trimesh and our movement model is innocent. If it does not, it is ours.
##
## "$HYPER_TPS_GODOT" --headless res://scenes/test/bare_slide_probe.tscn
const SLOPES := [5.0, 15.0, 20.0, 50.0]
const FRAMES := 200
const SPEED := 8.0
var _cases: Array = []
var _idx := 0
var _f := 0
var _body: CharacterBody3D = null
var _ground: StaticBody3D = null
var _grounded := 0
var _rows: Array = []
var _done := false
func _ready() -> void:
for s in SLOPES:
_cases.append({"deg": s, "const_speed": true, "snap": 0.2})
_cases.append({"deg": s, "const_speed": false, "snap": 0.2})
_build()
func _build() -> void:
var c: Dictionary = _cases[_idx]
var s: float = tan(deg_to_rad(float(c["deg"])))
_ground = StaticBody3D.new()
var cs := CollisionShape3D.new()
var f := PackedVector3Array()
var i := 0
while i < 280:
var j := 0
while j < 20:
var x0 := float(i) * 1.5 - 210.0
var z0 := float(j) * 1.5 - 15.0
var x1 := x0 + 1.5
var z1 := z0 + 1.5
f.push_back(Vector3(x0, x0 * s, z0)); f.push_back(Vector3(x1, x1 * s, z1)); f.push_back(Vector3(x0, x0 * s, z1))
f.push_back(Vector3(x0, x0 * s, z0)); f.push_back(Vector3(x1, x1 * s, z0)); f.push_back(Vector3(x1, x1 * s, z1))
j += 1
i += 1
var tri := ConcavePolygonShape3D.new()
tri.set_faces(f)
cs.shape = tri
_ground.add_child(cs)
add_child(_ground)
_body = CharacterBody3D.new()
_body.floor_snap_length = float(c["snap"])
_body.floor_constant_speed = bool(c["const_speed"])
_body.floor_max_angle = deg_to_rad(60.0)
var shape := CollisionShape3D.new()
var cap := CapsuleShape3D.new()
cap.radius = 0.4
cap.height = 1.8
shape.shape = cap
shape.transform = Transform3D(Basis.IDENTITY, Vector3(0.0, 0.79, 0.0))
_body.add_child(shape)
add_child(_body)
_body.global_position = Vector3(0.0, 0.6, 0.0)
_f = 0
_grounded = 0
func _physics_process(_d: float) -> void:
if _done or _body == null:
return
_f += 1
# Exactly what the controller hands move_and_slide: flat and constant while
# grounded (State.MOVEMENT), gravity while not (State.AIRBORNE).
if _body.is_on_floor():
_body.velocity = Vector3(-SPEED, 0.0, 0.0)
else:
_body.velocity.x = -SPEED
_body.velocity.z = 0.0
_body.velocity.y -= 9.8 * _d
_body.move_and_slide()
if _f > 40:
if _body.is_on_floor():
_grounded += 1
if _f < FRAMES:
return
var c: Dictionary = _cases[_idx]
_rows.append({"deg": c["deg"], "cs": c["const_speed"],
"pct": 100.0 * float(_grounded) / float(FRAMES - 40)})
_body.queue_free(); _body = null
_ground.queue_free(); _ground = null
_idx += 1
if _idx >= _cases.size():
print("")
print("slope floor_constant_speed grounded")
print("---------------------------------------")
for r in _rows:
print("%5.0f %-20s %6.1f %%" % [r["deg"], str(r["cs"]), r["pct"]])
print("")
print("A bare body, constant horizontal velocity, nothing else.")
_done = true
get_tree().quit(0)
return
_build()