#![doc = include_str!("../README.md")]
use bevy::{
input::mouse::MouseMotion,
log::{error, warn},
prelude::*,
window::{CursorGrabMode, PrimaryWindow},
};
#[derive(Component)]
pub struct Spectator;
pub struct SpectatorPlugin;
impl Plugin for SpectatorPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SpectatorSettings>();
#[cfg(feature = "init")]
app.add_systems(PostStartup, spectator_init);
app.add_systems(Update, spectator_update);
}
}
#[cfg(feature = "init")]
fn spectator_init(
cameras: Query<Entity, With<Spectator>>,
mut settings: ResMut<SpectatorSettings>,
) {
use bevy::ecs::query::QuerySingleError;
if settings.active_spectator.is_none() {
settings.active_spectator = match cameras.single() {
Ok(a) => Some(a),
Err(QuerySingleError::NoEntities(_)) => {
warn!("Failed to find a Spectator; Active camera will remain unset.");
None
}
Err(QuerySingleError::MultipleEntities(_)) => {
warn!("Found more than one Spectator; Active camera will remain unset.");
None
}
};
}
}
#[allow(clippy::too_many_arguments)]
fn spectator_update(
time: Res<Time>,
keys: Res<ButtonInput<KeyCode>>,
buttons: Res<ButtonInput<MouseButton>>,
mut motion: EventReader<MouseMotion>,
mut settings: ResMut<SpectatorSettings>,
mut windows: Query<(&mut Window, Option<&PrimaryWindow>)>,
mut camera_transforms: Query<&mut Transform, With<Spectator>>,
mut focus: Local<bool>,
) {
let Some(camera_id) = settings.active_spectator else {
motion.clear();
return;
};
let Ok(mut camera_transform) = camera_transforms.get_mut(camera_id) else {
error!("Failed to find camera for active camera entity ({camera_id:?})");
settings.active_spectator = None;
motion.clear();
return;
};
let mut window = match settings.active_window {
Some(active) => {
let Ok((window, _)) = windows.get_mut(active) else {
error!("Failed to find active window ({active:?})");
settings.active_window = None;
motion.clear();
return;
};
window
}
None => {
let Some((window, _)) = windows.iter_mut().find(|(_, primary)| primary.is_some())
else {
panic!("No primary window found!");
};
window
}
};
let mut set_focus = |focused: bool| {
*focus = focused;
if !settings.orthographic {
let grab_mode = match focused {
true => CursorGrabMode::Confined,
false => CursorGrabMode::None,
};
window.cursor_options.grab_mode = grab_mode;
window.cursor_options.visible = !focused;
}
};
if keys.just_pressed(KeyCode::Escape) {
set_focus(false);
} else if buttons.just_pressed(MouseButton::Left) {
set_focus(true);
}
if *focus || settings.orthographic {
if !settings.orthographic {
let mouse_delta = {
let mut total = Vec2::ZERO;
for d in motion.read() {
total += d.delta;
}
total
};
let mouse_x = -mouse_delta.x * settings.sensitivity;
let mouse_y = -mouse_delta.y * settings.sensitivity;
let mut dof: Vec3 = camera_transform.rotation.to_euler(EulerRot::YXZ).into();
dof.x += mouse_x;
dof.y = (dof.y + mouse_y).clamp(-89f32.to_radians(), 89f32.to_radians());
dof.z = 0f32;
camera_transform.rotation = Quat::from_euler(EulerRot::YXZ, dof.x, dof.y, dof.z);
}
{
let forward = if keys.pressed(KeyCode::KeyW) {
1f32
} else {
0f32
};
let backward = if keys.pressed(KeyCode::KeyS) {
1f32
} else {
0f32
};
let right = if keys.pressed(KeyCode::KeyD) {
1f32
} else {
0f32
};
let left = if keys.pressed(KeyCode::KeyA) {
1f32
} else {
0f32
};
let up_cond = if !settings.orthographic {
keys.pressed(KeyCode::Space)
} else {
keys.pressed(KeyCode::KeyW)
};
let up = if up_cond { 1f32 } else { 0f32 };
let down_cond = if !settings.orthographic {
keys.pressed(KeyCode::ControlLeft)
} else {
keys.pressed(KeyCode::KeyS)
};
let down = if down_cond { 1f32 } else { 0f32 };
let speed = if keys.pressed(KeyCode::ShiftLeft) {
settings.alt_speed
} else {
settings.base_speed
};
let delta_axial = if settings.orthographic {
0.0
} else {
(forward - backward) * speed
};
let delta_lateral = (right - left) * speed;
let delta_vertical = (up - down) * speed;
let mut forward = *camera_transform.forward();
forward.y = 0f32;
forward = forward.normalize_or_zero();
let mut right = *camera_transform.right();
right.y = 0f32; let up = Vec3::Y;
let result = forward * delta_axial + right * delta_lateral + up * delta_vertical;
camera_transform.translation += result * time.delta_secs();
}
}
motion.clear();
}
#[derive(Resource)]
pub struct SpectatorSettings {
pub active_spectator: Option<Entity>,
pub active_window: Option<Entity>,
pub base_speed: f32,
pub alt_speed: f32,
pub sensitivity: f32,
pub orthographic: bool,
}
impl Default for SpectatorSettings {
fn default() -> Self {
Self {
active_spectator: None,
active_window: None,
base_speed: 10.0,
alt_speed: 50.0,
sensitivity: 0.001,
orthographic: false,
}
}
}