diff --git a/avian2d/src/lib.rs b/avian2d/src/lib.rs index b7474ed8d..ef605bfdf 100644 --- a/avian2d/src/lib.rs +++ b/avian2d/src/lib.rs @@ -22,6 +22,20 @@ use bevy_tnua_physics_integration_layer::*; /// Add this plugin to use avian2d as a physics backend. /// /// This plugin should be used in addition to `TnuaControllerPlugin`. +/// Note that you should make sure both of these plugins use the same schedule. +/// This should usually be `PhysicsSchedule`, which by default is `FixedUpdate`. +/// +/// # Example +/// +/// ```ignore +/// App::new() +/// .add_plugins(( +/// DefaultPlugins, +/// PhysicsPlugins::default(), +/// TnuaControllerPlugin::new(PhysicsSchedule), +/// TnuaAvian2dPlugin::new(PhysicsSchedule), +/// )); +/// ``` pub struct TnuaAvian2dPlugin { schedule: InternedScheduleLabel, } @@ -39,8 +53,7 @@ impl Plugin for TnuaAvian2dPlugin { app.configure_sets( self.schedule, TnuaSystemSet - .before(PhysicsSet::Prepare) - .before(PhysicsStepSet::First) + .in_set(PhysicsStepSet::First) .run_if(|physics_time: Res>| !physics_time.is_paused()), ); app.add_systems( @@ -65,24 +78,25 @@ pub struct TnuaAvian2dSensorShape(pub Collider); fn update_rigid_body_trackers_system( gravity: Res, mut query: Query<( - &GlobalTransform, + &Position, + &Rotation, &LinearVelocity, &AngularVelocity, &mut TnuaRigidBodyTracker, Option<&TnuaToggle>, )>, ) { - for (transform, linaer_velocity, angular_velocity, mut tracker, tnua_toggle) in query.iter_mut() + for (position, rotation, linaer_velocity, angular_velocity, mut tracker, tnua_toggle) in + query.iter_mut() { match tnua_toggle.copied().unwrap_or_default() { TnuaToggle::Disabled => continue, TnuaToggle::SenseOnly => {} TnuaToggle::Enabled => {} } - let (_, rotation, translation) = transform.to_scale_rotation_translation(); *tracker = TnuaRigidBodyTracker { - translation: translation.adjust_precision(), - rotation: rotation.adjust_precision(), + translation: position.adjust_precision().extend(0.0), + rotation: Quaternion::from(*rotation).adjust_precision(), velocity: linaer_velocity.0.extend(0.0), angvel: Vector3::new(0.0, 0.0, angular_velocity.0), gravity: gravity.0.extend(0.0), @@ -96,7 +110,9 @@ fn update_proximity_sensors_system( collisions: Res, mut query: Query<( Entity, - &GlobalTransform, + &Position, + &Rotation, + &Collider, &mut TnuaProximitySensor, Option<&TnuaAvian2dSensorShape>, Option<&mut TnuaGhostSensor>, @@ -105,7 +121,7 @@ fn update_proximity_sensors_system( )>, collision_layers_entity: Query<&CollisionLayers>, other_object_query: Query<( - Option<(&GlobalTransform, &LinearVelocity, &AngularVelocity)>, + Option<(&Position, &LinearVelocity, &AngularVelocity)>, Option<&CollisionLayers>, Has, Has, @@ -114,7 +130,9 @@ fn update_proximity_sensors_system( query.par_iter_mut().for_each( |( owner_entity, - transform, + position, + rotation, + collider, mut sensor, shape, mut ghost_sensor, @@ -126,6 +144,11 @@ fn update_proximity_sensors_system( TnuaToggle::SenseOnly => {} TnuaToggle::Enabled => {} } + let transform = Transform { + translation: position.adjust_precision().extend(0.0), + rotation: Quaternion::from(*rotation).adjust_precision(), + scale: collider.scale().adjust_precision().extend(1.0), + }; let cast_origin = transform.transform_point(sensor.cast_origin.f32()); let cast_direction = sensor.cast_direction; let cast_direction_2d = Dir2::new(cast_direction.truncate()) @@ -195,14 +218,14 @@ fn update_proximity_sensors_system( let entity_linvel; let entity_angvel; - if let Some((entity_transform, entity_linear_velocity, entity_angular_velocity)) = + if let Some((entity_position, entity_linear_velocity, entity_angular_velocity)) = entity_kinematic_data { entity_angvel = Vector3::new(0.0, 0.0, entity_angular_velocity.0); entity_linvel = entity_linear_velocity.0.extend(0.0) + if 0.0 < entity_angvel.length_squared() { - let relative_point = intersection_point - - entity_transform.translation().truncate().adjust_precision(); + let relative_point = + intersection_point - entity_position.adjust_precision(); // NOTE: no need to project relative_point on the // rotation plane, it will not affect the cross // product. diff --git a/avian3d/src/lib.rs b/avian3d/src/lib.rs index 3989a6d2c..43a4d6737 100644 --- a/avian3d/src/lib.rs +++ b/avian3d/src/lib.rs @@ -30,6 +30,20 @@ use bevy_tnua_physics_integration_layer::TnuaSystemSet; /// Add this plugin to use avian3d as a physics backend. /// /// This plugin should be used in addition to `TnuaControllerPlugin`. +/// Note that you should make sure both of these plugins use the same schedule. +/// This should usually be `PhysicsSchedule`, which by default is `FixedUpdate`. +/// +/// # Example +/// +/// ```ignore +/// App::new() +/// .add_plugins(( +/// DefaultPlugins, +/// PhysicsPlugins::default(), +/// TnuaControllerPlugin::new(PhysicsSchedule), +/// TnuaAvian3dPlugin::new(PhysicsSchedule), +/// )); +/// ``` pub struct TnuaAvian3dPlugin { schedule: InternedScheduleLabel, } @@ -47,8 +61,7 @@ impl Plugin for TnuaAvian3dPlugin { app.configure_sets( self.schedule, TnuaSystemSet - .before(PhysicsSet::Prepare) - .before(PhysicsStepSet::First) + .in_set(PhysicsStepSet::First) .run_if(|physics_time: Res>| !physics_time.is_paused()), ); app.add_systems( @@ -73,23 +86,24 @@ pub struct TnuaAvian3dSensorShape(pub Collider); fn update_rigid_body_trackers_system( gravity: Res, mut query: Query<( - &GlobalTransform, + &Position, + &Rotation, &LinearVelocity, &AngularVelocity, &mut TnuaRigidBodyTracker, Option<&TnuaToggle>, )>, ) { - for (transform, linaer_velocity, angular_velocity, mut tracker, tnua_toggle) in query.iter_mut() + for (position, rotation, linaer_velocity, angular_velocity, mut tracker, tnua_toggle) in + query.iter_mut() { match tnua_toggle.copied().unwrap_or_default() { TnuaToggle::Disabled => continue, TnuaToggle::SenseOnly => {} TnuaToggle::Enabled => {} } - let (_, rotation, translation) = transform.to_scale_rotation_translation(); *tracker = TnuaRigidBodyTracker { - translation: translation.adjust_precision(), + translation: position.adjust_precision(), rotation: rotation.adjust_precision(), velocity: linaer_velocity.0.adjust_precision(), angvel: angular_velocity.0.adjust_precision(), @@ -104,7 +118,9 @@ fn update_proximity_sensors_system( collisions: Res, mut query: Query<( Entity, - &GlobalTransform, + &Position, + &Rotation, + &Collider, &mut TnuaProximitySensor, Option<&TnuaAvian3dSensorShape>, Option<&mut TnuaGhostSensor>, @@ -113,7 +129,7 @@ fn update_proximity_sensors_system( )>, collision_layers_entity: Query<&CollisionLayers>, other_object_query: Query<( - Option<(&GlobalTransform, &LinearVelocity, &AngularVelocity)>, + Option<(&Position, &LinearVelocity, &AngularVelocity)>, Option<&CollisionLayers>, Has, Has, @@ -122,7 +138,9 @@ fn update_proximity_sensors_system( query.par_iter_mut().for_each( |( owner_entity, - transform, + position, + rotation, + collider, mut sensor, shape, mut ghost_sensor, @@ -134,6 +152,11 @@ fn update_proximity_sensors_system( TnuaToggle::SenseOnly => {} TnuaToggle::Enabled => {} } + let transform = Transform { + translation: position.0, + rotation: rotation.0, + scale: collider.scale(), + }; // TODO: is there any point in doing these transformations as f64 when that feature // flag is active? @@ -203,14 +226,14 @@ fn update_proximity_sensors_system( let entity_linvel; let entity_angvel; - if let Some((entity_transform, entity_linear_velocity, entity_angular_velocity)) = + if let Some((entity_position, entity_linear_velocity, entity_angular_velocity)) = entity_kinematic_data { entity_angvel = entity_angular_velocity.0.adjust_precision(); entity_linvel = entity_linear_velocity.0.adjust_precision() + if 0.0 < entity_angvel.length_squared() { - let relative_point = intersection_point - - entity_transform.translation().adjust_precision(); + let relative_point = + intersection_point - entity_position.adjust_precision(); // NOTE: no need to project relative_point on the // rotation plane, it will not affect the cross // product. @@ -252,10 +275,9 @@ fn update_proximity_sensors_system( let query_filter = SpatialQueryFilter::from_excluded_entities([owner_entity]); if let Some(TnuaAvian3dSensorShape(shape)) = shape { - let (_, owner_rotation, _) = transform.to_scale_rotation_translation(); let owner_rotation = Quat::from_axis_angle( *cast_direction, - owner_rotation.to_scaled_axis().dot(*cast_direction), + rotation.to_scaled_axis().dot(*cast_direction), ); spatial_query_pipeline.shape_hits_callback( shape, diff --git a/demos/src/app_setup_options.rs b/demos/src/app_setup_options.rs index 150c03cba..e2cc5f57c 100644 --- a/demos/src/app_setup_options.rs +++ b/demos/src/app_setup_options.rs @@ -33,7 +33,18 @@ impl AppSetupConfiguration { } else if cfg!(feature = "avian") { ScheduleToUse::FixedUpdate } else { - ScheduleToUse::Update + #[cfg(feature = "avian")] + { + ScheduleToUse::PhysicsSchedule + } + #[cfg(feature = "rapier")] + { + ScheduleToUse::Update + } + #[cfg(all(not(feature = "avian"), not(feature = "rapier")))] + { + panic!("No schedule was specified, but also no physics engine is avaible. Therefore, there is no fallback.") + } }, level_to_load: url_params.get("level"), } diff --git a/demos/src/bin/platformer_2d.rs b/demos/src/bin/platformer_2d.rs index 22a1db0b4..6f7085486 100644 --- a/demos/src/bin/platformer_2d.rs +++ b/demos/src/bin/platformer_2d.rs @@ -1,5 +1,5 @@ #[cfg(feature = "avian2d")] -use avian2d::{prelude as avian, prelude::*, schedule::PhysicsSchedule}; +use avian2d::{prelude as avian, prelude::*}; use bevy::ecs::schedule::ScheduleLabel; use bevy::prelude::*; #[cfg(feature = "rapier2d")] @@ -22,6 +22,7 @@ use tnua_demos_crate::app_setup_options::{AppSetupConfiguration, ScheduleToUse}; use tnua_demos_crate::character_control_systems::info_dumpeing_systems::character_control_info_dumping_system; use tnua_demos_crate::character_control_systems::platformer_control_systems::{ apply_platformer_controls, CharacterMotionConfigForPlatformerDemo, FallingThroughControlScheme, + JustPressedCachePlugin, }; use tnua_demos_crate::character_control_systems::Dimensionality; use tnua_demos_crate::level_mechanics::LevelMechanicsPlugin; @@ -101,7 +102,7 @@ fn main() { app.add_plugins(TnuaControllerPlugin::new(FixedUpdate)); app.add_plugins(TnuaCrouchEnforcerPlugin::new(FixedUpdate)); } - #[cfg(any(feature = "avian", feature = "avian"))] + #[cfg(feature = "avian")] ScheduleToUse::PhysicsSchedule => { app.add_plugins(TnuaControllerPlugin::new(PhysicsSchedule)); app.add_plugins(TnuaCrouchEnforcerPlugin::new(PhysicsSchedule)); @@ -129,11 +130,14 @@ fn main() { ScheduleToUse::Update => Update.intern(), ScheduleToUse::FixedUpdate => FixedUpdate.intern(), #[cfg(feature = "avian")] - ScheduleToUse::PhysicsSchedule => PhysicsSchedule.intern(), + // `PhysicsSchedule` is `FixedPostUpdate` by default, which allows us + // to run user code like the platformer controls in `FixedUpdate`, + // which is a bit more idiomatic. + ScheduleToUse::PhysicsSchedule => FixedUpdate.intern(), }, apply_platformer_controls.in_set(TnuaUserControlsSystemSet), ); - app.add_plugins(LevelMechanicsPlugin); + app.add_plugins((LevelMechanicsPlugin, JustPressedCachePlugin)); #[cfg(feature = "rapier2d")] { app.add_systems(Startup, |mut cfg: Single<&mut RapierConfiguration>| { diff --git a/demos/src/bin/platformer_3d.rs b/demos/src/bin/platformer_3d.rs index 822b913ed..b14896f4c 100644 --- a/demos/src/bin/platformer_3d.rs +++ b/demos/src/bin/platformer_3d.rs @@ -1,5 +1,5 @@ #[cfg(feature = "avian3d")] -use avian3d::{prelude as avian, prelude::*, schedule::PhysicsSchedule}; +use avian3d::{prelude as avian, prelude::*}; use bevy::ecs::schedule::ScheduleLabel; use bevy::prelude::*; #[cfg(feature = "rapier3d")] @@ -19,9 +19,6 @@ use bevy_tnua_avian3d::*; use bevy_tnua_rapier3d::*; use tnua_demos_crate::app_setup_options::{AppSetupConfiguration, ScheduleToUse}; -use tnua_demos_crate::character_animating_systems::platformer_animating_systems::{ - animate_platformer_character, AnimationState, -}; #[cfg(feature = "egui")] use tnua_demos_crate::character_control_systems::info_dumpeing_systems::character_control_info_dumping_system; use tnua_demos_crate::character_control_systems::platformer_control_systems::{ @@ -41,6 +38,12 @@ use tnua_demos_crate::ui::plotting::PlotSource; #[cfg(feature = "egui")] use tnua_demos_crate::ui::DemoInfoUpdateSystemSet; use tnua_demos_crate::util::animating::{animation_patcher_system, GltfSceneHandler}; +use tnua_demos_crate::{ + character_animating_systems::platformer_animating_systems::{ + animate_platformer_character, AnimationState, + }, + character_control_systems::platformer_control_systems::JustPressedCachePlugin, +}; fn main() { tnua_demos_crate::verify_physics_backends_features!("rapier3d", "avian3d"); @@ -137,13 +140,16 @@ fn main() { ScheduleToUse::Update => Update.intern(), ScheduleToUse::FixedUpdate => FixedUpdate.intern(), #[cfg(feature = "avian")] - ScheduleToUse::PhysicsSchedule => PhysicsSchedule.intern(), + // `PhysicsSchedule` is `FixedPostUpdate` by default, which allows us + // to run user code like the platformer controls in `FixedUpdate`, + // which is a bit more idiomatic. + ScheduleToUse::PhysicsSchedule => FixedUpdate.intern(), }, apply_platformer_controls.in_set(TnuaUserControlsSystemSet), ); app.add_systems(Update, animation_patcher_system); app.add_systems(Update, animate_platformer_character); - app.add_plugins(LevelMechanicsPlugin); + app.add_plugins((LevelMechanicsPlugin, JustPressedCachePlugin)); app.run(); } diff --git a/demos/src/bin/shooter_like.rs b/demos/src/bin/shooter_like.rs index 7a8e96fa2..fed27bdc9 100644 --- a/demos/src/bin/shooter_like.rs +++ b/demos/src/bin/shooter_like.rs @@ -1,5 +1,5 @@ #[cfg(feature = "avian3d")] -use avian3d::{prelude as avian, prelude::*, schedule::PhysicsSchedule}; +use avian3d::{prelude as avian, prelude::*}; use bevy::ecs::schedule::ScheduleLabel; use bevy::input::mouse::MouseMotion; use bevy::prelude::*; @@ -23,12 +23,15 @@ use tnua_demos_crate::app_setup_options::{AppSetupConfiguration, ScheduleToUse}; use tnua_demos_crate::character_animating_systems::platformer_animating_systems::{ animate_platformer_character, AnimationState, }; -use tnua_demos_crate::character_control_systems::info_dumpeing_systems::character_control_info_dumping_system; use tnua_demos_crate::character_control_systems::platformer_control_systems::{ apply_platformer_controls, CharacterMotionConfigForPlatformerDemo, FallingThroughControlScheme, ForwardFromCamera, }; use tnua_demos_crate::character_control_systems::Dimensionality; +use tnua_demos_crate::character_control_systems::{ + info_dumpeing_systems::character_control_info_dumping_system, + platformer_control_systems::JustPressedCachePlugin, +}; use tnua_demos_crate::level_mechanics::LevelMechanicsPlugin; #[cfg(feature = "avian3d")] use tnua_demos_crate::levels_setup::for_3d_platformer::LayerNames; @@ -131,8 +134,6 @@ fn main() { let system = apply_camera_controls; #[cfg(feature = "rapier")] let system = system.after(bevy_rapier3d::prelude::PhysicsSet::SyncBackend); - #[cfg(feature = "avian")] - let system = system.after(avian3d::prelude::PhysicsSet::Sync); system.before(bevy::transform::TransformSystem::TransformPropagate) }); app.add_systems( @@ -140,13 +141,16 @@ fn main() { ScheduleToUse::Update => Update.intern(), ScheduleToUse::FixedUpdate => FixedUpdate.intern(), #[cfg(feature = "avian")] - ScheduleToUse::PhysicsSchedule => PhysicsSchedule.intern(), + // `PhysicsSchedule` is `FixedPostUpdate` by default, which allows us + // to run user code like the platformer controls in `FixedUpdate`, + // which is a bit more idiomatic. + ScheduleToUse::PhysicsSchedule => FixedUpdate.intern(), }, apply_platformer_controls.in_set(TnuaUserControlsSystemSet), ); app.add_systems(Update, animation_patcher_system); app.add_systems(Update, animate_platformer_character); - app.add_plugins(LevelMechanicsPlugin); + app.add_plugins((LevelMechanicsPlugin, JustPressedCachePlugin)); app.run(); } diff --git a/demos/src/character_control_systems/platformer_control_systems.rs b/demos/src/character_control_systems/platformer_control_systems.rs index 9cd3ff6a6..b11682eaf 100644 --- a/demos/src/character_control_systems/platformer_control_systems.rs +++ b/demos/src/character_control_systems/platformer_control_systems.rs @@ -1,4 +1,7 @@ -use bevy::prelude::*; +use bevy::{ + app::{FixedMain, RunFixedMainLoop}, + prelude::*, +}; #[cfg(feature = "egui")] use bevy_egui::{egui, EguiContexts}; use bevy_tnua::builtins::{ @@ -20,6 +23,7 @@ use super::Dimensionality; pub fn apply_platformer_controls( #[cfg(feature = "egui")] mut egui_context: EguiContexts, keyboard: Res>, + mut just_pressed: ResMut, mut query: Query<( &CharacterMotionConfigForPlatformerDemo, // This is the main component used for interacting with Tnua. It is used for both issuing @@ -113,25 +117,13 @@ pub fn apply_platformer_controls( let turn_in_place = forward_from_camera.is_none() && keyboard.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]); - let crouch_pressed: bool; - let crouch_just_pressed: bool; - match config.dimensionality { - Dimensionality::Dim2 => { - let crouch_buttons = [ - KeyCode::ControlLeft, - KeyCode::ControlRight, - KeyCode::ArrowDown, - KeyCode::KeyS, - ]; - crouch_pressed = keyboard.any_pressed(crouch_buttons); - crouch_just_pressed = keyboard.any_just_pressed(crouch_buttons); - } - Dimensionality::Dim3 => { - let crouch_buttons = [KeyCode::ControlLeft, KeyCode::ControlRight]; - crouch_pressed = keyboard.any_pressed(crouch_buttons); - crouch_just_pressed = keyboard.any_just_pressed(crouch_buttons); - } - } + let crouch_buttons = match config.dimensionality { + Dimensionality::Dim2 => CROUCH_BUTTONS_2D.iter().copied(), + Dimensionality::Dim3 => CROUCH_BUTTONS_3D.iter().copied(), + }; + let crouch_pressed = keyboard.any_pressed(crouch_buttons); + let crouch_just_pressed = just_pressed.crouch; + just_pressed.was_read = true; // This needs to be called once per frame. It lets the air actions counter know about the // air status of the character. Specifically: @@ -463,3 +455,58 @@ impl Default for ForwardFromCamera { } } } + +/// Since the fixed timestep schedule does not cache just pressed states that happened +/// in a frame with no fixed updates, we need to cache them ourselves in order to not miss them. +/// Note that if you use a smarter input manager like LWIM, this is handled for you. +/// If the demo is running with a variable timestep, this will just report the current frame's +/// state as expected. +pub struct JustPressedCachePlugin; + +impl Plugin for JustPressedCachePlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + app.add_systems( + RunFixedMainLoop, + ( + collect_just_pressed_cache.before(FixedMain::run_fixed_main), + clear_just_pressed_cache.after(FixedMain::run_fixed_main), + ), + ); + } +} + +fn collect_just_pressed_cache( + query: Query<&CharacterMotionConfigForPlatformerDemo>, + keyboard: Res>, + mut just_pressed: ResMut, +) { + for config in &query { + let crouch_buttons = match config.dimensionality { + Dimensionality::Dim2 => CROUCH_BUTTONS_2D.iter().copied(), + Dimensionality::Dim3 => CROUCH_BUTTONS_3D.iter().copied(), + }; + just_pressed.crouch = keyboard.any_just_pressed(crouch_buttons); + } +} + +fn clear_just_pressed_cache(mut just_pressed: ResMut) { + if just_pressed.was_read { + *just_pressed = default() + } +} + +#[derive(Resource, Default)] +pub struct JustPressedCache { + crouch: bool, + was_read: bool, +} + +const CROUCH_BUTTONS_2D: &[KeyCode] = &[ + KeyCode::ControlLeft, + KeyCode::ControlRight, + KeyCode::ArrowDown, + KeyCode::KeyS, +]; + +const CROUCH_BUTTONS_3D: &[KeyCode] = &[KeyCode::ControlLeft, KeyCode::ControlRight]; diff --git a/examples/example.rs b/examples/example.rs index fdda1cf2d..530776652 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -11,7 +11,7 @@ fn main() { DefaultPlugins, PhysicsPlugins::default(), // We need both Tnua's main controller plugin, and the plugin to connect to the physics - // backend (in this case XBPD-3D) + // backend (in this case Avian 3D) TnuaControllerPlugin::new(FixedUpdate), TnuaAvian3dPlugin::new(FixedUpdate), )) diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..2d5315df1 --- /dev/null +++ b/shell.nix @@ -0,0 +1,15 @@ +{ pkgs ? import { } }: + +with pkgs; + +mkShell rec { + nativeBuildInputs = [ + pkg-config + ]; + buildInputs = [ + udev alsa-lib vulkan-loader cmake rustfmt jdk21 openssl + xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr # To use the x11 feature + libxkbcommon wayland # To use the wayland feature + ]; + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; +}