use bevy::prelude::*; use bevy::input::mouse::MouseMotion; use bevy::window::PrimaryWindow; use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings}; use bevy::core_pipeline::tonemapping::Tonemapping; use bevy::pbr::CascadeShadowConfigBuilder; use bevy::transform::TransformSystem; use bevy::math::{DVec3, DQuat}; use bevy_xpbd_3d::prelude::*; use std::f32::consts::PI; use crate::{actor, audio, hud, var}; pub struct CameraPlugin; impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup_camera); app.add_systems(Update, handle_input); app.add_systems(Update, manage_player_actor.after(handle_input)); app.add_systems(PostUpdate, sync_camera_to_player .after(PhysicsSet::Sync) .after(apply_input_to_player) .before(TransformSystem::TransformPropagate)); app.add_systems(Update, update_map_camera); app.add_systems(Update, update_fov); app.add_systems(PostUpdate, apply_input_to_player .after(PhysicsSet::Sync) .before(TransformSystem::TransformPropagate)); app.insert_resource(MapCam::default()); } } #[derive(Resource)] pub struct MapCam { zoom_level: f32, target_zoom_level: f32, pitch: f32, yaw: f32, } impl Default for MapCam { fn default() -> Self { Self { zoom_level: 10.0, target_zoom_level: 10000.0, pitch: PI * 0.3, yaw: 0.0, } } } pub fn setup_camera( mut commands: Commands, ) { // Add player commands.spawn(( Camera3dBundle { camera: Camera { hdr: true, // HDR is required for bloom clear_color: ClearColorConfig::Custom(Color::BLACK), ..default() }, tonemapping: Tonemapping::TonyMcMapface, transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), ..default() }, BloomSettings { composite_mode: BloomCompositeMode::EnergyConserving, ..default() }, )); // Add Light from the Sun commands.spawn(DirectionalLightBundle { directional_light: DirectionalLight { illuminance: 1000.0, shadows_enabled: false, ..default() }, transform: Transform::from_rotation(Quat::from_rotation_y(PI/2.0)), cascade_shadow_config: CascadeShadowConfigBuilder { first_cascade_far_bound: 7.0, maximum_distance: 25.0, ..default() } .into(), ..default() }); } pub fn sync_camera_to_player( settings: Res, mut q_camera: Query<&mut Transform, (With, Without)>, q_playercam: Query<(&actor::Actor, &Transform), (With, Without)>, ) { if settings.map_active || q_camera.is_empty() || q_playercam.is_empty() { return; } let mut camera_transform = q_camera.get_single_mut().unwrap(); let (actor, player_transform) = q_playercam.get_single().unwrap(); // Rotation camera_transform.rotation = player_transform.rotation * Quat::from_array([0.0, -1.0, 0.0, 0.0]); // Translation if settings.third_person { camera_transform.translation = player_transform.translation + camera_transform.rotation * (actor.camdistance * Vec3::new(0.0, 0.2, 1.0)); } else { camera_transform.translation = player_transform.translation; } } pub fn update_map_camera( settings: Res, mut mapcam: ResMut, mut q_camera: Query<&mut Transform, (With, Without)>, q_playercam: Query<&Transform, (With, Without)>, mut mouse_events: EventReader, keyboard_input: Res>, ) { if !settings.map_active || q_camera.is_empty() || q_playercam.is_empty() { return; } let mut camera_transform = q_camera.get_single_mut().unwrap(); let player_transform = q_playercam.get_single().unwrap(); // Get mouse movement let mut mouse_delta = Vec2::ZERO; for mouse_event in mouse_events.read() { mouse_delta += mouse_event.delta; } // NOTE: we need to subtract a bit from PI/2, otherwise the "up" // direction parameter for the Transform.look_at function is ambiguous // at the extreme values and the orientation will flicker back/forth. let epsilon = 0.001; mapcam.pitch = (mapcam.pitch + mouse_delta.y / 180.0 * settings.mouse_sensitivity).clamp(-PI / 2.0 + epsilon, PI / 2.0 - epsilon); mapcam.yaw += mouse_delta.x / 180.0 * settings.mouse_sensitivity; // Update zoom level if keyboard_input.pressed(settings.key_map_zoom_out) { mapcam.target_zoom_level = (mapcam.target_zoom_level * 1.1).min(17e18); } if keyboard_input.pressed(settings.key_map_zoom_in) { mapcam.target_zoom_level = (mapcam.target_zoom_level / 1.1).max(2.0); } let zoom_speed = 0.05; // should be between 0.0001 (slow) and 1.0 (instant) mapcam.zoom_level = zoom_speed * mapcam.target_zoom_level + (1.0 - zoom_speed) * mapcam.zoom_level; // Update point of view let pov_rotation = Quat::from_euler(EulerRot::XYZ, 0.0, mapcam.yaw, mapcam.pitch); let point_of_view = pov_rotation * (mapcam.zoom_level * Vec3::new(1.0, 0.0, 0.0)); // Apply updates to camera camera_transform.translation = player_transform.translation + point_of_view; camera_transform.look_at(player_transform.translation, Vec3::Y); } pub fn update_fov( q_player: Query<&actor::ExperiencesGForce, With>, mouse_input: Res>, mut settings: ResMut, mut q_camera: Query<&mut Projection, With>, ) { if let (Ok(gforce), Ok(mut projection)) = (q_player.get_single(), q_camera.get_single_mut()) { let fov: f32; if settings.hud_active && mouse_input.pressed(settings.key_zoom) { fov = settings.zoom_fov.to_radians(); if !settings.is_zooming { settings.is_zooming = true; } } else { fov = (gforce.visual_effect.clamp(0.0, 1.0) * settings.fov_highspeed + settings.fov).to_radians(); if settings.is_zooming { settings.is_zooming = false; } }; *projection = Projection::Perspective(PerspectiveProjection { fov: fov, ..default() }); } } pub fn handle_input( keyboard_input: Res>, mut settings: ResMut, mut mapcam: ResMut, mut ew_sfx: EventWriter, ) { if keyboard_input.just_pressed(settings.key_camera) { settings.third_person ^= true; } if keyboard_input.just_pressed(settings.key_map) { settings.map_active ^= true; *mapcam = MapCam::default(); } if keyboard_input.just_pressed(settings.key_rotation_stabilizer) { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click)); settings.rotation_stabilizer_active ^= true; } } fn manage_player_actor( mut commands: Commands, settings: Res, mut q_playercam: Query<&mut Visibility, With>, mut q_hiddenplayer: Query<(Entity, &mut Visibility, &mut Position, &mut Rotation, &mut LinearVelocity, &mut AngularVelocity, Option<&mut actor::ExperiencesGForce>, Option<&actor::JustNowEnteredVehicle>), (With, Without)>, q_ride: Query<(&Transform, &Position, &Rotation, &LinearVelocity, &AngularVelocity), (With, Without)>, ) { for mut vis in &mut q_playercam { if settings.third_person || settings.map_active { *vis = Visibility::Inherited; } else { *vis = Visibility::Hidden; } } for (entity, mut vis, mut pos, mut rot, mut v, mut angv, mut gforce, entering) in &mut q_hiddenplayer { // If we are riding a vehicle, place the player at the position where // it would be after exiting the vehicle. // I would rather place it in the center of the vehicle, but at the time // of writing, I couldn't set the position/rotation of the player *during* // exiting the vehicle, so I'm doing it here instead, as a workaround. *vis = Visibility::Hidden; if let Ok((ride_trans, ride_pos, ride_rot, ride_v, ride_angv)) = q_ride.get_single() { pos.0 = ride_pos.0 + DVec3::from(ride_trans.rotation * Vec3::new(0.0, 0.0, ride_trans.scale.z * 2.0)); rot.0 = ride_rot.0 * DQuat::from_array([-1.0, 0.0, 0.0, 0.0]); *v = ride_v.clone(); *angv = ride_angv.clone(); // I really don't want people to die from the g-forces of entering // vehicles at high relative speed, even though they probably should. if let (Some(gforce), Some(_)) = (&mut gforce, entering) { gforce.last_linear_velocity = v.0; commands.entity(entity).remove::(); } } } } #[allow(clippy::too_many_arguments)] pub fn apply_input_to_player( time: Res