1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
# raylib-jamstack: 3D physics demo — raylib (render) + Jolt (physics).
#
# ./zig-out/bin/game game/physics_demo.rb (desktop)
# (web: preloaded; runnable as /game/physics_demo.rb)
#
# A ball pit: colourful spheres drop into a box and bounce. SPACE shoots a ball
# from the camera into the pile; R resets. The camera orbits automatically.
Rl.init_window(960, 540, "raylib-jamstack: 3D physics (Jolt)")
Rl.target_fps = 60
# --- camera (auto-orbiting) ---
camera = Rl::Camera3D.new(
Rl::Vector3.new(18, 14, 18), # position
Rl::Vector3.new(0, 3, 0), # target
Rl::Vector3.new(0, 1, 0), # up
45.0, # fovy
Rl::CAMERA_PERSPECTIVE
)
# --- physics world ---
world = Jolt::World.new(gravity: [0, -18, 0])
GROUND_HALF = 10.0
# A static floor + four low walls to keep the balls in the pit.
world.body(shape: Jolt.box(GROUND_HALF * 2, 1, GROUND_HALF * 2),
position: [0, -0.5, 0], motion: Jolt::STATIC, friction: 0.6)
[[GROUND_HALF, 0], [-GROUND_HALF, 0], [0, GROUND_HALF], [0, -GROUND_HALF]].each do |wx, wz|
sx = wz == 0 ? 1.0 : GROUND_HALF * 2
sz = wz == 0 ? GROUND_HALF * 2 : 1.0
world.body(shape: Jolt.box(sx, 6, sz), position: [wx, 3, wz],
motion: Jolt::STATIC, restitution: 0.3)
end
PALETTE = [Rl::RED, Rl::ORANGE, Rl::GOLD, Rl::LIME, Rl::SKYBLUE,
Rl::BLUE, Rl::VIOLET, Rl::PINK]
MAX_BALLS = 200
# Each ball: [body, radius, color]
balls = []
def spawn_ball(world, balls, pos, radius, velocity = nil)
body = world.body(shape: Jolt.sphere(radius), position: pos,
motion: Jolt::DYNAMIC, restitution: 0.55, friction: 0.4,
velocity: velocity)
balls << [body, radius, PALETTE[rand(PALETTE.length)]]
# keep the body count bounded — retire the oldest ball
if balls.length > MAX_BALLS
old = balls.shift
old[0].remove
end
end
def reset!(world, balls)
balls.each { |b, _| b.remove }
balls.clear
# a loose 5x5x3 stack of spheres above the pit
5.times do |ix|
3.times do |iy|
5.times do |iz|
spawn_ball(world, balls,
[ix * 1.2 - 2.4, 4 + iy * 1.3, iz * 1.2 - 2.4], 0.5)
end
end
end
end
reset!(world, balls)
world.optimize_broad_phase
drip = 0
Rl.while_window_open do
# --- input ---
Rl.update_camera(camera, Rl::CAMERA_ORBITAL)
if Rl.key_pressed?(:space)
# shoot a ball toward what the camera is looking at. The camera sits OUTSIDE
# the pit walls, so spawn the projectile past the walls (inside the pit) —
# otherwise it just bounces off the outer face of a wall.
fwd = Rl.vector3_normalize(Rl.vector3_subtract(camera.target, camera.position))
dist = Rl.vector3_length(Rl.vector3_subtract(camera.target, camera.position))
d = [dist - 8.0, 1.0].max
speed = 35.0
spawn_ball(world, balls,
[camera.position.x + fwd.x * d,
camera.position.y + fwd.y * d,
camera.position.z + fwd.z * d], 0.6,
[fwd.x * speed, fwd.y * speed, fwd.z * speed])
end
reset!(world, balls) if Rl.key_pressed?(:r)
# gentle rain of new balls
drip += 1
if drip >= 20
drip = 0
spawn_ball(world, balls, [rand * 6 - 3, 12, rand * 6 - 3], 0.4 + rand * 0.3)
end
# --- step physics (fixed timestep for stability) ---
world.step(1.0 / 60.0)
# --- render ---
Rl.draw(clear_color: Rl::Color.new(28, 28, 38, 255)) do
Rl.begin_mode3d(camera)
Rl.draw_grid(GROUND_HALF.to_i * 2, 1.0)
balls.each do |body, radius, color|
Rl.draw_sphere(body.position, radius, color)
end
Rl.end_mode3d
Rl.draw_text(text: "balls: #{balls.length}", x: 12, y: 12, font_size: 22, color: Rl::RAYWHITE)
Rl.draw_text(text: "SPACE: shoot R: reset", x: 12, y: 40, font_size: 18, color: Rl::LIGHTGRAY)
# FPS counter in the top-right corner
Rl.draw_fps(Rl.screen_width - 95, 12)
end
end
|