Character Push¶
a character shoves what it walks into, by mass.
▶ Run in browserTags: physics 3d character
A CharacterBody3D moves where move_and_slide(dt) sends it, so on its own a
crate in the way would only block it. push_factor turns that block into a
shove, and it defaults to 1.0: walk into a crate and the crate goes.
Each blocking contact hands the struck body an impulse of
push_factor * (M * m / (M + m)) * approach_speed, where M is the character’s
mass and m the mass of what it hit. At 1.0 that is the impulse of a
perfectly inelastic collision, so one contact never sends a crate off faster
than the character was walking. Set push_factor = 0.0 for a pure block.
The character here weighs 1 kg rather than the 70 kg default, so the three crates in a row (1 kg, 20 kg and 200 kg) react three different ways: the light one skitters ahead, the middle one is carried, the heavy one barely moves.
The wall is static: it has no finite mass to share, so it takes no push and the character simply stops.
A floor contact is exempt at any setting: jump onto a crate and standing on it does not drive it into the ground.
The stack at the far end falls asleep while it waits. Sleep is per island, so shoving the bottom crate wakes the whole stack.
push_factoris read on every move, so changing it mid-walk (as a “carrying something heavy” state would) takes effect at once.collisionslists the blocking contacts of the last move whatever the factor is, for a game that wants to add physics of its own. The readout shows them.
move_and_slide does not apply gravity, so the character’s loop in
on_fixed_update adds it to velocity.y itself before each move.
Walk with WASD, the arrow keys, a left stick or the on-screen stick, and jump
with Space or the A button. The buttons along the bottom change push_factor
(also + and -, and 0 for no push) and rebuild the scene (R).
Source¶
1# /// simvx
2# tags = ["3d", "physics", "character"]
3# ///
4"""Character Push: a character shoves what it walks into, by mass.
5
6A `CharacterBody3D` moves where `move_and_slide(dt)` sends it, so on its own a
7crate in the way would only block it. `push_factor` turns that block into a
8shove, and it defaults to `1.0`: walk into a crate and the crate goes.
9
10Each blocking contact hands the struck body an impulse of
11`push_factor * (M * m / (M + m)) * approach_speed`, where `M` is the character's
12`mass` and `m` the mass of what it hit. At `1.0` that is the impulse of a
13perfectly inelastic collision, so one contact never sends a crate off faster
14than the character was walking. Set `push_factor = 0.0` for a pure block.
15
16- The character here weighs 1 kg rather than the 70 kg default, so the three
17 crates in a row (1 kg, 20 kg and 200 kg) react three different ways: the light
18 one skitters ahead, the middle one is carried, the heavy one barely moves.
19- The wall is static: it has no finite mass to share, so it takes no push and
20 the character simply stops.
21- A floor contact is exempt at any setting: jump onto a crate and standing on it
22 does not drive it into the ground.
23- The stack at the far end falls asleep while it waits. Sleep is per island, so
24 shoving the bottom crate wakes the whole stack.
25- `push_factor` is read on every move, so changing it mid-walk (as a "carrying
26 something heavy" state would) takes effect at once.
27- `collisions` lists the blocking contacts of the last move whatever the factor
28 is, for a game that wants to add physics of its own. The readout shows them.
29
30`move_and_slide` does not apply gravity, so the character's loop in
31`on_fixed_update` adds it to `velocity.y` itself before each move.
32
33Walk with WASD, the arrow keys, a left stick or the on-screen stick, and jump
34with Space or the A button. The buttons along the bottom change `push_factor`
35(also + and -, and 0 for no push) and rebuild the scene (R).
36"""
37
38from simvx.core import (
39 Anchor,
40 Axis2D,
41 BodyMode,
42 BoxShape3D,
43 Button,
44 Camera3D,
45 CanvasLayer,
46 CharacterBody3D,
47 Controller,
48 DirectionalLight3D,
49 HBoxContainer,
50 Input,
51 Key,
52 Label,
53 Material,
54 Mesh,
55 MeshInstance3D,
56 Node,
57 PhysicsBody3D,
58 TouchController,
59 Vec3,
60 on_input,
61)
62from simvx.graphics import App
63
64SPEED = 4.0
65JUMP_SPEED = 7.0
66GRAVITY = 18.0
67PUSH_STEP = 0.1
68CRATE = Vec3(1.0, 1.0, 1.0)
69
70
71def box(size, colour, *, mode=BodyMode.DYNAMIC, mass=1.0, **kwargs) -> PhysicsBody3D:
72 """A body whose collider and mesh are one box of ``size``."""
73 body = PhysicsBody3D(shape=BoxShape3D(half_extents=Vec3(size) / 2), mode=mode, mass=mass, **kwargs)
74 body.add_child(MeshInstance3D(mesh=Mesh.box(size), material=Material(colour=colour, roughness=0.7)))
75 return body
76
77
78class CharacterPush(Node):
79 input_context = {
80 "move": [Axis2D.WASD, Axis2D.ARROWS, Controller.LEFT_STICK],
81 "jump": [Key.SPACE, Controller.A],
82 "push_down": [Key.MINUS, Key.KP_SUBTRACT],
83 "push_up": [Key.EQUAL, Key.KP_ADD],
84 "no_push": [Key.KEY_0],
85 "rebuild": [Key.R],
86 }
87
88 def on_ready(self):
89 self.add_child(Camera3D(position=(-3.5, 7.0, 9.5), look_at=(2.5, 0.5, 0.0)))
90 self.add_child(DirectionalLight3D(direction=(-6, -12, -8), colour=(1.0, 0.96, 0.85), intensity=3.0))
91 # A dim fill from the camera side, so faces turned toward the viewer are not black.
92 self.add_child(DirectionalLight3D(direction=(8, -6, -10), colour=(0.6, 0.68, 0.85), intensity=1.0))
93
94 self.add_child(box((28, 1, 28), (0.2, 0.22, 0.26, 1), mode=BodyMode.STATIC, position=(3, -0.5, 0)))
95 self.wall = self.add_child(box((1, 3, 6), (0.42, 0.4, 0.46, 1), mode=BodyMode.STATIC, position=(6.5, 1.5, 2.5)))
96 # Identical but for their mass, and darker as they get heavier.
97 self.crates = [
98 self.add_child(box(CRATE, colour, mass=mass, name=f"{mass:g} kg", position=(2.0, 0.5, z)))
99 for mass, z, colour in (
100 (1.0, -2.5, (0.95, 0.78, 0.3, 1)),
101 (20.0, 0.0, (0.85, 0.48, 0.2, 1)),
102 (200.0, 2.5, (0.55, 0.22, 0.16, 1)),
103 )
104 ]
105 self.stack = [
106 self.add_child(
107 box(CRATE, (0.35 + 0.2 * (i % 2), 0.55, 0.8, 1), mass=2.0, position=(6.5, 0.5 + i * 1.02, -2.5))
108 )
109 for i in range(3)
110 ]
111 self.start_x = [float(c.world_position.x) for c in self.crates]
112
113 size = Vec3(0.7, 1.8, 0.7)
114 self.character = self.add_child(
115 CharacterBody3D(shape=BoxShape3D(half_extents=size / 2), mass=1.0, position=(-2.0, 0.9, 0.0))
116 )
117 self.character.add_child(
118 MeshInstance3D(mesh=Mesh.box(size), material=Material(colour=(0.45, 0.85, 0.55, 1), roughness=0.4))
119 )
120
121 hud = self.add_child(CanvasLayer())
122 self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=16, font_size=18))
123 buttons = hud.add_child(HBoxContainer(anchor=Anchor.CENTER_BOTTOM, inset=16))
124 for text, action in (
125 ("Push -", "push_down"),
126 ("Push +", "push_up"),
127 ("No push", "no_push"),
128 ("Rebuild", "rebuild"),
129 ):
130 buttons.add_child(Button(text, action=action))
131 buttons.autosize_to_content()
132 self.add_child(TouchController(buttons=(Controller.A,)))
133
134 # Example help overlay: not part of the technique shown.
135 hud.add_child(Label("WASD / arrows / stick: walk\nSpace / A: jump", anchor=Anchor.TOP_RIGHT, inset=16))
136
137 @on_input(("push_down", "push_up"))
138 def change_push(self, event):
139 # The Property's range clamps at 0.
140 self.character.push_factor += PUSH_STEP if event.action == "push_up" else -PUSH_STEP
141
142 @on_input("no_push")
143 def remove_push(self, event):
144 self.character.push_factor = 0.0
145
146 @on_input("rebuild")
147 def rebuild(self, event):
148 self.tree.change_scene(CharacterPush())
149
150 def on_fixed_update(self, dt):
151 character = self.character
152 walk = Input.value("move") * SPEED
153 fall = character.velocity.y - GRAVITY * dt
154 if Input.just_pressed("jump") and character.is_on_floor():
155 fall = JUMP_SPEED
156 character.velocity = Vec3(walk.x, fall, walk.y)
157 character.move_and_slide(dt)
158
159 def on_update(self, dt):
160 travel = " ".join(
161 f"{c.name} {float(c.world_position.x) - x0:+.2f} m" for c, x0 in zip(self.crates, self.start_x, strict=True)
162 )
163 asleep = sum(c.is_sleeping for c in self.stack)
164 self.status.text = (
165 f"push_factor {self.character.push_factor:.2f}\n"
166 f"crate travel: {travel}\n"
167 f"stack asleep: {asleep}/{len(self.stack)}\n"
168 f"blocking contacts last move: {len(self.character.collisions)}"
169 )
170
171
172if __name__ == "__main__":
173 App(title="Character Push", width=1280, height=720).run(CharacterPush())