// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This module manages the game's viewport, handles camera- and // movement-related keyboard input, and provides some camera- // related computation functions. use bevy::prelude::*; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::window::PrimaryWindow; use bevy::core_pipeline::bloom::{BloomCompositeMode, BloomSettings}; use bevy::core_pipeline::tonemapping::Tonemapping; use bevy::pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap}; use bevy::transform::TransformSystem; use bevy::math::{DVec3, DQuat}; use bevy_xpbd_3d::prelude::*; use bevy_xpbd_3d::plugins::sync; use std::f32::consts::PI; use std::f64::consts::PI as PI64; use crate::{actor, audio, hud, var}; pub const INITIAL_ZOOM_LEVEL: f64 = 10.0; 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, update_map_only_object_visibility); 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()); // To center the renderer origin on the player camera, // 1. Disable bevy_xpbd's position->transform sync function app.insert_resource(sync::SyncConfig { position_to_transform: true, transform_to_position: false, }); // 2. Add own position->transform sync function app.add_systems(PostUpdate, position_to_transform .after(sync::position_to_transform) .in_set(sync::SyncSet::PositionToTransform)); } } #[derive(Component)] pub struct ShowOnlyInMap { pub min_distance: f64, pub distance_to_id: String, } #[derive(Resource)] pub struct MapCam { pub initialized: bool, pub zoom_level: f64, pub target_zoom_level: f64, pub pitch: f64, pub yaw: f64, pub offset_x: f64, pub offset_z: f64, pub center: DVec3, } impl Default for MapCam { fn default() -> Self { Self { initialized: false, zoom_level: 2.0, target_zoom_level: INITIAL_ZOOM_LEVEL, pitch: PI64 * 0.3, yaw: 0.0, offset_x: 0.0, offset_z: 0.0, center: DVec3::new(0.0, 0.0, 0.0), } } } pub fn setup_camera( mut commands: Commands, settings: Res, ) { // 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: 2000.0, shadows_enabled: settings.shadows_sun, ..default() }, transform: Transform::from_rotation(Quat::from_rotation_y(PI/2.0)), cascade_shadow_config: CascadeShadowConfigBuilder { num_cascades: 4, minimum_distance: 0.1, maximum_distance: 5000.0, ..default() }.into(), ..default() }); commands.insert_resource(DirectionalLightShadowMap { size: settings.shadowmap_resolution, }); } 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, &Position), (With, Without)>, q_target: Query<(&Transform, &Position), (With, Without, Without)>, q_target_changed: Query<(), Changed>, mut mouse_events: EventReader, mut er_mousewheel: 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_trans, player_pos) = q_playercam.get_single().unwrap(); let (target_trans, target_pos) = if let Ok(target) = q_target.get_single() { target } else { (player_trans, player_pos) }; // 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; let min_zoom: f64 = target_trans.scale.x as f64 * 2.0; let max_zoom: f64 = 17e18; // at this point, camera starts glitching mapcam.pitch = (mapcam.pitch + mouse_delta.y as f64 / 180.0 * settings.mouse_sensitivity as f64).clamp(-PI64 / 2.0 + epsilon, PI64 / 2.0 - epsilon); mapcam.yaw += mouse_delta.x as f64 / 180.0 * settings.mouse_sensitivity as f64; // Reset movement offset if target changes if !q_target_changed.is_empty() { mapcam.offset_x = 0.0; mapcam.offset_z = 0.0; } // Get keyboard movement let mut offset_x: f64 = 0.0; let mut offset_z: f64 = 0.0; if keyboard_input.pressed(settings.key_forward) { offset_x -= 1.0; } if keyboard_input.pressed(settings.key_back) { offset_x += 1.0; } if keyboard_input.pressed(settings.key_right) { offset_z -= 1.0; } if keyboard_input.pressed(settings.key_left) { offset_z += 1.0; } if keyboard_input.pressed(settings.key_stop) { mapcam.offset_x = 0.0; mapcam.offset_z = 0.0; } // Update zoom level if !mapcam.initialized { let factor: f64 = if target_trans == player_trans { 7.0 } else { 1.0 }; mapcam.target_zoom_level *= target_trans.scale.x as f64 * factor; mapcam.zoom_level *= target_trans.scale.x as f64 * factor; mapcam.initialized = true; } let mut change_zoom: f64 = 0.0; if keyboard_input.pressed(settings.key_map_zoom_out) { change_zoom += 0.5; } if keyboard_input.pressed(settings.key_map_zoom_in) { change_zoom -= 0.5; } for wheel_event in er_mousewheel.read() { change_zoom -= wheel_event.y as f64 * 3.0; } mapcam.target_zoom_level = (mapcam.target_zoom_level * 1.1f64.powf(change_zoom)).clamp(min_zoom, max_zoom); 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).clamp(min_zoom, max_zoom); // Update point of view let pov_rotation = DQuat::from_euler(EulerRot::XYZ, 0.0, mapcam.yaw as f64, mapcam.pitch as f64); let offset = DVec3::new(mapcam.offset_x, 0.0, mapcam.offset_z); let point_of_view = pov_rotation * (mapcam.zoom_level as f64 * DVec3::new(1.0, 0.0, 0.0)); // Update movement offset let mut direction = pov_rotation * DVec3::new(offset_x, 0.0, offset_z); let speed = direction.length(); direction.y = 0.0; let direction = speed * direction.normalize_or_zero(); mapcam.offset_x += 0.01 * (direction.x * mapcam.zoom_level); mapcam.offset_z += 0.01 * (direction.z * mapcam.zoom_level); // Apply updates to camera mapcam.center = **target_pos + offset; camera_transform.translation = point_of_view.as_vec3(); camera_transform.look_at(Vec3::ZERO, 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 q_light: Query<&mut DirectionalLight>, mut settings: ResMut, mut mapcam: ResMut, mut ew_sfx: EventWriter, mut ew_updateoverlays: EventWriter, ) { if keyboard_input.just_pressed(settings.key_camera) { settings.third_person ^= true; } if keyboard_input.just_pressed(settings.key_shadows) { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click)); settings.shadows_sun ^= true; for mut light in &mut q_light { light.shadows_enabled = settings.shadows_sun; } } if keyboard_input.just_pressed(settings.key_map) { settings.map_active ^= true; if settings.map_active { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Woosh)); } *mapcam = MapCam::default(); ew_updateoverlays.send(hud::UpdateOverlayVisibility); } if keyboard_input.just_pressed(settings.key_rotation_stabilizer) { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch)); 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