3D Physics Playground¶
falling bodies and a walking character.
▶ Run in browserTags: physics 3d character
Drop a batch of spheres and boxes onto a static slab, then walk a glowing character through the pile.
A PhysicsBody3D is simulated by the world: mode=BodyMode.DYNAMIC falls
under gravity, tumbles and stacks, and BodyMode.STATIC (the slab) never
moves. Its shape Property is the collider, and a MeshInstance3D child is
what you see; the child inherits the pose the world writes back each fixed step.
body = PhysicsBody3D(mode=BodyMode.DYNAMIC, shape=SphereShape3D(radius=0.5), position=(0, 6, 0))
body.add_child(MeshInstance3D(mesh=Mesh.sphere(radius=0.5), material=Material(colour=(1, 0, 0))))
A CharacterBody3D is moved by game code instead. Each fixed step the scene
sets its velocity and calls move_and_slide(dt), which sweeps it through
the world, slides it along what it hits and writes the deflected velocity back,
so the next step reads its fall speed straight off the character. Gravity is not
applied for you: the scene adds the world’s own gravity to that fall speed,
so the character drops at the rate the bodies do, and is_on_floor() reports
whether the last move ended standing on something. Dynamic bodies collide with
the character and can come to rest on its head, and it shoves the ones it walks
into; walk out from under a resting box and it is left behind to fall, because a
character carries no riders.
Movement is camera-relative: the move action is one Vec2 from WASD or the
left stick, rotated by the OrbitCamera3D’s yaw so pushing away from you
walks away from the camera however it is orbited. The camera takes its own
input (drag, right stick, wheel); the arrow keys orbit it through
camera.orbit(). A TouchController publishes an on-screen twin-stick pad
as a real controller, so the same bindings make it playable with a finger.
Controls: WASD / left stick - Walk (camera-relative) Drag / arrows / right stick - Orbit the camera Scroll - Zoom R / A / Drop button - Drop a fresh batch of bodies
Source¶
1# /// simvx
2# tags = ["3d", "physics", "character"]
3# ///
4"""3D Physics Playground: falling bodies and a walking character.
5
6Drop a batch of spheres and boxes onto a static slab, then walk a glowing
7character through the pile.
8
9A ``PhysicsBody3D`` is simulated by the world: ``mode=BodyMode.DYNAMIC`` falls
10under gravity, tumbles and stacks, and ``BodyMode.STATIC`` (the slab) never
11moves. Its ``shape`` Property is the collider, and a ``MeshInstance3D`` child is
12what you see; the child inherits the pose the world writes back each fixed step.
13
14 body = PhysicsBody3D(mode=BodyMode.DYNAMIC, shape=SphereShape3D(radius=0.5), position=(0, 6, 0))
15 body.add_child(MeshInstance3D(mesh=Mesh.sphere(radius=0.5), material=Material(colour=(1, 0, 0))))
16
17A ``CharacterBody3D`` is moved by game code instead. Each fixed step the scene
18sets its ``velocity`` and calls ``move_and_slide(dt)``, which sweeps it through
19the world, slides it along what it hits and writes the deflected velocity back,
20so the next step reads its fall speed straight off the character. Gravity is not
21applied for you: the scene adds the world's own ``gravity`` to that fall speed,
22so the character drops at the rate the bodies do, and ``is_on_floor()`` reports
23whether the last move ended standing on something. Dynamic bodies collide with
24the character and can come to rest on its head, and it shoves the ones it walks
25into; walk out from under a resting box and it is left behind to fall, because a
26character carries no riders.
27
28Movement is camera-relative: the ``move`` action is one ``Vec2`` from WASD or the
29left stick, rotated by the ``OrbitCamera3D``'s ``yaw`` so pushing away from you
30walks away from the camera however it is orbited. The camera takes its own
31input (drag, right stick, wheel); the arrow keys orbit it through
32``camera.orbit()``. A ``TouchController`` publishes an on-screen twin-stick pad
33as a real controller, so the same bindings make it playable with a finger.
34
35Controls:
36 WASD / left stick - Walk (camera-relative)
37 Drag / arrows / right stick - Orbit the camera
38 Scroll - Zoom
39 R / A / Drop button - Drop a fresh batch of bodies
40"""
41
42from simvx.core import (
43 Anchor,
44 Axis2D,
45 BodyMode,
46 BoxShape3D,
47 Button,
48 CanvasLayer,
49 CharacterBody3D,
50 Controller,
51 DirectionalLight3D,
52 Input,
53 Key,
54 Label,
55 Material,
56 Mesh,
57 MeshInstance3D,
58 Node3D,
59 OrbitCamera3D,
60 PhysicsBody3D,
61 SphereShape3D,
62 TouchController,
63 TouchControllerLayout,
64 VBoxContainer,
65 Vec3,
66 on_input,
67)
68from simvx.graphics import App
69
70WALK_SPEED = 6.0
71# The bodies are 1 unit across and the slab's top is y = 0.5, so a body lying on
72# the slab has its centre near y = 1.0; above this it is still falling or stacked.
73REST_HEIGHT = 1.7
74
75BODY_COLOURS = [
76 (0.90, 0.30, 0.25),
77 (0.30, 0.70, 0.95),
78 (0.95, 0.80, 0.25),
79 (0.55, 0.85, 0.35),
80 (0.80, 0.45, 0.90),
81]
82SPHERE = Mesh.sphere(radius=0.5, rings=16, segments=24)
83CUBE = Mesh.box(1.0)
84
85
86class PhysicsScene(Node3D):
87 input_context = {
88 "move": [Axis2D.WASD, Controller.LEFT_STICK],
89 "orbit": [Axis2D.ARROWS],
90 "respawn": [Key.R, Controller.A],
91 }
92
93 def on_ready(self):
94 self.camera = self.add_child(
95 OrbitCamera3D(
96 pivot=Vec3(0, 1.5, 0),
97 distance=22.0,
98 yaw=0.6,
99 pitch=-0.5,
100 min_pitch=-1.3,
101 max_pitch=-0.05,
102 min_distance=8.0,
103 max_distance=40.0,
104 )
105 )
106 self.add_child(DirectionalLight3D(colour=(1.0, 0.96, 0.85), intensity=1.0, direction=(-6, -12, -8)))
107
108 ground = self.add_child(
109 PhysicsBody3D(name="Ground", mode=BodyMode.STATIC, shape=BoxShape3D(half_extents=(12, 0.5, 12)))
110 )
111 ground.add_child(
112 MeshInstance3D(mesh=Mesh.box((24, 1, 24)), material=Material(colour=(0.24, 0.26, 0.30), roughness=0.9))
113 )
114
115 self.character = self.add_child(
116 CharacterBody3D(name="Character", shape=SphereShape3D(radius=0.6), position=(0, 1.2, 4))
117 )
118 self.character.add_child(
119 MeshInstance3D(
120 mesh=Mesh.sphere(radius=0.6, rings=16, segments=24),
121 material=Material(colour=(1.0, 0.55, 0.15), emissive_colour=(1.0, 0.45, 0.10, 1.5), roughness=0.5),
122 )
123 )
124
125 self.bodies: list[PhysicsBody3D] = []
126 self.spawn_batch()
127
128 self.add_child(TouchController(layout=TouchControllerLayout.TWIN_STICK, buttons=(Controller.A,)))
129 hud = self.add_child(CanvasLayer()).add_child(VBoxContainer(anchor=Anchor.TOP_LEFT, inset=12, separation=8))
130 self.status = hud.add_child(Label(font_size=16))
131
132 # Example help overlay: not part of the technique shown.
133 hud.add_child(Label("WASD: walk Drag / arrows: orbit Scroll: zoom"))
134 hud.add_child(Button("Drop", action="respawn"))
135
136 def spawn_batch(self):
137 for body in self.bodies:
138 body.destroy()
139 self.bodies = []
140 for i in range(8):
141 is_sphere = i % 2 == 0
142 body = self.add_child(
143 PhysicsBody3D(
144 mode=BodyMode.DYNAMIC,
145 shape=SphereShape3D(radius=0.5) if is_sphere else BoxShape3D(half_extents=(0.5, 0.5, 0.5)),
146 position=((i % 4 - 1.5) * 1.6, 6.0 + (i % 4) * 0.9, (i // 4 - 0.5) * 1.6),
147 )
148 )
149 colour = BODY_COLOURS[i % len(BODY_COLOURS)]
150 body.add_child(
151 MeshInstance3D(mesh=SPHERE if is_sphere else CUBE, material=Material(colour=colour, roughness=0.4))
152 )
153 self.bodies.append(body)
154
155 @on_input("respawn")
156 def _on_respawn(self, _event):
157 self.spawn_batch()
158
159 def on_fixed_update(self, dt):
160 # Rotating the input by the camera's yaw turns "up the screen" into "away from the camera".
161 walk = Input.value("move").rotated(-self.camera.yaw) * WALK_SPEED
162 fall = float(self.character.velocity.y) + float(self.character.world.gravity.y) * dt
163 self.character.velocity = Vec3(walk.x, fall, walk.y)
164 self.character.move_and_slide(dt)
165
166 def on_update(self, dt):
167 turn = Input.value("orbit")
168 speed = self.camera.stick_speed * dt
169 self.camera.orbit(-turn.x * speed, -turn.y * speed)
170
171 resting = sum(1 for body in self.bodies if body.world_position.y < REST_HEIGHT)
172 self.status.text = f"Resting: {resting}/{len(self.bodies)} On floor: {self.character.is_on_floor()}"
173
174
175if __name__ == "__main__":
176 App(title="3D Physics Playground", width=1280, height=720).run(PhysicsScene())