use crate::{ GameState, control::ControlState, loading_assets::UIAssets, physics_layers::GameLayer, }; use avian3d::prelude::*; use bevy::prelude::*; #[derive(Component, Reflect, Debug)] pub struct CameraTarget; #[derive(Component, Reflect, Debug)] pub struct CameraArmRotation; /// Requested camera rotation based on various input sources (keyboard, gamepad) #[derive(Component, Reflect, Debug, Default, Deref, DerefMut)] #[reflect(Component)] pub struct CameraRotationInput(pub Vec2); #[derive(Resource, Reflect, Debug, Default)] #[reflect(Resource)] pub struct CameraState { pub cutscene: bool, pub look_around: bool, } #[derive(Component, Reflect, Debug, Default)] struct CameraUi; #[derive(Component, Reflect, Debug)] #[reflect(Component)] pub struct MainCamera { dir: Dir3, distance: f32, target_offset: Vec3, } impl MainCamera { fn new(arm: Vec3) -> Self { let (dir, distance) = Dir3::new_and_length(arm).expect("invalid arm length"); Self { dir, distance, target_offset: Vec3::new(0., 2., 0.), } } } pub fn plugin(app: &mut App) { app.register_type::(); app.register_type::(); app.register_type::(); app.init_resource::(); app.add_systems(OnEnter(GameState::Playing), startup); app.add_systems( RunFixedMainLoop, (update, update_ui, update_look_around, rotate_view) .after(RunFixedMainLoopSystem::AfterFixedMainLoop) .run_if(in_state(GameState::Playing)), ); } fn startup(mut commands: Commands) { commands.spawn(( Camera3d::default(), MainCamera::new(Vec3::new(0., 1.8, -15.)), CameraRotationInput::default(), )); } fn update_look_around(controls: Res, mut cam_state: ResMut) { let look_around = controls.view_mode; if look_around != cam_state.look_around { cam_state.look_around = look_around; } } fn update_ui( mut commands: Commands, cam_state: Res, assets: Res, query: Query>, ) { if cam_state.is_changed() { let show_ui = cam_state.look_around || cam_state.cutscene; if show_ui { commands.spawn(( CameraUi, Node { margin: UiRect::top(Val::Px(20.)) .with_left(Val::Auto) .with_right(Val::Auto), justify_content: JustifyContent::Center, ..default() }, children![( Node { display: Display::Block, position_type: PositionType::Absolute, ..default() }, ImageNode::new(assets.camera.clone()), )], )); } else { for entity in query.iter() { commands.entity(entity).despawn(); } } } } fn update( mut cam: Query< (&MainCamera, &mut Transform, &CameraRotationInput), (Without, Without), >, target_q: Single<&Transform, (With, Without)>, arm_rotation: Single<&Transform, With>, spatial_query: SpatialQuery, cam_state: Res, ) { if cam_state.cutscene { return; } let arm_tf = arm_rotation; let Ok((camera, mut cam_transform, cam_rotation_input)) = cam.single_mut() else { return; }; let target = target_q.translation + camera.target_offset; let direction = arm_tf.rotation * Quat::from_rotation_y(cam_rotation_input.x) * camera.dir; let max_distance = camera.distance; let filter = SpatialQueryFilter::from_mask(LayerMask(GameLayer::Level.to_bits())); let cam_pos = if let Some(first_hit) = spatial_query.cast_shape( &Collider::sphere(0.5), target, Quat::IDENTITY, direction, &ShapeCastConfig::from_max_distance(max_distance), &filter, ) { let distance = first_hit.distance; target + (direction * distance) } else { target + (direction * camera.distance) }; *cam_transform = Transform::from_translation(cam_pos).looking_at(target, Vec3::Y); } fn rotate_view(controls: Res, mut cam: Single<&mut CameraRotationInput>) { if !controls.view_mode { cam.x = 0.; return; } cam.0 += controls.look_dir * -0.001; }