Skip to content

Commit 9e42c78

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 9e42c78

File tree

10 files changed

+1761
-16
lines changed

10 files changed

+1761
-16
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: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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),
81+
AngularJointMotor {
82+
target_velocity: 5.0,
83+
damping: 1.0,
84+
max_torque: 1000.0,
85+
motor_model: MotorModel::AccelerationBased,
86+
..default()
87+
},
88+
VelocityMotorJoint,
89+
));
90+
91+
// === Position-Controlled Revolute Joint (center) ===
92+
// Static anchor for the servo
93+
let position_anchor = commands
94+
.spawn((
95+
Sprite {
96+
color: Color::srgb(0.5, 0.5, 0.5),
97+
custom_size: Some(Vec2::splat(20.0)),
98+
..default()
99+
},
100+
Transform::from_xyz(0.0, 0.0, 0.0),
101+
RigidBody::Static,
102+
))
103+
.id();
104+
105+
// Servo arm - also positioned at anchor, rotates around its center
106+
let servo_arm = commands
107+
.spawn((
108+
Sprite {
109+
color: Color::srgb(0.3, 0.5, 0.9),
110+
custom_size: Some(Vec2::new(100.0, 20.0)),
111+
..default()
112+
},
113+
Transform::from_xyz(0.0, 0.0, 0.0),
114+
RigidBody::Dynamic,
115+
Mass(1.0),
116+
AngularInertia(1.0),
117+
SleepingDisabled, // Prevent sleeping so motor can always control it
118+
))
119+
.id();
120+
121+
// Revolute joint with position-controlled motor (servo behavior)
122+
//
123+
// Using spring parameters (frequency, damping_ratio) for timestep-independent behavior.
124+
// This provides predictable spring-damper dynamics regardless of substep count.
125+
// - frequency: 5 Hz = fairly stiff spring
126+
// - damping_ratio: 1.0 = critically damped (fastest approach without overshoot)
127+
commands.spawn((
128+
RevoluteJoint::new(position_anchor, servo_arm),
129+
AngularJointMotor::new(0.0)
130+
.with_spring_parameters(5.0, 1.0)
131+
.with_target_position_value(0.0)
132+
.with_max_torque(Scalar::MAX),
133+
PositionMotorJoint,
134+
));
135+
136+
// === Prismatic Joint with Linear Motor (right side) ===
137+
let piston_base_sprite = Sprite {
138+
color: Color::srgb(0.5, 0.5, 0.5),
139+
custom_size: Some(Vec2::new(40.0, 200.0)),
140+
..default()
141+
};
142+
143+
let piston_sprite = Sprite {
144+
color: Color::srgb(0.3, 0.9, 0.3),
145+
custom_size: Some(Vec2::new(60.0, 40.0)),
146+
..default()
147+
};
148+
149+
// Static base for the piston
150+
let piston_base = commands
151+
.spawn((
152+
piston_base_sprite,
153+
Transform::from_xyz(200.0, 0.0, 0.0),
154+
RigidBody::Static,
155+
Position(Vector::new(200.0, 0.0)),
156+
))
157+
.id();
158+
159+
// Moving piston
160+
let piston = commands
161+
.spawn((
162+
piston_sprite,
163+
Transform::from_xyz(200.0, 0.0, 0.0),
164+
RigidBody::Dynamic,
165+
Mass(1.0),
166+
AngularInertia(1.0),
167+
SleepingDisabled, // Prevent sleeping so motor can always control it
168+
Position(Vector::new(200.0, 0.0)),
169+
))
170+
.id();
171+
172+
// frequency = 20 Hz, damping_ratio = 1.0 (critically damped)
173+
commands.spawn((
174+
PrismaticJoint::new(piston_base, piston).with_slider_axis(Vector::Y),
175+
LinearJointMotor::new(0.0)
176+
.with_spring_parameters(20.0, 1.0)
177+
.with_target_position_value(50.0)
178+
.with_max_force(Scalar::MAX),
179+
PrismaticMotorJoint,
180+
));
181+
182+
commands.spawn((
183+
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"),
184+
TextFont {
185+
font_size: 18.0,
186+
..default()
187+
},
188+
TextColor(Color::WHITE),
189+
Node {
190+
position_type: PositionType::Absolute,
191+
top: Val::Px(10.0),
192+
left: Val::Px(10.0),
193+
..default()
194+
},
195+
UiText,
196+
));
197+
}
198+
199+
fn control_motors(
200+
keyboard: Res<ButtonInput<KeyCode>>,
201+
mut velocity_motors: Query<&mut AngularJointMotor, With<VelocityMotorJoint>>,
202+
mut position_motors: Query<
203+
&mut AngularJointMotor,
204+
(With<PositionMotorJoint>, Without<VelocityMotorJoint>),
205+
>,
206+
mut prismatic_motors: Query<&mut LinearJointMotor, With<PrismaticMotorJoint>>,
207+
) {
208+
for mut motor in velocity_motors.iter_mut() {
209+
if keyboard.just_pressed(KeyCode::ArrowUp) {
210+
motor.target_velocity += 1.0;
211+
}
212+
if keyboard.just_pressed(KeyCode::ArrowDown) {
213+
motor.target_velocity -= 1.0;
214+
}
215+
if keyboard.just_pressed(KeyCode::Space) {
216+
if motor.target_velocity != 0.0 {
217+
motor.target_velocity = 0.0;
218+
} else {
219+
motor.target_velocity = 5.0;
220+
}
221+
}
222+
}
223+
224+
for mut motor in position_motors.iter_mut() {
225+
if keyboard.just_pressed(KeyCode::KeyA) {
226+
motor.target_position += 0.5;
227+
}
228+
if keyboard.just_pressed(KeyCode::KeyD) {
229+
motor.target_position -= 0.5;
230+
}
231+
if keyboard.just_pressed(KeyCode::Space) {
232+
motor.target_position = 0.0;
233+
}
234+
}
235+
236+
for mut motor in prismatic_motors.iter_mut() {
237+
if keyboard.just_pressed(KeyCode::KeyW) {
238+
motor.target_position += 25.0;
239+
}
240+
if keyboard.just_pressed(KeyCode::KeyS) {
241+
motor.target_position -= 25.0;
242+
}
243+
if keyboard.just_pressed(KeyCode::Space) {
244+
if motor.target_position != 0.0 {
245+
motor.target_position = 0.0;
246+
} else {
247+
motor.target_position = 50.0;
248+
}
249+
}
250+
}
251+
}
252+
253+
fn update_ui(
254+
velocity_motors: Query<&AngularJointMotor, With<VelocityMotorJoint>>,
255+
position_motors: Query<&AngularJointMotor, With<PositionMotorJoint>>,
256+
prismatic_motors: Query<&LinearJointMotor, With<PrismaticMotorJoint>>,
257+
mut ui_text: Query<&mut Text, With<UiText>>,
258+
) {
259+
let velocity_target = velocity_motors
260+
.iter()
261+
.next()
262+
.map(|m| m.target_velocity)
263+
.unwrap_or(0.0);
264+
let position_target = position_motors
265+
.iter()
266+
.next()
267+
.map(|m| m.target_position)
268+
.unwrap_or(0.0);
269+
let prismatic_pos = prismatic_motors
270+
.iter()
271+
.next()
272+
.map(|m| m.target_position)
273+
.unwrap_or(0.0);
274+
275+
for mut text in ui_text.iter_mut() {
276+
text.0 = format!(
277+
"Motor Joints Demo\n\n\
278+
Arrow Up/Down: Velocity motor speed\n\
279+
A/D: Position motor angle\n\
280+
W/S: Prismatic motor position\n\
281+
Space: Reset motors\n\n\
282+
Velocity: {:.1} rad/s\n\
283+
Position: {:.2} rad\n\
284+
Prismatic: {:.1} units",
285+
velocity_target, position_target, prismatic_pos
286+
);
287+
}
288+
}

