Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/avian2d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ required-features = ["2d", "default-collider"]
name = "revolute_joint_2d"
required-features = ["2d", "default-collider"]

[[example]]
name = "joint_motors_2d"
required-features = ["2d", "default-collider"]

[[example]]
name = "sensor"
required-features = ["2d", "default-collider"]
Expand Down
271 changes: 271 additions & 0 deletions crates/avian2d/examples/joint_motors_2d.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
//! Demonstrates motor joints in 2D.
//!
//! - Left side: Revolute joint with velocity-controlled angular motor (spinning wheel)
//! - Center: Revolute joint with position-controlled angular motor (servo)
//! - Right side: Prismatic joint with linear motor (piston)
//!
//! Controls:
//! - Arrow Up/Down: Adjust left motor target velocity
//! - A/D: Adjust center motor target angle
//! - W/S: Adjust right motor target position
//! - Space: Toggle motors on/off

use avian2d::{math::*, prelude::*};
use bevy::prelude::*;
use examples_common_2d::ExampleCommonPlugin;

fn main() {
App::new()
.add_plugins((
DefaultPlugins,
ExampleCommonPlugin,
PhysicsPlugins::default(),
))
.insert_resource(ClearColor(Color::srgb(0.05, 0.05, 0.1)))
.insert_resource(Gravity(Vector::NEG_Y * 1000.0))
.add_systems(Startup, setup)
.add_systems(Update, (control_motors, update_ui))
.run();
}

#[derive(Component)]
struct VelocityMotorJoint;

#[derive(Component)]
struct PositionMotorJoint;

#[derive(Component)]
struct PrismaticMotorJoint;

#[derive(Component)]
struct UiText;

fn setup(mut commands: Commands) {
commands.spawn(Camera2d);

// === Velocity-Controlled Revolute Joint (left side) ===
// Static anchor for the wheel
let velocity_anchor = commands
.spawn((
Sprite {
color: Color::srgb(0.5, 0.5, 0.5),
custom_size: Some(Vec2::splat(20.0)),
..default()
},
Transform::from_xyz(-200.0, 0.0, 0.0),
RigidBody::Static,
))
.id();

// Spinning wheel
let velocity_wheel = commands
.spawn((
Sprite {
color: Color::srgb(0.9, 0.3, 0.3),
custom_size: Some(Vec2::splat(80.0)),
..default()
},
Transform::from_xyz(-200.0, 0.0, 0.0),
RigidBody::Dynamic,
Mass(1.0),
AngularInertia(1.0),
SleepingDisabled, // Prevent sleeping so motor can always control it
))
.id();

// Revolute joint with velocity-controlled motor
// Default anchors are at body centers (Vector::ZERO)
commands.spawn((
RevoluteJoint::new(velocity_anchor, velocity_wheel).with_motor(AngularMotor {
target_velocity: 5.0,
max_torque: 1000.0,
motor_model: MotorModel::AccelerationBased {
stiffness: 0.0,
damping: 1.0,
},
..default()
}),
VelocityMotorJoint,
));

// === Position-Controlled Revolute Joint (center) ===
// Static anchor for the servo
let position_anchor = commands
.spawn((
Sprite {
color: Color::srgb(0.5, 0.5, 0.5),
custom_size: Some(Vec2::splat(20.0)),
..default()
},
Transform::from_xyz(0.0, 0.0, 0.0),
RigidBody::Static,
))
.id();

// Servo arm - also positioned at anchor, rotates around its center
let servo_arm = commands
.spawn((
Sprite {
color: Color::srgb(0.3, 0.5, 0.9),
custom_size: Some(Vec2::new(100.0, 20.0)),
..default()
},
Transform::from_xyz(0.0, 0.0, 0.0),
RigidBody::Dynamic,
Mass(1.0),
AngularInertia(1.0),
SleepingDisabled, // Prevent sleeping so motor can always control it
))
.id();

// Revolute joint with position-controlled motor (servo behavior)
//
// Using spring parameters (frequency, damping_ratio) for stable behavior.
// This provides predictable spring-damper dynamics across different configurations.
// - frequency: 5 Hz = fairly stiff spring
// - damping_ratio: 1.0 = critically damped (fastest approach without overshoot)
commands.spawn((
RevoluteJoint::new(position_anchor, servo_arm).with_motor(
AngularMotor::new(MotorModel::SpringDamper {
frequency: 5.0,
damping_ratio: 1.0,
})
.with_target_position(0.0)
.with_max_torque(Scalar::MAX),
),
PositionMotorJoint,
));

// === Prismatic Joint with Linear Motor (right side) ===
let piston_base_sprite = Sprite {
color: Color::srgb(0.5, 0.5, 0.5),
custom_size: Some(Vec2::new(40.0, 200.0)),
..default()
};

let piston_sprite = Sprite {
color: Color::srgb(0.3, 0.9, 0.3),
custom_size: Some(Vec2::new(60.0, 40.0)),
..default()
};

// Static base for the piston
let piston_base = commands
.spawn((
piston_base_sprite,
Transform::from_xyz(200.0, 0.0, 0.0),
RigidBody::Static,
Position(Vector::new(200.0, 0.0)),
))
.id();

// Moving piston
let piston = commands
.spawn((
piston_sprite,
Transform::from_xyz(200.0, 0.0, 0.0),
RigidBody::Dynamic,
Mass(1.0),
AngularInertia(1.0),
SleepingDisabled, // Prevent sleeping so motor can always control it
Position(Vector::new(200.0, 0.0)),
))
.id();

// frequency = 20 Hz, damping_ratio = 1.0 (critically damped)
commands.spawn((
PrismaticJoint::new(piston_base, piston)
.with_slider_axis(Vector::Y)
.with_limits(-100.0, 100.0)
.with_motor(
LinearMotor::new(MotorModel::SpringDamper {
frequency: 20.0,
damping_ratio: 1.0,
})
.with_target_position(50.0)
.with_max_force(Scalar::MAX),
),
PrismaticMotorJoint,
));

commands.spawn((
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"),
TextFont {
font_size: 18.0,
..default()
},
TextColor(Color::WHITE),
Node {
position_type: PositionType::Absolute,
top: Val::Px(10.0),
left: Val::Px(10.0),
..default()
},
UiText,
));
}

