use bevy::app::App; use bevy::input::mouse::{MouseMotion, MouseWheel}; use bevy::math::{vec3, Vec2, Vec3Swizzles}; use bevy::prelude::{ Camera2dBundle, Commands, Component, CoreStage, Entity, EventReader, Input, Local, MouseButton, OrthographicProjection, Plugin, Projection, Query, Res, Transform, Windows, With, Without, }; use bevy::render::camera::ScalingMode; use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet}; use crate::system::flow::AppState; use crate::system::load_config::virtual_size; use crate::system::utilities::{f32_max, f32_min}; use crate::system::window::WindowManager; use crate::world::MapQuery; /// A flag component to indicate which entity should be followed by the camera #[derive(Component)] pub struct ChaseCam; /// A flag component to indicate a camera that should be used for rendering world entities and sprites #[derive(Component)] pub struct GameCamera; /// System that creates a default orthographic camera, with correct tags for querying pub fn spawn_orthographic_camera(mut commands: Commands) { let (target_width, target_height) = virtual_size(); commands.spawn(( Camera2dBundle { projection: OrthographicProjection { left: -(target_width / 2.0), right: (target_width / 2.0), top: (target_height / 2.0), bottom: -(target_height / 2.0), scaling_mode: ScalingMode::Auto { min_width: target_width, min_height: target_height, }, ..Default::default() }, ..Default::default() }, GameCamera, )); } pub fn pan_camera( mut ev_motion: EventReader<MouseMotion>, mut ev_scroll: EventReader<MouseWheel>, input_mouse: Res<Input<MouseButton>>, mut query: Query<(&mut Transform, &OrthographicProjection), With<GameCamera>>, windows: Res<Windows>, mut last_mouse_pos: Local<Vec2>, ) { let mut pan = Vec2::ZERO; if let Some(mouse) = windows .get_primary() .and_then(|window| window.cursor_position()) { let previous_mouse = std::mem::replace(&mut *last_mouse_pos, mouse); let mouse_diff = mouse - previous_mouse; if input_mouse.pressed(MouseButton::Right) { for ev in ev_motion.iter() { pan += ev.delta; } if mouse_diff != Vec2::ZERO && mouse_diff.x < 50.0 && mouse_diff.y < 50.0 { for (mut transform, projection) in &mut query { transform.translation -= mouse_diff.extend(0.0); } } } } } /// System that takes the average location of all chase camera entities, and updates the location /// of all world cameras to track the average location. /// /// e.g. If a player entity is chased, and a mouse tracking entity is chased, the world cameras will /// by updated to the midpoint between the player and the mouse pub fn sync_chase_camera_location( mut commands: Commands, chased_query: Query<&Transform, (With<ChaseCam>, Without<GameCamera>)>, mut camera_query: Query< (Entity, &mut Transform, &OrthographicProjection), (With<GameCamera>, Without<ChaseCam>), >, map_query: MapQuery, ) { if chased_query.is_empty() { return; } let mut average_location = Vec2::new(0.0, 0.0); let mut count = 0; for location in chased_query.iter() { average_location += location.translation.xy(); count += 1; } if count > 0 { average_location /= count as f32; } for (_, mut location, proj) in camera_query.iter_mut() { if let Some(bounds) = map_query.get_camera_bounds() { // log::info!("BOUNDS {:?}", bounds); let width = proj.right - proj.left; let height = proj.top - proj.bottom; let val_x = f32_max( bounds.get_min_x(width), f32_min(bounds.get_max_x(width), average_location.x), ); let val_y = f32_max( bounds.get_min_y(height), f32_min(bounds.get_max_y(height), average_location.y), ); location.translation = vec3(val_x, val_y, location.translation.z); } else { location.translation = average_location.extend(location.translation.z); } } } /// A marker struct for spawning and managing cameras. Cameras will be created on startup, and /// will constantly have their positions synced pub struct CameraManagementPlugin; impl Plugin for CameraManagementPlugin { fn build(&self, app: &mut App) { app.add_enter_system(AppState::Preload, spawn_orthographic_camera) .add_system_to_stage(CoreStage::PreUpdate, sync_chase_camera_location) .add_system_set( ConditionSet::new() .run_in_state(AppState::InGame) .with_system(pan_camera) .into(), ); } }