feat(camera): comprehensive camera system (C1)
Rebuild the camera around a single persistent MainCamera and the Camera System design-doc model: two framing modes (Orbit = inspection/ docked tactical, Follow = track the ship) plus Cinematic as a boolean overlay (free rotation + HUD hide + live gameplay), not a third mode. Critical fixes (gap analysis A1-A3): - A1: scenes now *reconfigure* the one startup camera instead of spawning a second one; despawn_in_system_scene no longer destroys MainCamera, so the session never loses its camera. .single_mut() on MainCamera now succeeds during flight. - A2: Cinematic is a real overlay — toggle (KeyC), free-look rotation in any framing, HUD hidden while active, gameplay keeps running. - A3: removed dead FollowCamera component + setup_follow_camera; tracking is unified in track_camera_target. Gaps B1-B6: - B1: adopted doc model (Cinematic overlay, not exclusive mode). - B2: canonical isometric tactical_rotation() baseline. - B3: smooth reframing via OrbitFocusGoal exponential-damp tween. - B4: non-zero CameraState defaults. - B5: consolidated the three orbit-control impls into one (dropped the starting_base local control + its Euler variant). - B6: track_camera_target keeps OrbitCamera.target synced to the focus entity every frame. Docked view now frames the actual station at a tactical iso distance. cargo check + clippy clean for all newly-authored code; net -10 lines (more dead code removed than added). 42 tests pass.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::camera::{CameraState, CameraMode};
|
||||
use crate::camera::{CameraState, CameraMode, OrbitFocusGoal};
|
||||
use crate::gameplay::movement::components::{Velocity, MoveTarget};
|
||||
use crate::gameplay::galaxy::Identifiable;
|
||||
use super::{DockedState, UndockEvent};
|
||||
@@ -53,14 +53,15 @@ fn handle_undock(
|
||||
mut events: EventReader<UndockEvent>,
|
||||
mut docked_state: ResMut<DockedState>,
|
||||
mut camera_state: ResMut<CameraState>,
|
||||
player_query: Query<(Entity, &Transform), (With<PlayerShip>, With<Docked>)>,
|
||||
mut focus_goal: ResMut<OrbitFocusGoal>,
|
||||
player_query: Query<Entity, (With<PlayerShip>, With<Docked>)>,
|
||||
// docked_ui_query removed - UI no longer needed
|
||||
) {
|
||||
for event in events.read() {
|
||||
bevy::log::info!("Handling undock from station {:?}", event.station_entity);
|
||||
|
||||
// Find player ship
|
||||
let Ok((player_entity, ship_transform)) = player_query.single() else {
|
||||
let Ok(player_entity) = player_query.single() else {
|
||||
bevy::log::warn!("No docked player ship found");
|
||||
continue;
|
||||
};
|
||||
@@ -87,11 +88,19 @@ fn handle_undock(
|
||||
// Update docked state resource
|
||||
docked_state.undock();
|
||||
|
||||
// Transition camera to tactical follow mode (isometric view)
|
||||
// Transition camera to follow mode: a tactical iso view tracking the
|
||||
// ship. Arm a focus goal so the distance eases from the docked framing
|
||||
// to the follow distance instead of hard-snapping. The orbit target is
|
||||
// left to `track_camera_target`, which locks it to the ship.
|
||||
camera_state.mode = CameraMode::Follow;
|
||||
camera_state.target_entity = Some(player_entity);
|
||||
camera_state.follow_distance = 45.0; // Higher for tactical view
|
||||
camera_state.follow_height = 35.0; // Isometric angle
|
||||
camera_state.follow_distance = 45.0; // Higher for tactical view
|
||||
camera_state.follow_height = 35.0; // Isometric angle
|
||||
focus_goal.arm(
|
||||
None,
|
||||
Some(camera_state.follow_distance),
|
||||
Some(crate::camera::tactical_rotation()),
|
||||
);
|
||||
|
||||
// UI removed - gameplay only
|
||||
// setup_flight_ui(commands.reborrow());
|
||||
@@ -111,6 +120,7 @@ fn handle_docking(
|
||||
mut events: EventReader<DockEvent>,
|
||||
mut docked_state: ResMut<DockedState>,
|
||||
mut camera_state: ResMut<CameraState>,
|
||||
mut focus_goal: ResMut<OrbitFocusGoal>,
|
||||
identifiable_query: Query<&Identifiable>,
|
||||
// flight_ui_query removed - UI no longer needed
|
||||
) {
|
||||
@@ -134,9 +144,15 @@ fn handle_docking(
|
||||
// Update docked state resource
|
||||
docked_state.dock_at(event.station);
|
||||
|
||||
// Transition camera to cinematic mode
|
||||
camera_state.mode = CameraMode::Cinematic;
|
||||
// Transition camera back to the docked tactical framing (Orbit on the
|
||||
// station), easing the distance in.
|
||||
camera_state.mode = CameraMode::Orbit;
|
||||
camera_state.target_entity = Some(event.station);
|
||||
focus_goal.arm(
|
||||
None,
|
||||
Some(crate::camera::DOCKED_FRAMING_DISTANCE),
|
||||
Some(crate::camera::tactical_rotation()),
|
||||
);
|
||||
|
||||
// UI removed - no longer needed
|
||||
// Despawn flight HUD
|
||||
|
||||
@@ -21,7 +21,7 @@ pub use docked::{DockedState, UndockEvent};
|
||||
pub use flight::{FlightState, FlightControlsPlugin};
|
||||
pub use scene::ActiveSystem;
|
||||
pub use target::{Targetable, TargetKind, SelectedTarget, TargetSelectionPlugin};
|
||||
pub use actions::{ContextualActionPlugin, ActionType, ActionTriggeredEvent};
|
||||
pub use actions::{ContextualActionPlugin, ActionType, ActionTriggeredEvent, ActionUi};
|
||||
pub use operations::{TimedOperationPlugin, OperationKind, ActiveOperation};
|
||||
|
||||
pub struct InSystemPlugin;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! Handles spawning the star, POIs, and player ship docked at a station.
|
||||
|
||||
use bevy::ecs::system::SystemParam;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::camera::{MainCamera, OrbitCamera};
|
||||
@@ -11,8 +12,7 @@ use crate::gameplay::galaxy::{
|
||||
contents, Massive, Luminosity, MassLock, BoundingVolume, Identifiable,
|
||||
SystemContents,
|
||||
};
|
||||
use crate::gameplay::in_system::{DockedState};
|
||||
use crate::state::AppState;
|
||||
use crate::gameplay::in_system::DockedState;
|
||||
|
||||
/// Tracks the currently active system for gameplay.
|
||||
#[derive(Resource, Debug, Clone, Default)]
|
||||
@@ -45,12 +45,6 @@ pub struct DockingTarget {
|
||||
/// Offset from station where player ship spawns when docked.
|
||||
const DOCKED_OFFSET: Vec3 = Vec3::new(1.5, 0.0, 0.0);
|
||||
|
||||
/// Distance for camera to view the docked scene cinematically.
|
||||
const DOCKED_CAMERA_DISTANCE: f32 = 8.0;
|
||||
|
||||
/// Slight upward tilt for cinematic docked view.
|
||||
const DOCKED_CAMERA_PITCH: f32 = 0.3;
|
||||
|
||||
/// Represents a docking target (either a station or a habitable planet).
|
||||
#[derive(Debug, Clone)]
|
||||
struct DockingTargetInfo {
|
||||
@@ -61,14 +55,22 @@ struct DockingTargetInfo {
|
||||
is_station: bool,
|
||||
}
|
||||
|
||||
/// Bundles the in-system runtime state so [`setup_in_system_view`] stays
|
||||
/// under clippy's argument-count threshold while also taking a camera query.
|
||||
#[derive(SystemParam)]
|
||||
pub(crate) struct InSystemRuntimeState<'w> {
|
||||
docked: ResMut<'w, DockedState>,
|
||||
active: ResMut<'w, ActiveSystem>,
|
||||
}
|
||||
|
||||
pub fn setup_in_system_view(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
campaign: Res<CampaignDraft>,
|
||||
mut docked_state: ResMut<DockedState>,
|
||||
mut active_system: ResMut<ActiveSystem>,
|
||||
mut runtime: InSystemRuntimeState,
|
||||
mut camera_state: ResMut<crate::camera::CameraState>,
|
||||
mut camera_query: Query<&mut OrbitCamera, With<MainCamera>>,
|
||||
) {
|
||||
// Get the selected starting base
|
||||
let Some(starting_base) = &campaign.starting_base else {
|
||||
@@ -134,19 +136,29 @@ pub fn setup_in_system_view(
|
||||
);
|
||||
|
||||
// Update resources
|
||||
docked_state.system_id = system.id.clone();
|
||||
docked_state.is_docked = true;
|
||||
docked_state.station_entity = Some(station_entity);
|
||||
runtime.docked.system_id = system.id.clone();
|
||||
runtime.docked.is_docked = true;
|
||||
runtime.docked.station_entity = Some(station_entity);
|
||||
|
||||
active_system.system_id = system.id.clone();
|
||||
active_system.system_name = system.name.clone();
|
||||
active_system.star_entity = Some(star_entity);
|
||||
runtime.active.system_id = system.id.clone();
|
||||
runtime.active.system_name = system.name.clone();
|
||||
runtime.active.star_entity = Some(star_entity);
|
||||
|
||||
// Position camera for cinematic docked view
|
||||
setup_docked_camera(&mut commands);
|
||||
// Reconfigure the persistent camera for a tactical framing of the docked
|
||||
// station. Scenes must never spawn a new camera (see `camera` module docs);
|
||||
// a hard set is correct here because the coordinate scale changes
|
||||
// discontinuously (galaxy → in-system).
|
||||
let station_pos = orbital_position(docking_target.orbit, docking_target.phase);
|
||||
crate::camera::retarget_main_camera(
|
||||
&mut camera_query,
|
||||
station_pos,
|
||||
crate::camera::DOCKED_FRAMING_DISTANCE,
|
||||
crate::camera::tactical_rotation(),
|
||||
);
|
||||
|
||||
// Set camera mode to cinematic
|
||||
camera_state.mode = crate::camera::CameraMode::Cinematic;
|
||||
// Orbit framing focused on the station; `track_camera_target` keeps the
|
||||
// orbit target locked to the station each frame.
|
||||
camera_state.mode = crate::camera::CameraMode::Orbit;
|
||||
camera_state.target_entity = Some(station_entity);
|
||||
}
|
||||
|
||||
@@ -363,45 +375,17 @@ fn spawn_player_ship_docked(
|
||||
ship_entity
|
||||
}
|
||||
|
||||
/// Setup camera for cinematic docked view.
|
||||
fn setup_docked_camera(commands: &mut Commands) {
|
||||
// Position camera for a cinematic view looking at the docked scene
|
||||
// Offset back and up, angled down slightly
|
||||
let camera_distance = DOCKED_CAMERA_DISTANCE;
|
||||
let camera_pitch = DOCKED_CAMERA_PITCH;
|
||||
let yaw = Quat::from_rotation_y(0.0);
|
||||
let pitch = Quat::from_rotation_x(camera_pitch);
|
||||
let rotation = yaw * pitch;
|
||||
|
||||
let offset = rotation * Vec3::new(0.0, 0.0, camera_distance);
|
||||
let position = Vec3::new(DOCKED_OFFSET.x * 0.5, 1.0, 0.0) + offset;
|
||||
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
Transform::from_translation(position)
|
||||
.with_rotation(rotation)
|
||||
.looking_at(Vec3::new(DOCKED_OFFSET.x * 0.5, 0.0, 0.0), Vec3::Y),
|
||||
MainCamera,
|
||||
OrbitCamera {
|
||||
target: Vec3::new(DOCKED_OFFSET.x * 0.5, 0.0, 0.0),
|
||||
distance: camera_distance,
|
||||
rotation,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
/// Despawn all entities spawned for the in-system view.
|
||||
///
|
||||
/// The [`MainCamera`] is persistent across states (spawned once at [`Startup`])
|
||||
/// and is intentionally **not** despawned here — scenes reconfigure it.
|
||||
/// Despawning it would leave the session with no camera, since [`Startup`] does
|
||||
/// not re-run.
|
||||
pub fn despawn_in_system_scene(
|
||||
mut commands: Commands,
|
||||
query: Query<Entity, With<InSystemSpawned>>,
|
||||
camera_query: Query<Entity, With<MainCamera>>,
|
||||
) {
|
||||
for entity in &query {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
|
||||
// Also despawn the main camera (it gets recreated on next enter)
|
||||
for entity in &camera_query {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user