fn control_motors(
keyboard: Res<ButtonInput<KeyCode>>,
mut velocity_motors: Single<&mut RevoluteJoint, With<VelocityMotorJoint>>,
mut position_motors: Single<
&mut RevoluteJoint,
(With<PositionMotorJoint>, Without<VelocityMotorJoint>),
>,
mut prismatic_motors: Single<&mut PrismaticJoint, With<PrismaticMotorJoint>>,
) {
// Velocity-controlled revolute joint motor
if keyboard.just_pressed(KeyCode::ArrowUp) {
velocity_motors.motor.target_velocity += 1.0;
}
if keyboard.just_pressed(KeyCode::ArrowDown) {
velocity_motors.motor.target_velocity -= 1.0;
}

// Position-controlled revolute joint motor
if keyboard.just_pressed(KeyCode::KeyA) {
position_motors.motor.target_position += 0.5;
}
if keyboard.just_pressed(KeyCode::KeyD) {
position_motors.motor.target_position -= 0.5;
}

// Position-controlled prismatic joint motor
if keyboard.just_pressed(KeyCode::KeyW) {
prismatic_motors.motor.target_position += 25.0;
}
if keyboard.just_pressed(KeyCode::KeyS) {
prismatic_motors.motor.target_position -= 25.0;
}

// Toggle motors on/off
if keyboard.just_pressed(KeyCode::Space) {
velocity_motors.motor.enabled = !velocity_motors.motor.enabled;
position_motors.motor.enabled = !position_motors.motor.enabled;
prismatic_motors.motor.enabled = !prismatic_motors.motor.enabled;
}
}

fn update_ui(
velocity_motor: Single<&RevoluteJoint, With<VelocityMotorJoint>>,
position_motor: Single<&RevoluteJoint, With<PositionMotorJoint>>,
prismatic_motor: Single<&PrismaticJoint, With<PrismaticMotorJoint>>,
mut ui_text: Single<&mut Text, With<UiText>>,
) {
ui_text.0 = format!(
"Motor Joints Demo\n\n\
Arrow Up/Down: Velocity motor speed\n\
A/D: Position motor angle\n\
W/S: Prismatic motor position\n\
Space: Toggle motors\n\n\
Velocity: {:.1} rad/s\n\
Position: {:.2} rad\n\
Prismatic: {:.1} units\n\
Enabled: {}",
velocity_motor.motor.target_velocity,
position_motor.motor.target_position,
prismatic_motor.motor.target_position,
// We can pick any of the motors here since they are toggled together
velocity_motor.motor.enabled
);
}
4 changes: 4 additions & 0 deletions crates/avian3d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ required-features = ["3d", "default-collider"]
name = "revolute_joint_3d"
required-features = ["3d", "default-collider"]

[[example]]
name = "joint_motors_3d"
required-features = ["3d", "default-collider"]

[[example]]
name = "gyroscopic_motion"
required-features = ["3d", "default-collider"]
Expand Down
Loading