Playable table (hinge-motor flippers, plunger, bumper, drain) plus a headless probe harness that measured, per engine: tunneling of a fast ball through a 1 cm wall with CCD off/on, and hinge-motor drive. Findings in godot/README.md: Box3D never tunnels even with CCD off, but its hinge motor sign is inverted vs Jolt/GodotPhysics, and Jolt blows through angular limits that Box3D holds. Ships a from-source macOS arm64 libgodot-box3d.dylib (upstream has no releases). Also gitignore the remaining Microsoft media at repo root. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
113 lines
3.8 KiB
GDScript
113 lines
3.8 KiB
GDScript
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)}
|