macrosoft3dpinball/godot/scenes/table.gd
m3ultra 2ca67bb28b Add Godot 4.7 physics testbed: Box3D/Jolt/GodotPhysics A/B pinball table
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>
2026-08-06 05:33:07 +10:00

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