extends Node3D ## Headless physics probe. Run via run_probe.sh, which switches the physics ## engine through override.cfg and launches this scene under each backend. ## ## Test 1 — tunneling: fire a ball at a 1 cm static wall at increasing speeds, ## with CCD off and on, and report whether the wall contains it. ## Test 2 — hinge motor: drive a hinged arm with a joint motor and report ## whether it actually rotates (a flipper is a motorised hinge). const WALL_THICKNESS := 0.01 const BALL_RADIUS := 0.0135 const START_Z := 2.0 const SPEEDS := [5.0, 10.0, 20.0, 40.0, 80.0, 160.0] func _ready() -> void: var engine := str(ProjectSettings.get_setting("physics/3d/physics_engine")) print("PROBE_START engine=%s tick=%d" % [engine, Engine.physics_ticks_per_second]) _run(engine) func _run(engine: String) -> void: var wall := StaticBody3D.new() var cs := CollisionShape3D.new() var box := BoxShape3D.new() box.size = Vector3(1.0, 1.0, WALL_THICKNESS) cs.shape = box wall.add_child(cs) add_child(wall) for ccd in [false, true]: for speed in SPEEDS: var r := await _fire(float(speed), ccd) print("PROBE engine=\"%s\" speed=%d ccd=%s result=%s final_z=%.3f drift_y=%.4f" % [ engine, int(speed), "on" if ccd else "off", "tunneled" if r.tunneled else "contained", r.final_z, r.drift_y]) var m := await _motor_test() print("PROBE_MOTOR engine=\"%s\" moved_deg=%.1f works=%s" % [ engine, m.moved_deg, "yes" if m.moved_deg > 20.0 else "NO"]) get_tree().quit(0) func _fire(speed: float, ccd: bool) -> Dictionary: var ball := RigidBody3D.new() var cs := CollisionShape3D.new() var sph := SphereShape3D.new() sph.radius = BALL_RADIUS cs.shape = sph ball.add_child(cs) ball.mass = 0.08 ball.continuous_cd = ccd ball.gravity_scale = 0.0 ball.can_sleep = false ball.position = Vector3(0, 0, START_Z) add_child(ball) ball.linear_velocity = Vector3(0, 0, -speed) # Enough ticks to cover the distance plus settle time. var ticks := maxi(90, int(ceil((START_Z + 1.0) / speed * 60.0)) + 30) for i in ticks: await get_tree().physics_frame var result := { "tunneled": ball.position.z < -(WALL_THICKNESS / 2.0 + BALL_RADIUS + 0.05), "final_z": ball.position.z, "drift_y": absf(ball.position.y), # nonzero = gravity_scale unsupported } ball.queue_free() await get_tree().physics_frame return result func _motor_test() -> Dictionary: var anchor := StaticBody3D.new() var acs := CollisionShape3D.new() var abox := BoxShape3D.new() abox.size = Vector3(0.05, 0.05, 0.05) acs.shape = abox anchor.add_child(acs) anchor.position = Vector3(3, 0, 0) # far from the wall test add_child(anchor) var arm := RigidBody3D.new() var ccs := CollisionShape3D.new() var bbox := BoxShape3D.new() bbox.size = Vector3(0.1, 0.03, 0.022) ccs.shape = bbox ccs.position = Vector3(0.05, 0, 0) arm.add_child(ccs) arm.mass = 0.15 arm.gravity_scale = 0.0 arm.can_sleep = false arm.position = Vector3(3.1, 0, 0) add_child(arm) var joint := HingeJoint3D.new() # Local Z (the hinge axis) pointed along world +Y. joint.transform = Transform3D(Basis(Vector3(1, 0, 0), -PI / 2.0), Vector3(3.1, 0, 0)) joint.set_flag(HingeJoint3D.FLAG_ENABLE_MOTOR, true) joint.set_param(HingeJoint3D.PARAM_MOTOR_TARGET_VELOCITY, 5.0) joint.set_param(HingeJoint3D.PARAM_MOTOR_MAX_IMPULSE, 5.0) add_child(joint) # Paths only resolve once the joint is inside the tree. joint.node_a = joint.get_path_to(anchor) joint.node_b = joint.get_path_to(arm) var start_q := arm.global_transform.basis.get_rotation_quaternion() for i in 60: await get_tree().physics_frame var end_q := arm.global_transform.basis.get_rotation_quaternion() var moved := start_q.angle_to(end_q) print("PROBE_MOTOR_DEBUG rotation=%s ang_vel=%s pos=%s" % [ arm.rotation, arm.angular_velocity, arm.position]) return {"moved_deg": rad_to_deg(moved)}