245 lines
7.9 KiB
Rust
245 lines
7.9 KiB
Rust
use std::f32::consts::{FRAC_PI_3, FRAC_PI_4};
|
|
|
|
use bevy::prelude::{
|
|
Commands, Entity, Quat, Query, Res, ResMut, Time, Transform, Vec3, With, Without,
|
|
};
|
|
use bevy_rapier3d::prelude::{
|
|
CollisionGroups, ExternalForce, Group, MultibodyJoint, QueryFilter, RapierConfiguration,
|
|
RapierContext, ReadMassProperties, TimestepMode, Velocity,
|
|
};
|
|
|
|
#[cfg(feature = "inspector")]
|
|
use super::ActionDebugInstant;
|
|
use super::{CatControllerSettings, CatControllerState, CyberLean, MovementSettings, Tunneling};
|
|
use crate::{
|
|
bike::{CyberBikeBody, CyberSteering, CyberWheel, WheelConfig, BIKE_WHEEL_COLLISION_GROUP},
|
|
input::InputState,
|
|
};
|
|
|
|
fn yaw_to_angle(yaw: f32) -> f32 {
|
|
let v = yaw.powi(5) * FRAC_PI_4;
|
|
if v.is_normal() {
|
|
v
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
fn rotate_point(pt: &Vec3, rot: &Quat) -> Vec3 {
|
|
// thanks to https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
|
|
let [x, y, z] = pt.normalize().to_array();
|
|
let qpt = Quat::from_xyzw(x, y, z, 0.0);
|
|
// p' = rot^-1 * qpt * rot
|
|
let rot_qpt = rot.inverse() * qpt * *rot;
|
|
|
|
// why does this need to be inverted???
|
|
-Vec3::from_array([rot_qpt.x, rot_qpt.y, rot_qpt.z])
|
|
}
|
|
|
|
pub(super) fn timestep_setup(mut config: ResMut<RapierConfiguration>) {
|
|
let ts = TimestepMode::Fixed {
|
|
dt: 1.0 / 60.0,
|
|
substeps: 2,
|
|
};
|
|
config.timestep_mode = ts;
|
|
}
|
|
|
|
/// The gravity vector points from the cyberbike to the center of the planet.
|
|
pub(super) fn gravity(
|
|
mut query: Query<(&Transform, &mut ExternalForce), With<CyberBikeBody>>,
|
|
settings: Res<MovementSettings>,
|
|
mut rapier_config: ResMut<RapierConfiguration>,
|
|
#[cfg(feature = "inspector")] mut debug_instant: ResMut<ActionDebugInstant>,
|
|
) {
|
|
let (xform, mut forces) = query.single_mut();
|
|
|
|
#[cfg(feature = "inspectorb")]
|
|
{
|
|
if debug_instant.elapsed().as_millis() > 6000 {
|
|
dbg!(&forces);
|
|
debug_instant.reset();
|
|
}
|
|
}
|
|
|
|
rapier_config.gravity = xform.translation.normalize() * -settings.gravity;
|
|
forces.force = Vec3::ZERO;
|
|
forces.torque = Vec3::ZERO;
|
|
}
|
|
|
|
/// The desired lean angle, given steering input and speed.
|
|
pub(super) fn cyber_lean(
|
|
bike_state: Query<(&Velocity, &Transform), With<CyberBikeBody>>,
|
|
wheels: Query<&Transform, With<CyberWheel>>,
|
|
input: Res<InputState>,
|
|
gravity_settings: Res<MovementSettings>,
|
|
mut lean: ResMut<CyberLean>,
|
|
) {
|
|
let (velocity, xform) = bike_state.single();
|
|
let vel = velocity.linvel.dot(xform.forward());
|
|
let v_squared = vel.powi(2);
|
|
let steering_angle = yaw_to_angle(input.yaw);
|
|
let wheels: Vec<_> = wheels.iter().map(|w| w.translation).collect();
|
|
let wheel_base = (wheels[0] - wheels[1]).length();
|
|
let radius = wheel_base / steering_angle.tan();
|
|
let gravity = gravity_settings.gravity;
|
|
let v2_r = v_squared / radius;
|
|
let tan_theta = (v2_r / gravity).clamp(-FRAC_PI_3, FRAC_PI_3);
|
|
|
|
if tan_theta.is_finite() && !tan_theta.is_subnormal() {
|
|
lean.lean = tan_theta.atan().clamp(-FRAC_PI_4, FRAC_PI_4);
|
|
} else {
|
|
lean.lean = 0.0;
|
|
}
|
|
}
|
|
|
|
/// PID-based controller for stabilizing attitude; keeps the cyberbike upright.
|
|
pub(super) fn falling_cat(
|
|
mut bike_query: Query<(&Transform, &mut ExternalForce, &mut CatControllerState)>,
|
|
time: Res<Time>,
|
|
settings: Res<CatControllerSettings>,
|
|
lean: Res<CyberLean>,
|
|
) {
|
|
let (xform, mut forces, mut control_vars) = bike_query.single_mut();
|
|
let world_up = xform.translation.normalize();
|
|
let rot = Quat::from_axis_angle(xform.back(), lean.lean);
|
|
let target_up = rotate_point(&world_up, &rot).normalize();
|
|
|
|
let bike_right = xform.right();
|
|
|
|
let roll_error = bike_right.dot(target_up);
|
|
let pitch_error = world_up.dot(xform.back());
|
|
|
|
// only try to correct roll if we're not totally vertical
|
|
if pitch_error.abs() < 0.95 {
|
|
let (derivative, integral) = control_vars.update_roll(roll_error, time.delta_seconds());
|
|
let mag =
|
|
(settings.kp * roll_error) + (settings.ki * integral) + (settings.kd * derivative);
|
|
if mag.is_finite() {
|
|
forces.torque += xform.back() * mag;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Apply forces to the cyberbike as a result of input.
|
|
pub(super) fn input_forces(
|
|
settings: Res<MovementSettings>,
|
|
input: Res<InputState>,
|
|
mut braking_query: Query<&mut MultibodyJoint, (Without<CyberSteering>, With<CyberWheel>)>,
|
|
mut body_query: Query<
|
|
(&Transform, &mut ExternalForce),
|
|
(With<CyberBikeBody>, Without<CyberSteering>),
|
|
>,
|
|
mut steering_query: Query<&mut MultibodyJoint, With<CyberSteering>>,
|
|
) {
|
|
let (xform, mut forces) = body_query.single_mut();
|
|
|
|
// thrust
|
|
let thrust = xform.forward() * input.throttle * settings.accel;
|
|
let point = xform.translation + xform.back();
|
|
let force = ExternalForce::at_point(thrust, point, xform.translation);
|
|
*forces += force;
|
|
|
|
// brake + thrust
|
|
for mut motor in braking_query.iter_mut() {
|
|
let factor = if input.brake {
|
|
500.00
|
|
} else {
|
|
input.throttle * settings.accel
|
|
};
|
|
let speed = if input.brake { 0.0 } else { -70.0 };
|
|
motor.data = (*motor
|
|
.data
|
|
.as_revolute_mut()
|
|
.unwrap()
|
|
.set_motor_max_force(factor)
|
|
.set_motor_velocity(speed, factor))
|
|
.into();
|
|
}
|
|
|
|
// steering
|
|
let angle = yaw_to_angle(input.yaw);
|
|
let mut steering = steering_query.single_mut();
|
|
steering.data = (*steering
|
|
.data
|
|
.as_revolute_mut()
|
|
.unwrap()
|
|
.set_motor_position(-angle, 100.0, 0.5))
|
|
.into();
|
|
}
|
|
|
|
/// Don't let the wheels get stuck underneat the planet
|
|
pub(super) fn surface_fix(
|
|
mut commands: Commands,
|
|
mut wheel_query: Query<
|
|
(Entity, &Transform, &mut CollisionGroups),
|
|
(With<CyberWheel>, Without<Tunneling>),
|
|
>,
|
|
span_query: Query<&Transform, With<CyberWheel>>,
|
|
config: Res<WheelConfig>,
|
|
context: Res<RapierContext>,
|
|
) {
|
|
let mut wheels = Vec::new();
|
|
for xform in span_query.iter() {
|
|
wheels.push(xform);
|
|
}
|
|
let span = (wheels[1].translation - wheels[0].translation).normalize();
|
|
|
|
for (entity, xform, mut cgroups) in wheel_query.iter_mut() {
|
|
//let ray_dir = xform.translation.normalize();
|
|
let ray_dir = xform.right().cross(span).normalize();
|
|
if let Some(hit) = context.cast_ray_and_get_normal(
|
|
xform.translation,
|
|
ray_dir,
|
|
config.radius * 1.1,
|
|
false,
|
|
QueryFilter::only_fixed(),
|
|
) {
|
|
cgroups.memberships = Group::NONE;
|
|
cgroups.filters = Group::NONE;
|
|
commands.entity(entity).insert(Tunneling {
|
|
frames: 3,
|
|
dir: hit.1.normal,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn tunnel_out(
|
|
mut commands: Commands,
|
|
mut wheel_query: Query<
|
|
(
|
|
Entity,
|
|
&mut Tunneling,
|
|
&mut ExternalForce,
|
|
&mut CollisionGroups,
|
|
),
|
|
With<CyberWheel>,
|
|
>,
|
|
mprops: Query<&ReadMassProperties, With<CyberBikeBody>>,
|
|
settings: Res<MovementSettings>,
|
|
) {
|
|
let mprops = mprops.single();
|
|
for (entity, mut tunneling, mut force, mut cgroups) in wheel_query.iter_mut() {
|
|
if tunneling.frames == 0 {
|
|
commands.entity(entity).remove::<Tunneling>();
|
|
force.force = Vec3::ZERO;
|
|
(cgroups.memberships, cgroups.filters) = BIKE_WHEEL_COLLISION_GROUP;
|
|
continue;
|
|
}
|
|
tunneling.frames -= 1;
|
|
force.force = tunneling.dir * settings.gravity * 1.5 * mprops.0.mass;
|
|
#[cfg(feature = "inspector")]
|
|
dbg!(&tunneling);
|
|
}
|
|
}
|
|
|
|
/// General velocity-based drag-force calculation; does not take orientation
|
|
/// into account.
|
|
pub(super) fn drag(mut query: Query<(&Velocity, &mut ExternalForce), With<CyberBikeBody>>) {
|
|
let (vels, mut forces) = query.single_mut();
|
|
|
|
if let Some(vel) = vels.linvel.try_normalize() {
|
|
let v2 = vels.linvel.length_squared();
|
|
forces.force -= vel * (v2 * 0.02);
|
|
}
|
|
}
|