Skip to content

Commit 2555b89

Browse files
committed
Add joint motors for revolute and prismatic joints
Supports velocity and position control with optional timestep-independent spring-damper parameters (frequency, damping_ratio). Includes warm starting and motor force feedback via JointForces.
1 parent 0c06418 commit 2555b89

File tree

13 files changed

+2314
-18
lines changed

13 files changed

+2314
-18
lines changed

crates/avian2d/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,7 @@ required-features = ["2d", "default-collider"]
208208
[[example]]
209209
name = "debugdump_2d"
210210
required-features = ["2d", "debug-plugin"]
211+
212+
[[example]]
213+
name = "motor_joints_2d"
214+
required-features = ["2d", "default-collider"]
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
//! Demonstrates motor joints in 2D.
2+
//!
3+
//! - Left side: Revolute joint with velocity-controlled angular motor (spinning wheel)
4+
//! - Center: Revolute joint with position-controlled angular motor (servo)
5+
//! - Right side: Prismatic joint with linear motor (piston)
6+
//!
7+
//! Controls:
8+
//! - Arrow Up/Down: Adjust left motor target velocity
9+
//! - A/D: Adjust center motor target angle
10+
//! - W/S: Adjust right motor target position
11+
//! - Space: Toggle motors on/off
12+
13+
use avian2d::{math::*, prelude::*};
14+
use bevy::prelude::*;
15+
use examples_common_2d::ExampleCommonPlugin;
16+
17+
fn main() {
18+
App::new()
19+
.add_plugins((
20+
DefaultPlugins,
21+
ExampleCommonPlugin,
22+
PhysicsPlugins::default(),
23+
))
24+
.insert_resource(ClearColor(Color::srgb(0.05, 0.05, 0.1)))
25+
.insert_resource(SubstepCount(50))
26+
.insert_resource(Gravity(Vector::ZERO)) // No gravity for clearer motor demo
27+
.add_systems(Startup, setup)
28+
.add_systems(Update, (control_motors, update_ui))
29+
.run();
30+
}
31+
32+
#[derive(Component)]
33+
struct VelocityMotorJoint;
34+
35+
#[derive(Component)]
36+
struct PositionMotorJoint;
37+
38+
#[derive(Component)]
39+
struct PrismaticMotorJoint;
40+
41+
#[derive(Component)]
42+
struct UiText;
43+
44+
fn setup(mut commands: Commands) {
45+
commands.spawn(Camera2d);
46+
47+
// === Velocity-Controlled Revolute Joint (left side) ===
48+
// Static anchor for the wheel
49+
let velocity_anchor = commands
50+
.spawn((
51+
Sprite {
52+
color: Color::srgb(0.5, 0.5, 0.5),
53+
custom_size: Some(Vec2::splat(20.0)),
54+
..default()
55+
},
56+
Transform::from_xyz(-200.0, 0.0, 0.0),
57+
RigidBody::Static,
58+
))
59+
.id();
60+
61+
// Spinning wheel
62+
let velocity_wheel = commands
63+
.spawn((
64+
Sprite {
65+
color: Color::srgb(0.9, 0.3, 0.3),
66+
custom_size: Some(Vec2::splat(80.0)),
67+
..default()
68+
},
69+
Transform::from_xyz(-200.0, 0.0, 0.0),
70+
RigidBody::Dynamic,
71+
Mass(1.0),
72+
AngularInertia(1.0),
73+
SleepingDisabled, // Prevent sleeping so motor can always control it
74+
))
75+
.id();
76+
77+
// Revolute joint with velocity-controlled motor
78+
// Default anchors are at body centers (Vector::ZERO)
79+
commands.spawn((
80+
RevoluteJoint::new(velocity_anchor, velocity_wheel).with_motor(AngularMotor {
81+
target_velocity: 5.0,
82+
max_torque: 1000.0,
83+
motor_model: MotorModel::AccelerationBased {
84+
stiffness: 0.0,
85+
damping: 1.0,
86+
},
87+
..default()
88+
}),
89+
VelocityMotorJoint,
90+
));
91+
92+
// === Position-Controlled Revolute Joint (center) ===
93+
// Static anchor for the servo
94+
let position_anchor = commands
95+
.spawn((
96+
Sprite {
97+
color: Color::srgb(0.5, 0.5, 0.5),
98+
custom_size: Some(Vec2::splat(20.0)),
99+
..default()
100+
},
101+
Transform::from_xyz(0.0, 0.0, 0.0),
102+
RigidBody::Static,
103+
))
104+
.id();
105+
106+
// Servo arm - also positioned at anchor, rotates around its center
107+
let servo_arm = commands
108+
.spawn((
109+
Sprite {
110+
color: Color::srgb(0.3, 0.5, 0.9),
111+
custom_size: Some(Vec2::new(100.0, 20.0)),
112+
..default()
113+
},
114+
Transform::from_xyz(0.0, 0.0, 0.0),
115+
RigidBody::Dynamic,
116+
Mass(1.0),
117+
AngularInertia(1.0),
118+
SleepingDisabled, // Prevent sleeping so motor can always control it
119+
))
120+
.id();
121+
122+
// Revolute joint with position-controlled motor (servo behavior)
123+
//
124+
// Using spring parameters (frequency, damping_ratio) for timestep-independent behavior.
125+
// This provides predictable spring-damper dynamics regardless of substep count.
126+
// - frequency: 5 Hz = fairly stiff spring
127+
// - damping_ratio: 1.0 = critically damped (fastest approach without overshoot)
128+
commands.spawn((
129+
RevoluteJoint::new(position_anchor, servo_arm).with_motor(
130+
AngularMotor::new(MotorModel::SpringDamper {
131+
frequency: 5.0,
132+
damping_ratio: 1.0,
133+
})
134+
.with_target_position(0.0)
135+
.with_max_torque(Scalar::MAX),
136+
),
137+
PositionMotorJoint,
138+
));
139+
140+
// === Prismatic Joint with Linear Motor (right side) ===
141+
let piston_base_sprite = Sprite {
142+
color: Color::srgb(0.5, 0.5, 0.5),
143+
custom_size: Some(Vec2::new(40.0, 200.0)),
144+
..default()
145+
};
146+
147+
let piston_sprite = Sprite {
148+
color: Color::srgb(0.3, 0.9, 0.3),
149+
custom_size: Some(Vec2::new(60.0, 40.0)),
150+
..default()
151+
};
152+
153+
// Static base for the piston
154+
let piston_base = commands
155+
.spawn((
156+
piston_base_sprite,
157+
Transform::from_xyz(200.0, 0.0, 0.0),
158+
RigidBody::Static,
159+
Position(Vector::new(200.0, 0.0)),
160+
))
161+
.id();
162+
163+
// Moving piston
164+
let piston = commands
165+
.spawn((
166+
piston_sprite,
167+
Transform::from_xyz(200.0, 0.0, 0.0),
168+
RigidBody::Dynamic,
169+
Mass(1.0),
170+
AngularInertia(1.0),
171+
SleepingDisabled, // Prevent sleeping so motor can always control it
172+
Position(Vector::new(200.0, 0.0)),
173+
))
174+
.id();
175+
176+
// frequency = 20 Hz, damping_ratio = 1.0 (critically damped)
177+
commands.spawn((
178+
PrismaticJoint::new(piston_base, piston)
179+
.with_slider_axis(Vector::Y)
180+
.with_motor(
181+
LinearMotor::new(MotorModel::SpringDamper {
182+
frequency: 20.0,
183+
damping_ratio: 1.0,
184+
})
185+
.with_target_position(50.0)
186+
.with_max_force(Scalar::MAX),
187+
),
188+
PrismaticMotorJoint,
189+
));
190+
191+
commands.spawn((
192+
Text::new("Motor Joints Demo\n\nArrow Up/Down: Velocity motor speed\nA/D: Position motor angle\nW/S: Prismatic motor position\nSpace: Reset motors\n\nVelocity: 5.0 rad/s\nPosition: 0.00 rad\nPrismatic: 50.0 units"),
193+
TextFont {
194+
font_size: 18.0,
195+
..default()
196+
},
197+
TextColor(Color::WHITE),
198+
Node {
199+
position_type: PositionType::Absolute,
200+
top: Val::Px(10.0),
201+
left: Val::Px(10.0),
202+
..default()
203+
},
204+
UiText,
205+
));
206+
}
207+
208+
fn control_motors(
209+
keyboard: Res<ButtonInput<KeyCode>>,
210+
mut velocity_motors: Query<&mut RevoluteJoint, With<VelocityMotorJoint>>,
211+
mut position_motors: Query<
212+
&mut RevoluteJoint,
213+
(With<PositionMotorJoint>, Without<VelocityMotorJoint>),
214+
>,
215+
mut prismatic_motors: Query<&mut PrismaticJoint, With<PrismaticMotorJoint>>,
216+
) {
217+
for mut joint in velocity_motors.iter_mut() {
218+
let Some(motor) = joint.motor.as_mut() else {
219+
continue;
220+
};
221+
if keyboard.just_pressed(KeyCode::ArrowUp) {
222+
motor.target_velocity += 1.0;
223+
}
224+
if keyboard.just_pressed(KeyCode::ArrowDown) {
225+
motor.target_velocity -= 1.0;
226+
}
227+
if keyboard.just_pressed(KeyCode::Space) {
228+
if motor.target_velocity != 0.0 {
229+
motor.target_velocity = 0.0;
230+
} else {
231+
motor.target_velocity = 5.0;
232+
}
233+
}
234+
}
235+
236+
for mut joint in position_motors.iter_mut() {
237+
let Some(motor) = joint.motor.as_mut() else {
238+
continue;
239+
};
240+
if keyboard.just_pressed(KeyCode::KeyA) {
241+
motor.target_position += 0.5;
242+
}
243+
if keyboard.just_pressed(KeyCode::KeyD) {
244+
motor.target_position -= 0.5;
245+
}
246+
if keyboard.just_pressed(KeyCode::Space) {
247+
motor.target_position = 0.0;
248+
}
249+
}
250+
251+
for mut joint in prismatic_motors.iter_mut() {
252+
let Some(motor) = joint.motor.as_mut() else {
253+
continue;
254+
};
255+
if keyboard.just_pressed(KeyCode::KeyW) {
256+
motor.target_position += 25.0;
257+
}
258+
if keyboard.just_pressed(KeyCode::KeyS) {
259+
motor.target_position -= 25.0;
260+
}
261+
if keyboard.just_pressed(KeyCode::Space) {
262+
if motor.target_position != 0.0 {
263+
motor.target_position = 0.0;
264+
} else {
265+
motor.target_position = 50.0;
266+
}
267+
}
268+
}
269+
}
270+
271+
fn update_ui(
272+
velocity_motors: Query<&RevoluteJoint, With<VelocityMotorJoint>>,
273+
position_motors: Query<&RevoluteJoint, With<PositionMotorJoint>>,
274+
prismatic_motors: Query<&PrismaticJoint, With<PrismaticMotorJoint>>,
275+
mut ui_text: Query<&mut Text, With<UiText>>,
276+
) {
277+
let velocity_target = velocity_motors
278+
.iter()
279+
.next()
280+
.and_then(|j| j.motor.as_ref())
281+
.map(|m| m.target_velocity)
282+
.unwrap_or(0.0);
283+
let position_target = position_motors
284+
.iter()
285+
.next()
286+
.and_then(|j| j.motor.as_ref())
287+
.map(|m| m.target_position)
288+
.unwrap_or(0.0);
289+
let prismatic_pos = prismatic_motors
290+
.iter()
291+
.next()
292+
.and_then(|j| j.motor.as_ref())
293+
.map(|m| m.target_position)
294+
.unwrap_or(0.0);
295+
296+
for mut text in ui_text.iter_mut() {
297+
text.0 = format!(
298+
"Motor Joints Demo\n\n\
299+
Arrow Up/Down: Velocity motor speed\n\
300+
A/D: Position motor angle\n\
301+
W/S: Prismatic motor position\n\
302+
Space: Reset motors\n\n\
303+
Velocity: {:.1} rad/s\n\
304+
Position: {:.2} rad\n\
305+
Prismatic: {:.1} units",
306+
velocity_target, position_target, prismatic_pos
307+
);
308+
}
309+
}

0 commit comments

Comments
 (0)