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>
This commit is contained in:
parent
43d7e9ac8e
commit
2ca67bb28b
11
.gitignore
vendored
11
.gitignore
vendored
@ -315,3 +315,14 @@ Full_Tilt_Pinball_ISO/
|
||||
*.ccd
|
||||
*.sub
|
||||
*.cue
|
||||
|
||||
# More original game media at repo root (same policy as *.DAT/*.MID above)
|
||||
/SOUND*.WAV
|
||||
/Pinball.exe
|
||||
/table.bmp
|
||||
/wavemix.inf
|
||||
|
||||
# Godot testbed transients
|
||||
godot/.godot/
|
||||
godot/override.cfg
|
||||
godot/probe/results/
|
||||
|
||||
10
CLAUDE.md
10
CLAUDE.md
@ -1,6 +1,14 @@
|
||||
# macrosoft3dpinball
|
||||
|
||||
`3D Pinball for Windows – Space Cadet` on macOS. Vendored copy of
|
||||
`3D Pinball for Windows – Space Cadet` on macOS. Two halves:
|
||||
|
||||
- The vendored decompilation (below) — the faithful original.
|
||||
- `godot/` — a Godot 4.7 pinball testbed whose real job is A/B-ing 3D physics
|
||||
backends (Box3D vs Jolt vs GodotPhysics) on CCD, hinge-motor flippers, and joint
|
||||
limits. See `godot/README.md` for the probe harness and current findings (Box3D
|
||||
never tunnels with CCD off but has an inverted hinge-motor sign vs Jolt).
|
||||
|
||||
Vendored copy of
|
||||
[k4zmu2a/SpaceCadetPinball](https://github.com/k4zmu2a/SpaceCadetPinball) (MIT) at upstream
|
||||
commit `cb9b7b8`, not a submodule — patch it in place, pull upstream by re-vendoring.
|
||||
|
||||
|
||||
92
godot/README.md
Normal file
92
godot/README.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Godot physics testbed (Box3D / Jolt / GodotPhysics A/B)
|
||||
|
||||
A minimal playable pinball table for Godot 4.7 whose real job is comparing 3D physics
|
||||
backends on the things pinball stresses: fast-small-ball CCD, motorised hinge flippers,
|
||||
and joint limits. The backend is one project setting, so the same table runs under all
|
||||
three engines.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
/Applications/Godot.app/Contents/MacOS/Godot --path godot
|
||||
```
|
||||
|
||||
Keys: `Z` left flipper, `/` right flipper, `Space` plunger, `R` reset ball.
|
||||
|
||||
The project defaults to `Box3D Physics (Extension)` via
|
||||
[godot-box3d](https://github.com/bearlikelion/godot-box3d) (MIT). `bin/` carries a
|
||||
macOS arm64 dylib built from source (upstream has no releases; build recipe below).
|
||||
|
||||
## A/B probe harness
|
||||
|
||||
```bash
|
||||
godot/run_probe.sh # GODOT_BIN=... to point at a different binary
|
||||
```
|
||||
|
||||
Writes `override.cfg` to switch `physics/3d/physics_engine`, runs `probe/probe.tscn`
|
||||
headless under each backend, and prints per-engine results (also saved to
|
||||
`probe/results/`, untracked). Two tests:
|
||||
|
||||
1. **Tunneling** — ball fired at a 1 cm static wall at 5–160 m/s, CCD off and on.
|
||||
2. **Hinge motor** — a motorised HingeJoint3D arm; a flipper is a motorised hinge.
|
||||
|
||||
There is also a table-level flipper check:
|
||||
`PINBALL_AUTOTEST=1 godot --headless --path godot` flips both real flippers for 0.5 s
|
||||
and prints the swing angles.
|
||||
|
||||
## Results (2026-08-06, Godot 4.7.stable, M3 Ultra)
|
||||
|
||||
Tunneling — ball contained by the 1 cm wall?
|
||||
|
||||
| speed m/s | Godot ccd off | Jolt ccd off | Box3D ccd off | all three, ccd on |
|
||||
|---|---|---|---|---|
|
||||
| 5 | tunneled* | tunneled* | **contained** | contained |
|
||||
| 10 | tunneled | contained* | **contained** | contained |
|
||||
| 20 | contained* | contained* | **contained** | contained |
|
||||
| 40 | contained* | contained* | **contained** | contained |
|
||||
| 80 | tunneled | tunneled | **contained** | contained |
|
||||
| 160 | tunneled | tunneled | **contained** | contained |
|
||||
|
||||
\* With a 60 Hz tick, every speed here can step over a 1 cm wall; the discrete-engine
|
||||
"contained" rows are phase luck (2 m at 20 and 40 m/s lands a tick exactly on the wall
|
||||
— and both engines produce identical trajectories, so they get lucky identically).
|
||||
The signal is the pattern, not individual rows: **Box3D never tunnels even with the
|
||||
CCD flag off** (speculative contacts are always on), the other two need
|
||||
`continuous_cd = true`, and with it all three hold at 160 m/s.
|
||||
|
||||
Hinge motor (target 5 rad/s, 1 s):
|
||||
|
||||
| engine | reached rad/s | moved | verdict |
|
||||
|---|---|---|---|
|
||||
| GodotPhysics3D | −5.0 | 73.5° (wrapped) | works |
|
||||
| Jolt | −5.0 | 75.1° (wrapped) | works |
|
||||
| Box3D | **+5.0** | 73.5° (wrapped) | works |
|
||||
|
||||
Flipper autotest on the real table (motors at ±25 rad/s, limits −5°…+50°):
|
||||
|
||||
| engine | left swing | right swing |
|
||||
|---|---|---|
|
||||
| Box3D | +51.8° — **stops at the 50° limit** | −51.8° — stops at limit |
|
||||
| Jolt | −106.6° — wrong way, **blows ~100° past the limit** | +104.3° — same |
|
||||
|
||||
Two real findings, half a second of headless runtime each:
|
||||
|
||||
1. **Box3D's hinge motor sign is inverted** relative to GodotPhysics/Jolt (upstream
|
||||
drop-in-compatibility bug candidate for godot-box3d). The table's flipper signs are
|
||||
tuned for Box3D, the project default — flip `FLIP_SPEED`/`REST_SPEED` signs in
|
||||
`scenes/table.gd` to play under Jolt.
|
||||
2. With the motor overdriving into them, **Box3D enforces the angular limits hard**
|
||||
while Jolt lets this setup punch far through them.
|
||||
|
||||
## Rebuilding the extension
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/bearlikelion/godot-box3d
|
||||
cd godot-box3d && cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
|
||||
cp bin/libgodot-box3d.dylib <here>/bin/
|
||||
```
|
||||
|
||||
Known Box3D gaps that don't matter for pinball: no Generic6DOF/ConeTwist joints, no
|
||||
SoftBody3D, Area3D can't detect trimesh/heightmap bodies (the ball is a sphere; fine).
|
||||
Box3D also ignores Godot's hinge BIAS/LIMIT_SOFTNESS/LIMIT_RELAXATION params (warns at
|
||||
load; harmless here).
|
||||
14
godot/godot-box3d.gdextension
Normal file
14
godot/godot-box3d.gdextension
Normal file
@ -0,0 +1,14 @@
|
||||
[configuration]
|
||||
|
||||
entry_symbol = "godot_box3d_main"
|
||||
compatibility_minimum = "4.3"
|
||||
reloadable = true
|
||||
|
||||
[libraries]
|
||||
|
||||
linux.debug.x86_64 = "res://bin/libgodot-box3d.so"
|
||||
linux.release.x86_64 = "res://bin/libgodot-box3d.so"
|
||||
windows.debug.x86_64 = "res://bin/godot-box3d.dll"
|
||||
windows.release.x86_64 = "res://bin/godot-box3d.dll"
|
||||
macos.debug = "res://bin/libgodot-box3d.dylib"
|
||||
macos.release = "res://bin/libgodot-box3d.dylib"
|
||||
1
godot/godot-box3d.gdextension.uid
Normal file
1
godot/godot-box3d.gdextension.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bhuvu8ccww6xt
|
||||
112
godot/probe/probe.gd
Normal file
112
godot/probe/probe.gd
Normal file
@ -0,0 +1,112 @@
|
||||
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)}
|
||||
1
godot/probe/probe.gd.uid
Normal file
1
godot/probe/probe.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://djjx4g8ojqrt7
|
||||
6
godot/probe/probe.tscn
Normal file
6
godot/probe/probe.tscn
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://probe/probe.gd" id="1"]
|
||||
|
||||
[node name="Probe" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
17
godot/project.godot
Normal file
17
godot/project.godot
Normal file
@ -0,0 +1,17 @@
|
||||
; Godot 4.7 physics testbed for macrosoft3dpinball.
|
||||
; Physics engine is switched via this setting (or an override.cfg written by run_probe.sh):
|
||||
; "GodotPhysics3D" | "Jolt Physics" | "Box3D Physics (Extension)"
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="macrosoft3dpinball testbed"
|
||||
run/main_scene="res://scenes/table.tscn"
|
||||
config/features=PackedStringArray("4.7")
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Box3D Physics (Extension)"
|
||||
3d/default_gravity=9.81
|
||||
3d/default_gravity_vector=Vector3(0, -0.99357, 0.1132)
|
||||
20
godot/run_probe.sh
Executable file
20
godot/run_probe.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# A/B/C physics probe: runs probe/probe.tscn headless under each backend by
|
||||
# switching physics/3d/physics_engine through override.cfg.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
GODOT="${GODOT_BIN:-/Applications/Godot.app/Contents/MacOS/Godot}"
|
||||
ENGINES=("GodotPhysics3D" "Jolt Physics" "Box3D Physics (Extension)")
|
||||
|
||||
mkdir -p probe/results
|
||||
"$GODOT" --headless --path . --import > /dev/null 2>&1 || true
|
||||
|
||||
for e in "${ENGINES[@]}"; do
|
||||
printf '[physics]\n3d/physics_engine="%s"\n' "$e" > override.cfg
|
||||
slug=$(echo "$e" | tr ' ()' '_' | tr -s '_')
|
||||
echo "=== $e ==="
|
||||
"$GODOT" --headless --path . res://probe/probe.tscn 2>/dev/null \
|
||||
| grep '^PROBE' | tee "probe/results/$slug.txt"
|
||||
done
|
||||
rm -f override.cfg
|
||||
72
godot/scenes/table.gd
Normal file
72
godot/scenes/table.gd
Normal file
@ -0,0 +1,72 @@
|
||||
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
|
||||
1
godot/scenes/table.gd.uid
Normal file
1
godot/scenes/table.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cfsbqmggqh2jg
|
||||
262
godot/scenes/table.tscn
Normal file
262
godot/scenes/table.tscn
Normal file
@ -0,0 +1,262 @@
|
||||
[gd_scene load_steps=28 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/table.gd" id="1"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="floor_shape"]
|
||||
size = Vector3(0.54, 0.02, 1.04)
|
||||
|
||||
[sub_resource type="BoxMesh" id="floor_mesh"]
|
||||
size = Vector3(0.54, 0.02, 1.04)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="mat_floor"]
|
||||
albedo_color = Color(0.13, 0.28, 0.16, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="wall_long_shape"]
|
||||
size = Vector3(0.02, 0.06, 1.04)
|
||||
|
||||
[sub_resource type="BoxMesh" id="wall_long_mesh"]
|
||||
size = Vector3(0.02, 0.06, 1.04)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="wall_short_shape"]
|
||||
size = Vector3(0.54, 0.06, 0.02)
|
||||
|
||||
[sub_resource type="BoxMesh" id="wall_short_mesh"]
|
||||
size = Vector3(0.54, 0.06, 0.02)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="mat_wall"]
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="lane_shape"]
|
||||
size = Vector3(0.01, 0.06, 0.79)
|
||||
|
||||
[sub_resource type="BoxMesh" id="lane_mesh"]
|
||||
size = Vector3(0.01, 0.06, 0.79)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="guide_l_shape"]
|
||||
size = Vector3(0.16, 0.06, 0.01)
|
||||
|
||||
[sub_resource type="BoxMesh" id="guide_l_mesh"]
|
||||
size = Vector3(0.16, 0.06, 0.01)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="guide_r_shape"]
|
||||
size = Vector3(0.13, 0.06, 0.01)
|
||||
|
||||
[sub_resource type="BoxMesh" id="guide_r_mesh"]
|
||||
size = Vector3(0.13, 0.06, 0.01)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="flipper_shape"]
|
||||
size = Vector3(0.1, 0.03, 0.022)
|
||||
|
||||
[sub_resource type="BoxMesh" id="flipper_mesh"]
|
||||
size = Vector3(0.1, 0.03, 0.022)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="mat_flipper"]
|
||||
albedo_color = Color(0.85, 0.15, 0.12, 1)
|
||||
|
||||
[sub_resource type="SphereShape3D" id="ball_shape"]
|
||||
radius = 0.0135
|
||||
|
||||
[sub_resource type="SphereMesh" id="ball_mesh"]
|
||||
radius = 0.0135
|
||||
height = 0.027
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="mat_ball"]
|
||||
albedo_color = Color(0.75, 0.75, 0.8, 1)
|
||||
metallic = 0.9
|
||||
roughness = 0.25
|
||||
|
||||
[sub_resource type="PhysicsMaterial" id="ball_phys"]
|
||||
friction = 0.25
|
||||
bounce = 0.3
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="bumper_shape"]
|
||||
height = 0.05
|
||||
radius = 0.035
|
||||
|
||||
[sub_resource type="CylinderMesh" id="bumper_mesh"]
|
||||
top_radius = 0.035
|
||||
bottom_radius = 0.035
|
||||
height = 0.05
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="mat_bumper"]
|
||||
albedo_color = Color(0.95, 0.55, 0.1, 1)
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="bumper_area_shape"]
|
||||
height = 0.08
|
||||
radius = 0.055
|
||||
|
||||
[sub_resource type="BoxShape3D" id="drain_shape"]
|
||||
size = Vector3(0.2, 0.06, 0.04)
|
||||
|
||||
[node name="Table" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Playfield" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="FloorShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.01, 0)
|
||||
shape = SubResource("floor_shape")
|
||||
|
||||
[node name="FloorMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.01, 0)
|
||||
mesh = SubResource("floor_mesh")
|
||||
surface_material_override/0 = SubResource("mat_floor")
|
||||
|
||||
[node name="WallLeftShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.26, 0.03, 0)
|
||||
shape = SubResource("wall_long_shape")
|
||||
|
||||
[node name="WallLeftMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.26, 0.03, 0)
|
||||
mesh = SubResource("wall_long_mesh")
|
||||
surface_material_override/0 = SubResource("mat_wall")
|
||||
|
||||
[node name="WallRightShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.26, 0.03, 0)
|
||||
shape = SubResource("wall_long_shape")
|
||||
|
||||
[node name="WallRightMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.26, 0.03, 0)
|
||||
mesh = SubResource("wall_long_mesh")
|
||||
surface_material_override/0 = SubResource("mat_wall")
|
||||
|
||||
[node name="WallFarShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.03, -0.51)
|
||||
shape = SubResource("wall_short_shape")
|
||||
|
||||
[node name="WallFarMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.03, -0.51)
|
||||
mesh = SubResource("wall_short_mesh")
|
||||
surface_material_override/0 = SubResource("mat_wall")
|
||||
|
||||
[node name="WallNearShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.03, 0.51)
|
||||
shape = SubResource("wall_short_shape")
|
||||
|
||||
[node name="WallNearMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.03, 0.51)
|
||||
mesh = SubResource("wall_short_mesh")
|
||||
surface_material_override/0 = SubResource("mat_wall")
|
||||
|
||||
[node name="LaneWallShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.21, 0.03, 0.075)
|
||||
shape = SubResource("lane_shape")
|
||||
|
||||
[node name="LaneWallMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.21, 0.03, 0.075)
|
||||
mesh = SubResource("lane_mesh")
|
||||
surface_material_override/0 = SubResource("mat_wall")
|
||||
|
||||
[node name="GuideLeftShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(0.768, 0, -0.64, 0, 1, 0, 0.64, 0, 0.768, -0.19, 0.03, 0.35)
|
||||
shape = SubResource("guide_l_shape")
|
||||
|
||||
[node name="GuideLeftMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(0.768, 0, -0.64, 0, 1, 0, 0.64, 0, 0.768, -0.19, 0.03, 0.35)
|
||||
mesh = SubResource("guide_l_mesh")
|
||||
surface_material_override/0 = SubResource("mat_wall")
|
||||
|
||||
[node name="GuideRightShape" type="CollisionShape3D" parent="Playfield"]
|
||||
transform = Transform3D(-0.768, 0, -0.64, 0, 1, 0, 0.64, 0, -0.768, 0.167, 0.03, 0.35)
|
||||
shape = SubResource("guide_r_shape")
|
||||
|
||||
[node name="GuideRightMesh" type="MeshInstance3D" parent="Playfield"]
|
||||
transform = Transform3D(-0.768, 0, -0.64, 0, 1, 0, 0.64, 0, -0.768, 0.167, 0.03, 0.35)
|
||||
mesh = SubResource("guide_r_mesh")
|
||||
surface_material_override/0 = SubResource("mat_wall")
|
||||
|
||||
[node name="LeftFlipper" type="RigidBody3D" parent="."]
|
||||
transform = Transform3D(0.906, 0, -0.423, 0, 1, 0, 0.423, 0, 0.906, -0.115, 0.02, 0.42)
|
||||
mass = 0.15
|
||||
continuous_cd = true
|
||||
can_sleep = false
|
||||
|
||||
[node name="Shape" type="CollisionShape3D" parent="LeftFlipper"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.05, 0, 0)
|
||||
shape = SubResource("flipper_shape")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="LeftFlipper"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.05, 0, 0)
|
||||
mesh = SubResource("flipper_mesh")
|
||||
surface_material_override/0 = SubResource("mat_flipper")
|
||||
|
||||
[node name="RightFlipper" type="RigidBody3D" parent="."]
|
||||
transform = Transform3D(0.906, 0, 0.423, 0, 1, 0, -0.423, 0, 0.906, 0.115, 0.02, 0.42)
|
||||
mass = 0.15
|
||||
continuous_cd = true
|
||||
can_sleep = false
|
||||
|
||||
[node name="Shape" type="CollisionShape3D" parent="RightFlipper"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.05, 0, 0)
|
||||
shape = SubResource("flipper_shape")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="RightFlipper"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.05, 0, 0)
|
||||
mesh = SubResource("flipper_mesh")
|
||||
surface_material_override/0 = SubResource("mat_flipper")
|
||||
|
||||
[node name="LeftFlipperJoint" type="HingeJoint3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0, 1, 0, -1, 0, -0.115, 0.02, 0.42)
|
||||
node_a = NodePath("../Playfield")
|
||||
node_b = NodePath("../LeftFlipper")
|
||||
angular_limit/enable = true
|
||||
angular_limit/upper = 0.873
|
||||
angular_limit/lower = -0.087
|
||||
motor/enable = true
|
||||
motor/target_velocity = -8.0
|
||||
motor/max_impulse = 0.6
|
||||
|
||||
[node name="RightFlipperJoint" type="HingeJoint3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0, 1, 0, -1, 0, 0.115, 0.02, 0.42)
|
||||
node_a = NodePath("../Playfield")
|
||||
node_b = NodePath("../RightFlipper")
|
||||
angular_limit/enable = true
|
||||
angular_limit/upper = 0.087
|
||||
angular_limit/lower = -0.873
|
||||
motor/enable = true
|
||||
motor/target_velocity = 8.0
|
||||
motor/max_impulse = 0.6
|
||||
|
||||
[node name="Ball" type="RigidBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.2325, 0.0135, 0.44)
|
||||
mass = 0.08
|
||||
physics_material_override = SubResource("ball_phys")
|
||||
continuous_cd = true
|
||||
can_sleep = false
|
||||
|
||||
[node name="Shape" type="CollisionShape3D" parent="Ball"]
|
||||
shape = SubResource("ball_shape")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="Ball"]
|
||||
mesh = SubResource("ball_mesh")
|
||||
surface_material_override/0 = SubResource("mat_ball")
|
||||
|
||||
[node name="Bumper" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, -0.1)
|
||||
|
||||
[node name="Shape" type="CollisionShape3D" parent="Bumper"]
|
||||
shape = SubResource("bumper_shape")
|
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="Bumper"]
|
||||
mesh = SubResource("bumper_mesh")
|
||||
surface_material_override/0 = SubResource("mat_bumper")
|
||||
|
||||
[node name="BumperArea" type="Area3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, -0.1)
|
||||
|
||||
[node name="Shape" type="CollisionShape3D" parent="BumperArea"]
|
||||
shape = SubResource("bumper_area_shape")
|
||||
|
||||
[node name="Drain" type="Area3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.03, 0.485)
|
||||
|
||||
[node name="Shape" type="CollisionShape3D" parent="Drain"]
|
||||
shape = SubResource("drain_shape")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.417, 0.909, 0, -0.909, 0.417, 0, 1.2, 0.55)
|
||||
fov = 30.0
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.5, 0.866, 0, -0.866, 0.5, 0, 1, 0)
|
||||
shadow_enabled = true
|
||||
Loading…
Reference in New Issue
Block a user