diff --git a/Cargo.lock b/Cargo.lock index 5798ed3..95b41d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,37 @@ dependencies = [ "syn", ] +[[package]] +name = "bevy-tnua" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a5783db12b6f927e7cc9976c15e23fd5b107319a9c1be8b92622fa10b94a62" +dependencies = [ + "bevy", + "bevy-tnua-physics-integration-layer", + "thiserror 1.0.69", +] + +[[package]] +name = "bevy-tnua-avian3d" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3348f84267ced2c4535bd3415b384df84d49b9d04824442aef7f3b6b3af37c" +dependencies = [ + "avian3d", + "bevy", + "bevy-tnua-physics-integration-layer", +] + +[[package]] +name = "bevy-tnua-physics-integration-layer" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbe99d25b7253cb99a00f69fe9accc875645b4ecf9910bb5f24459835ff177e" +dependencies = [ + "bevy", +] + [[package]] name = "bevy_a11y" version = "0.15.3" @@ -643,6 +674,17 @@ dependencies = [ "sysinfo", ] +[[package]] +name = "bevy_dolly" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68783929254625d3ffa54a6fed7ff7216abf90296eea5e3885cde419d5daead0" +dependencies = [ + "bevy", + "bevy_math", + "bevy_transform", +] + [[package]] name = "bevy_ecs" version = "0.15.3" @@ -723,15 +765,6 @@ dependencies = [ "encase_derive_impl", ] -[[package]] -name = "bevy_flycam" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83039a8e4f7e92c96c97b0b3ac738a54781f77ce5086d3ae39698219586a7f1d" -dependencies = [ - "bevy", -] - [[package]] name = "bevy_gilrs" version = "0.15.3" @@ -2744,7 +2777,9 @@ dependencies = [ "avian3d", "bevy", "bevy-inspector-egui", - "bevy_flycam", + "bevy-tnua", + "bevy-tnua-avian3d", + "bevy_dolly", "bevy_trenchbroom", "nil", ] diff --git a/Cargo.toml b/Cargo.toml index d39f0b0..9c69280 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,11 @@ opt-level = 3 avian3d = "0.2" bevy = "0.15.3" bevy_trenchbroom = { version = "0.6.2", features = ["auto_register", "avian"] } -bevy_flycam = "0.15.0" nil = "0.14.0" bevy-inspector-egui = "0.29.1" +bevy-tnua = "0.21.0" +bevy-tnua-avian3d = "0.2.0" +bevy_dolly = { version = "0.0.5", default-features = false } [patch.crates-io] bevy_trenchbroom = { git = "https://github.com/Noxmore/bevy_trenchbroom.git", rev = "3b79f1b" } diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..dcd69b3 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,38 @@ +use bevy::prelude::*; +use bevy_dolly::prelude::*; + +impl GameCameraRig { + pub fn new_with_arm(arm: Vec3) -> Self { + Self( + CameraRig::builder() + .with(Position::new(Vec3::ZERO)) + .with(Rotation::new(Quat::IDENTITY)) + .with(Smooth::new_position(1.25).predictive(true)) + .with(Smooth::new_rotation(10.)) + .with(Arm::new(arm)) + .with( + LookAt::new(Vec3::ZERO + Vec3::Y) + .tracking_smoothness(1.25) + .tracking_predictive(true), + ) + .build(), + ) + } + + pub fn set_position_target(&mut self, target_position: Vec3, target_rotation: Quat) { + self.driver_mut::().position = target_position; + self.driver_mut::().rotation = target_rotation; + self.driver_mut::().target = target_position + Vec3::Y; + } +} + +/// A custom camera rig which combines smoothed movement with a look-at driver. +#[derive(Component, Debug, Deref, DerefMut)] +pub struct GameCameraRig(CameraRig); + +// Turn the nested rig into a driver, so it can be used in another rig. +impl RigDriver for GameCameraRig { + fn update(&mut self, params: RigUpdateParams) -> Transform { + self.0.update(params.delta_time_seconds) + } +} diff --git a/src/main.rs b/src/main.rs index f9b9a9a..b6b974d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ mod alien; +mod camera; mod cash; +mod player; mod tb_entities; use avian3d::PhysicsPlugins; @@ -7,8 +9,11 @@ use avian3d::prelude::*; use bevy::core_pipeline::tonemapping::Tonemapping; use bevy::prelude::*; use bevy::render::view::ColorGrading; -use bevy_flycam::prelude::*; +use bevy_dolly::prelude::*; +use bevy_tnua::prelude::TnuaControllerPlugin; +use bevy_tnua_avian3d::TnuaAvian3dPlugin; use bevy_trenchbroom::prelude::*; +use camera::GameCameraRig; #[derive(Resource, Reflect, Debug)] #[reflect(Resource)] @@ -19,6 +24,9 @@ struct DebugVisuals { pub shadows: bool, } +#[derive(Component, Debug)] +pub struct MainCamera; + fn main() { let mut app = App::new(); @@ -35,18 +43,18 @@ fn main() { })); app.add_plugins(PhysicsPlugins::default()); - // app.add_plugins(PhysicsDebugPlugin::default()); + app.add_plugins(PhysicsDebugPlugin::default()); + app.add_plugins(( + TnuaControllerPlugin::new(FixedUpdate), + TnuaAvian3dPlugin::new(FixedUpdate), + )); - // bevy_flycam setup so we can get a closer look at the scene, mainly for debugging - app.add_plugins(PlayerPlugin); + app.add_systems(Update, Dolly::::update_active); app.add_plugins(alien::plugin); app.add_plugins(cash::plugin); + app.add_plugins(player::plugin); - app.insert_resource(MovementSettings { - sensitivity: 0.00005, - speed: 12., - }); app.insert_resource(AmbientLight { color: Color::WHITE, brightness: 400., @@ -55,7 +63,7 @@ fn main() { app.add_plugins(TrenchBroomPlugin(TrenchBroomConfig::new("hedz"))); - app.add_systems(Startup, (write_trenchbroom_config, music)); + app.add_systems(Startup, (setup_cam, write_trenchbroom_config, music)); app.add_systems(PostStartup, setup_scene); app.add_systems( Update, @@ -65,6 +73,16 @@ fn main() { app.run(); } +fn setup_cam(mut commands: Commands) { + commands.spawn(( + Camera3d::default(), + MainCamera, + Rig::builder() + .with(GameCameraRig::new_with_arm(Vec3::new(0., 5., 14.))) + .build(), + )); +} + fn setup_scene(mut commands: Commands, asset_server: Res) { commands.spawn(SceneRoot(asset_server.load("maps/map1.map#Scene"))); @@ -80,14 +98,6 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { ..default() }, )); - - commands.spawn(( - SceneRoot( - asset_server - .load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry_demonstrator.glb")), - ), - Transform::from_xyz(0.0, 5.0, 0.0), - )); } fn spawn_box( @@ -110,9 +120,6 @@ fn spawn_box( fn music(asset_server: Res, mut commands: Commands) { commands.spawn(AudioPlayer::new(asset_server.load("sfx/music/02.ogg"))); - commands.spawn(AudioPlayer::new( - asset_server.load("sfx/heads/angry_demonstrator.ogg"), - )); } fn write_trenchbroom_config(server: Res) { diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..dce7909 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,189 @@ +use crate::{camera::GameCameraRig, cash::Cash, tb_entities::SpawnPoint}; +use avian3d::prelude::*; +use bevy::{ + input::mouse::MouseMotion, + prelude::*, + window::{CursorGrabMode, PrimaryWindow}, +}; +use bevy_dolly::prelude::Rig; +use bevy_tnua::{TnuaUserControlsSystemSet, prelude::*}; +use bevy_tnua_avian3d::TnuaAvian3dSensorShape; + +#[derive(Component, Default)] +pub struct Player; + +#[derive(Resource, Default)] +struct PlayerSpawned { + spawned: bool, +} + +pub fn plugin(app: &mut App) { + app.init_resource::(); + app.add_systems(Startup, initial_grab_cursor); + app.add_systems(Update, (spawn, update_camera, cursor_events, collect_cash)); + app.add_systems( + FixedUpdate, + apply_controls.in_set(TnuaUserControlsSystemSet), + ); +} + +fn spawn( + mut commands: Commands, + asset_server: Res, + query: Query<&Transform, With>, + mut player_spawned: ResMut, +) { + if player_spawned.spawned { + return; + } + + let Some(spawn) = query.iter().next() else { + return; + }; + + let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.)); + + let mesh = asset_server + .load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry demonstrator.glb")); + + commands + .spawn(( + Name::from("player"), + Player, + transform, + Visibility::default(), + RigidBody::Dynamic, + Collider::capsule(1.2, 1.5), + LockedAxes::ROTATION_LOCKED, + TnuaController::default(), + TnuaAvian3dSensorShape(Collider::cylinder(1.0, 0.0)), + )) + .with_child(( + Transform::from_translation(Vec3::new(0., 1.0, 0.)) + .with_rotation(Quat::from_rotation_y(std::f32::consts::PI)), + SceneRoot(mesh), + )); + + commands.spawn(AudioPlayer::new( + asset_server.load("sfx/heads/angry demonstrator.ogg"), + )); + + player_spawned.spawned = true; +} + +fn cursor_events( + mut mouse: EventReader, + mut player: Query<&mut Transform, With>, +) { + let Ok(mut player) = player.get_single_mut() else { + return; + }; + + for ev in mouse.read() { + player.rotate_y(ev.delta.x * -0.001); + } +} + +fn toggle_grab_cursor(window: &mut Window) { + match window.cursor_options.grab_mode { + CursorGrabMode::None => { + window.cursor_options.grab_mode = CursorGrabMode::Confined; + window.cursor_options.visible = false; + } + _ => { + window.cursor_options.grab_mode = CursorGrabMode::None; + window.cursor_options.visible = true; + } + } +} + +fn initial_grab_cursor(mut primary_window: Query<&mut Window, With>) { + if let Ok(mut window) = primary_window.get_single_mut() { + toggle_grab_cursor(&mut window); + } else { + warn!("Primary window not found for `initial_grab_cursor`!"); + } +} + +fn apply_controls( + keyboard: Res>, + mut query: Query<&mut TnuaController>, + player: Query<&Transform, With>, +) { + let Ok(mut controller) = query.get_single_mut() else { + return; + }; + + let Ok(player) = player.get_single() else { + return; + }; + + let mut direction = Vec3::ZERO; + + if keyboard.pressed(KeyCode::KeyW) { + direction = player.forward().as_vec3(); + } + if keyboard.pressed(KeyCode::KeyS) { + direction = player.forward().as_vec3() * -1.; + } + if keyboard.pressed(KeyCode::KeyA) { + direction += player.left().as_vec3(); + } + if keyboard.pressed(KeyCode::KeyD) { + direction += player.right().as_vec3(); + } + + controller.basis(TnuaBuiltinWalk { + // The `desired_velocity` determines how the character will move. + desired_velocity: direction.normalize_or_zero() * 10.0, + // The `float_height` must be greater (even if by little) from the distance between the + // character's center and the lowest point of its collider. + float_height: 3.0, + // `TnuaBuiltinWalk` has many other fields for customizing the movement - but they have + // sensible defaults. Refer to the `TnuaBuiltinWalk`'s documentation to learn what they do. + ..Default::default() + }); + + // Feed the jump action every frame as long as the player holds the jump button. If the player + // stops holding the jump button, simply stop feeding the action. + if keyboard.pressed(KeyCode::Space) { + controller.action(TnuaBuiltinJump { + // The height is the only mandatory field of the jump button. + height: 4.0, + // `TnuaBuiltinJump` also has customization fields with sensible defaults. + ..Default::default() + }); + } +} + +fn update_camera(player: Query<&Transform, With>, mut rig: Single<&mut Rig>) { + let Some(player) = player.iter().next() else { + return; + }; + + rig.driver_mut::() + .set_position_target(player.translation, player.rotation); +} + +fn collect_cash( + mut commands: Commands, + mut collision_event_reader: EventReader, + query_player: Query<&Player>, + query_cash: Query<&Cash>, + asset_server: Res, +) { + for CollisionStarted(e1, e2) in collision_event_reader.read() { + let collect = if query_player.contains(*e1) && query_cash.contains(*e2) { + Some(*e2) + } else if query_player.contains(*e2) && query_cash.contains(*e1) { + Some(*e1) + } else { + None + }; + + if let Some(cash) = collect { + commands.spawn(AudioPlayer::new(asset_server.load("sfx/effects/cash.ogg"))); + commands.entity(cash).despawn(); + } + } +} diff --git a/src/tb_entities.rs b/src/tb_entities.rs index 837de3f..667076a 100644 --- a/src/tb_entities.rs +++ b/src/tb_entities.rs @@ -89,9 +89,12 @@ impl CashSpawn { let mesh = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/cash.glb")); - world - .commands() - .entity(entity) - .insert((Name::new("cash"), SceneRoot(mesh), Cash)); + world.commands().entity(entity).insert(( + Name::new("cash"), + SceneRoot(mesh), + Cash, + Collider::cuboid(2., 3.0, 2.), + Sensor, + )); } }