src/dynamics/joints/mod.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,17 @@ Therefore, they have 3 translational DOF and 3 rotational DOF, a total of 6 DOF.
214214
215215
mod distance;
216216
mod fixed;
217+
mod motor;
217218
mod prismatic;
218219
mod revolute;
219220
#[cfg(feature = "3d")]
220221
mod spherical;
222+
#[cfg(test)]
223+
mod tests;
221224

222225
pub use distance::DistanceJoint;
223226
pub use fixed::FixedJoint;
227+
pub use motor::{AngularJointMotor, LinearJointMotor, MotorModel};
224228
pub use prismatic::PrismaticJoint;
225229
pub use revolute::RevoluteJoint;
226230
#[cfg(feature = "3d")]
@@ -660,6 +664,7 @@ pub struct JointDamping {
660664
pub struct JointForces {
661665
force: Vector,
662666
torque: AngularVector,
667+
motor_force: Scalar,
663668
}
664669

665670
impl JointForces {
@@ -669,6 +674,7 @@ impl JointForces {
669674
Self {
670675
force: Vector::ZERO,
671676
torque: AngularVector::ZERO,
677+
motor_force: 0.0,
672678
}
673679
}
674680

@@ -684,21 +690,38 @@ impl JointForces {
684690
self.torque
685691
}
686692

693+
/// Returns the force or torque applied by the motor, if any.
694+
///
695+
/// For angular motors ([`AngularJointMotor`]), this is the torque in N·m.
696+
/// For linear motors ([`LinearJointMotor`]), this is the force in N.
697+
#[inline]
698+
pub const fn motor_force(&self) -> Scalar {
699+
self.motor_force
700+
}
701+
687702
/// Sets the force applied by the joint.
688703
///
689-
/// This should be done automatically by the joint solver,
704+
/// This should be done automatically by the joint solver.
690705
#[inline]
691706
pub const fn set_force(&mut self, force: Vector) {
692707
self.force = force;
693708
}
694709

695710
/// Sets the torque applied by the joint.
696711
///
697-
/// This should be done automatically by the joint solver,
712+
/// This should be done automatically by the joint solver.
698713
#[inline]
699714
pub const fn set_torque(&mut self, torque: AngularVector) {
700715
self.torque = torque;
701716
}
717+
718+
/// Sets the motor force or torque.
719+
///
720+
/// This should be done automatically by the joint solver.
721+
#[inline]
722+
pub const fn set_motor_force(&mut self, motor_force: Scalar) {
723+
self.motor_force = motor_force;
724+
}
702725
}
703726

704727
/// The [reference frame] of a body that is being constrained by a [joint](self).

0 commit comments

Comments
 (0)