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>
73 lines
2.6 KiB
GDScript
73 lines
2.6 KiB
GDScript
extends Node3D
|
|
## Playable Box3D/Jolt/GodotPhysics pinball testbed.
|
|
## Z = left flipper, / = right flipper, Space = plunger, R = reset ball.
|
|
|
|
const FLIP_SPEED := 25.0
|
|
const REST_SPEED := 8.0
|
|
const PLUNGE_IMPULSE := 0.32 # ~4 m/s on the 80 g ball
|
|
const BUMPER_IMPULSE := 0.12
|
|
|
|
@onready var ball: RigidBody3D = $Ball
|
|
@onready var left_joint: HingeJoint3D = $LeftFlipperJoint
|
|
@onready var right_joint: HingeJoint3D = $RightFlipperJoint
|
|
|
|
var _ball_start: Transform3D
|
|
|
|
func _ready() -> void:
|
|
_ball_start = ball.transform
|
|
print("physics engine: ", ProjectSettings.get_setting("physics/3d/physics_engine"))
|
|
$Drain.body_entered.connect(_on_drain)
|
|
$BumperArea.body_entered.connect(_on_bumper)
|
|
if OS.get_environment("PINBALL_AUTOTEST") == "1":
|
|
_autotest()
|
|
|
|
## Headless check that the real table flippers flip: hold both motors at flip
|
|
## speed for half a second and report how far each flipper swung.
|
|
func _autotest() -> void:
|
|
set_physics_process(false)
|
|
var lf: RigidBody3D = $LeftFlipper
|
|
var rf: RigidBody3D = $RightFlipper
|
|
var l0 := lf.rotation.y
|
|
var r0 := rf.rotation.y
|
|
left_joint.set_param(HingeJoint3D.PARAM_MOTOR_TARGET_VELOCITY, FLIP_SPEED)
|
|
right_joint.set_param(HingeJoint3D.PARAM_MOTOR_TARGET_VELOCITY, -FLIP_SPEED)
|
|
for i in 30:
|
|
await get_tree().physics_frame
|
|
print("AUTOTEST left_swing_deg=%.1f right_swing_deg=%.1f" % [
|
|
rad_to_deg(angle_difference(l0, lf.rotation.y)),
|
|
rad_to_deg(angle_difference(r0, rf.rotation.y))])
|
|
get_tree().quit(0)
|
|
|
|
func _physics_process(_delta: float) -> void:
|
|
var left := Input.is_physical_key_pressed(KEY_Z)
|
|
var right := Input.is_physical_key_pressed(KEY_SLASH)
|
|
left_joint.set_param(HingeJoint3D.PARAM_MOTOR_TARGET_VELOCITY, FLIP_SPEED if left else -REST_SPEED)
|
|
right_joint.set_param(HingeJoint3D.PARAM_MOTOR_TARGET_VELOCITY, -FLIP_SPEED if right else REST_SPEED)
|
|
if Input.is_physical_key_pressed(KEY_SPACE) and _ball_in_lane():
|
|
ball.apply_central_impulse(Vector3(0, 0, -PLUNGE_IMPULSE))
|
|
if Input.is_physical_key_pressed(KEY_R):
|
|
_reset_ball()
|
|
|
|
func _ball_in_lane() -> bool:
|
|
return ball.position.x > 0.21 and ball.position.z > 0.30 \
|
|
and ball.linear_velocity.length() < 0.05
|
|
|
|
func _on_drain(body: Node3D) -> void:
|
|
if body == ball:
|
|
_reset_ball.call_deferred()
|
|
|
|
func _on_bumper(body: Node3D) -> void:
|
|
if body != ball:
|
|
return
|
|
var bumper: StaticBody3D = $Bumper
|
|
var dir := ball.global_position - bumper.global_position
|
|
dir.y = 0.0
|
|
if dir.length() < 0.001:
|
|
dir = Vector3(0, 0, 1)
|
|
ball.apply_central_impulse(dir.normalized() * BUMPER_IMPULSE)
|
|
|
|
func _reset_ball() -> void:
|
|
ball.linear_velocity = Vector3.ZERO
|
|
ball.angular_velocity = Vector3.ZERO
|
|
ball.transform = _ball_start
|