From 75a046d92a0f79622f285d2b99321ba5678aba69 Mon Sep 17 00:00:00 2001 From: Jonas H Date: Sat, 28 Mar 2026 13:24:05 +0100 Subject: [PATCH] intent refactor --- src/components/camera.rs | 11 ++- src/components/intent.rs | 8 ++ src/components/mod.rs | 3 +- src/editor/mod.rs | 14 +-- src/main.rs | 75 ++++++++------- src/paths.rs | 15 +++ src/systems/camera.rs | 179 ++++++++++++++++++++++++++++++++++- src/systems/dialog_camera.rs | 27 ++++-- src/systems/input.rs | 5 + src/systems/mod.rs | 10 +- src/systems/snow.rs | 5 +- src/world.rs | 28 +++++- 12 files changed, 310 insertions(+), 70 deletions(-) create mode 100644 src/components/intent.rs diff --git a/src/components/camera.rs b/src/components/camera.rs index fd4ea5e..76c8adb 100644 --- a/src/components/camera.rs +++ b/src/components/camera.rs @@ -1,4 +1,13 @@ -use glam::Mat4; +use glam::{Mat4, Vec3}; + +pub struct CameraTransition +{ + pub source_position: Vec3, + pub source_yaw: f32, + pub source_pitch: f32, + pub elapsed: f32, + pub duration: f32, +} #[derive(Clone, Copy)] pub struct CameraComponent diff --git a/src/components/intent.rs b/src/components/intent.rs new file mode 100644 index 0000000..b3ddf1a --- /dev/null +++ b/src/components/intent.rs @@ -0,0 +1,8 @@ +pub struct FollowPlayerIntent; + +pub struct StopFollowingIntent; + +pub struct CameraTransitionIntent +{ + pub duration: f32, +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 095e215..7933d28 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,6 +3,7 @@ pub mod dialog; pub mod dissolve; pub mod follow; pub mod input; +pub mod intent; pub mod jump; pub mod lights; pub mod mesh; @@ -14,7 +15,7 @@ pub mod rotate; pub mod tree_instances; pub mod trigger; -pub use camera::CameraComponent; +pub use camera::{CameraComponent, CameraTransition}; pub use dialog::{ DialogBubbleComponent, DialogOutcome, DialogOutcomeEvent, DialogPhase, DialogProjectileComponent, DialogSourceComponent, ParryButton, diff --git a/src/editor/mod.rs b/src/editor/mod.rs index a4518ba..cc45f9a 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -3,8 +3,6 @@ mod inspector; use sdl3_sys::events::SDL_Event; use crate::entity::EntityHandle; -use crate::systems::camera_noclip_system; -use crate::utility::input::InputState; use crate::world::World; pub use inspector::FrameStats; @@ -73,18 +71,8 @@ impl EditorState } } -pub fn editor_loop( - editor: &mut EditorState, - world: &mut World, - input_state: &InputState, - stats: &FrameStats, - delta: f32, -) +pub fn editor_loop(editor: &mut EditorState, world: &mut World, stats: &FrameStats) { - if editor.right_mouse_held - { - camera_noclip_system(world, input_state, delta); - } let selected = editor.selected_entity; let show_player_state = editor.show_player_state; editor diff --git a/src/main.rs b/src/main.rs index b309cb4..3f81cd5 100755 --- a/src/main.rs +++ b/src/main.rs @@ -21,20 +21,21 @@ use crate::bundles::spotlight::spawn_spotlights; use crate::bundles::terrain::{TerrainBundle, TerrainConfig}; use crate::bundles::test_char::TestCharBundle; use crate::bundles::Bundle; +use crate::components::intent::{FollowPlayerIntent, StopFollowingIntent}; use crate::debug::{collider_debug, DebugMode}; use crate::editor::{editor_loop, EditorState, FrameStats}; use crate::entity::EntityHandle; use crate::loaders::scene::Space; use crate::physics::PhysicsManager; use crate::render::snow::{SnowConfig, SnowLayer}; -use crate::systems::camera::stop_camera_following; use crate::systems::{ - camera_follow_system, camera_input_system, camera_view_matrix, dialog_bubble_render_system, - dialog_camera_system, dialog_projectile_system, dialog_system, physics_sync_system, - player_input_system, render_system, rotate_system, snow_system, spotlight_sync_system, - start_camera_following, state_machine_physics_system, state_machine_system, - tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system, - trigger_system, + camera_follow_system, camera_ground_clamp_system, camera_input_system, camera_intent_system, + camera_noclip_system, camera_transition_system, camera_view_matrix, + dialog_bubble_render_system, dialog_camera_system, dialog_camera_transition_system, + dialog_projectile_system, dialog_system, physics_sync_system, player_input_system, + render_system, rotate_system, snow_system, spotlight_sync_system, state_machine_physics_system, + state_machine_system, tree_dissolve_update_system, tree_instance_buffer_update_system, + tree_occlusion_system, trigger_system, }; use crate::utility::input::InputState; use crate::utility::time::Time; @@ -72,6 +73,7 @@ fn init() -> Result> .window("snow_trail", 1200, 900) .position_centered() .resizable() + .high_pixel_density() .vulkan() .build()?; let renderer = pollster::block_on(Renderer::new(&window, 2))?; @@ -85,7 +87,9 @@ fn init() -> Result> editor.init_platform(&window); let (mut world, camera_entity) = init_world()?; - start_camera_following(&mut world, camera_entity); + world + .follow_player_intents + .insert(camera_entity, FollowPlayerIntent); let _event_pump = sdl_context.event_pump()?; let input_state = InputState::new(); @@ -208,7 +212,9 @@ fn process_events(game: &mut Game) -> bool { game.editor.right_mouse_held = true; game.input_state.mouse_captured = true; - stop_camera_following(&mut game.world, game.camera_entity); + game.world + .stop_following_intents + .insert(game.camera_entity, StopFollowingIntent); game.sdl_context .mouse() .set_relative_mouse_mode(&game.window, true); @@ -265,7 +271,9 @@ fn toggle_editor(game: &mut Game) game.editor.active = !game.editor.active; if game.editor.active { - stop_camera_following(&mut game.world, game.camera_entity); + game.world + .stop_following_intents + .insert(game.camera_entity, StopFollowingIntent); game.sdl_context .mouse() .set_relative_mouse_mode(&game.window, false); @@ -274,7 +282,9 @@ fn toggle_editor(game: &mut Game) } else { - start_camera_following(&mut game.world, game.camera_entity); + game.world + .follow_player_intents + .insert(game.camera_entity, FollowPlayerIntent); game.input_state.mouse_captured = true; game.sdl_context .mouse() @@ -323,7 +333,7 @@ fn submit_frame(game: &mut Game, draw_calls: &[render::DrawCall], time: f32, del let view_proj = projection * view; let player_pos = game.world.player_position(); - let billboard_calls = + let (billboard_calls, text_vertices) = dialog_bubble_render_system(&game.world, camera_transform.position, view_proj); let frame = render::render( @@ -333,6 +343,7 @@ fn submit_frame(game: &mut Game, draw_calls: &[render::DrawCall], time: f32, del player_pos, draw_calls, &billboard_calls, + &text_vertices, time, delta, game.world.debug_mode, @@ -384,35 +395,27 @@ fn main() -> Result<(), Box> game.editor.show_player_state = !game.editor.show_player_state; } - // --- camera + input --- + // --- intent generation --- camera_input_system(&mut game.world, &game.input_state); + player_input_system(&mut game.world, &game.input_state); + dialog_camera_transition_system(&mut game.world, game.camera_entity); + // --- intent processing + camera --- + camera_intent_system(&mut game.world); + camera_noclip_system(&mut game.world, &game.input_state, delta); + dialog_camera_system(&mut game.world, delta); + camera_follow_system(&mut game.world); + camera_transition_system(&mut game.world, delta); + camera_ground_clamp_system(&mut game.world); + + // --- editor overlay --- if game.editor.active { - editor_loop( - &mut game.editor, - &mut game.world, - &game.input_state, - &game.stats, - delta, - ); + editor_loop(&mut game.editor, &mut game.world, &game.stats); } - else + if game.editor.show_player_state { - let dialog_active = !game.world.bubble_tags.all().is_empty(); - if dialog_active - { - dialog_camera_system(&mut game.world, delta); - } - else - { - camera_follow_system(&mut game.world); - } - player_input_system(&mut game.world, &game.input_state); - if game.editor.show_player_state - { - game.editor.build_hud(&game.world); - } + game.editor.build_hud(&game.world); } // --- fixed-step physics --- @@ -443,7 +446,7 @@ fn main() -> Result<(), Box> let spotlights = spotlight_sync_system(&game.world); render::update_spotlights(spotlights); - snow_system(&mut game.world, game.editor.active); + snow_system(&mut game.world); // --- draw call collection --- let mut draw_calls = render_system(&game.world); diff --git a/src/paths.rs b/src/paths.rs index cb5ee2a..41a1abc 100644 --- a/src/paths.rs +++ b/src/paths.rs @@ -63,6 +63,16 @@ pub mod dialogs } } +pub mod fonts +{ + use crate::paths::ASSETS_DIR; + + pub fn departure_mono() -> String + { + format!("{}/fonts/DepartureMono-Regular.otf", ASSETS_DIR) + } +} + pub mod shaders { use crate::paths::SHADERS_DIR; @@ -87,6 +97,11 @@ pub mod shaders format!("{}/snow_deform.wgsl", SHADERS_DIR) } + pub fn text() -> String + { + format!("{}/text.wgsl", SHADERS_DIR) + } + pub const SHADOW_PACKAGE: &str = "package::shadow"; pub const MAIN_PACKAGE: &str = "package::main"; pub const SNOW_LIGHT_ACCUMULATION_PACKAGE: &str = "package::snow_light_accumulation"; diff --git a/src/systems/camera.rs b/src/systems/camera.rs index 02458d8..6b45837 100644 --- a/src/systems/camera.rs +++ b/src/systems/camera.rs @@ -1,9 +1,14 @@ use glam::Vec3; +use crate::components::camera::CameraTransition; use crate::components::FollowComponent; +use crate::entity::EntityHandle; +use crate::physics::PhysicsManager; use crate::utility::input::InputState; use crate::world::{Transform, World}; +const CAMERA_GROUND_OFFSET: f32 = 2.0; + pub fn camera_view_matrix(world: &World) -> Option { let (camera_entity, camera_component) = world.active_camera()?; @@ -66,6 +71,35 @@ pub fn camera_input_system(world: &mut World, input_state: &InputState) } } +pub fn camera_intent_system(world: &mut World) +{ + let follow_entities: Vec = world.follow_player_intents.all(); + for entity in follow_entities + { + start_camera_following(world, entity); + world.follow_player_intents.remove(entity); + } + + let stop_entities: Vec = world.stop_following_intents.all(); + for entity in stop_entities + { + stop_camera_following(world, entity); + world.stop_following_intents.remove(entity); + } + + let transition_entities: Vec = world.camera_transition_intents.all(); + for entity in transition_entities + { + let duration = world + .camera_transition_intents + .get(entity) + .map(|i| i.duration) + .unwrap_or(0.5); + start_camera_transition(world, entity, duration); + world.camera_transition_intents.remove(entity); + } +} + pub fn camera_follow_system(world: &mut World) { let camera_entities: Vec<_> = world.follows.all(); @@ -109,10 +143,20 @@ pub fn camera_follow_system(world: &mut World) pub fn camera_noclip_system(world: &mut World, input_state: &InputState, delta: f32) { + if !input_state.mouse_captured + { + return; + } + let cameras: Vec<_> = world.cameras.all(); for camera_entity in cameras { + if world.follows.get(camera_entity).is_some() + { + continue; + } + if let Some(camera) = world.cameras.get(camera_entity) { if !camera.is_active @@ -167,7 +211,7 @@ pub fn camera_noclip_system(world: &mut World, input_state: &InputState, delta: } } -pub fn start_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle) +fn start_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle) { if let Some(camera_transform) = world.transforms.get(camera_entity) { @@ -202,7 +246,7 @@ pub fn start_camera_following(world: &mut World, camera_entity: crate::entity::E } } -pub fn stop_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle) +fn stop_camera_following(world: &mut World, camera_entity: crate::entity::EntityHandle) { if let Some(follow) = world.follows.get(camera_entity) { @@ -226,3 +270,134 @@ pub fn stop_camera_following(world: &mut World, camera_entity: crate::entity::En world.follows.remove(camera_entity); } } + +pub fn camera_ground_clamp_system(world: &mut World) +{ + let Some((camera_entity, _)) = world.active_camera() + else + { + return; + }; + + if world.follows.get(camera_entity).is_none() + { + return; + } + + world.transforms.with_mut(camera_entity, |t| { + let ground_y = + PhysicsManager::get_terrain_height_at(t.position.x, t.position.z).unwrap_or(0.0); + let min_y = ground_y + CAMERA_GROUND_OFFSET; + if t.position.y < min_y + { + t.position.y = min_y; + } + }); +} + +fn start_camera_transition(world: &mut World, camera_entity: EntityHandle, duration: f32) +{ + let Some(camera) = world.cameras.get(camera_entity) + else + { + return; + }; + + let source_yaw = camera.yaw; + let source_pitch = camera.pitch; + + let source_position = world + .transforms + .with(camera_entity, |t| t.position) + .unwrap_or(Vec3::ZERO); + + world.camera_transitions.insert( + camera_entity, + CameraTransition { + source_position, + source_yaw, + source_pitch, + elapsed: 0.0, + duration, + }, + ); +} + +pub fn camera_transition_system(world: &mut World, delta: f32) +{ + let entities: Vec = world.camera_transitions.all(); + + for entity in entities + { + let finished = { + let Some(transition) = world.camera_transitions.get_mut(entity) + else + { + continue; + }; + + transition.elapsed += delta; + let t = (transition.elapsed / transition.duration).min(1.0); + let t = smoothstep(t); + + let source_position = transition.source_position; + let source_yaw = transition.source_yaw; + let source_pitch = transition.source_pitch; + let finished = t >= 1.0; + + world.transforms.with_mut(entity, |transform| { + transform.position = source_position.lerp(transform.position, t); + }); + + if let Some(camera) = world.cameras.get_mut(entity) + { + camera.yaw = lerp_angle(source_yaw, camera.yaw, t); + camera.pitch = source_pitch + (camera.pitch - source_pitch) * t; + } + + if !finished + { + if let Some(transition) = world.camera_transitions.get_mut(entity) + { + let pos = world + .transforms + .with(entity, |tr| tr.position) + .unwrap_or(Vec3::ZERO); + let cam = world.cameras.get(entity); + transition.source_position = pos; + if let Some(cam) = cam + { + transition.source_yaw = cam.yaw; + transition.source_pitch = cam.pitch; + } + } + } + + finished + }; + + if finished + { + world.camera_transitions.remove(entity); + } + } +} + +fn smoothstep(t: f32) -> f32 +{ + t * t * (3.0 - 2.0 * t) +} + +fn lerp_angle(from: f32, to: f32, t: f32) -> f32 +{ + let mut diff = to - from; + while diff > std::f32::consts::PI + { + diff -= std::f32::consts::TAU; + } + while diff < -std::f32::consts::PI + { + diff += std::f32::consts::TAU; + } + from + diff * t +} diff --git a/src/systems/dialog_camera.rs b/src/systems/dialog_camera.rs index 009cbc8..c0f13ea 100644 --- a/src/systems/dialog_camera.rs +++ b/src/systems/dialog_camera.rs @@ -1,7 +1,21 @@ use glam::Vec3; +use crate::components::intent::CameraTransitionIntent; +use crate::entity::EntityHandle; use crate::world::World; +pub fn dialog_camera_transition_system(world: &mut World, camera_entity: EntityHandle) +{ + let dialog_active = !world.bubble_tags.all().is_empty(); + if dialog_active != world.was_dialog_active + { + world + .camera_transition_intents + .insert(camera_entity, CameraTransitionIntent { duration: 0.8 }); + world.was_dialog_active = dialog_active; + } +} + const CAMERA_LAG: f32 = 4.0; const VERTICAL_BIAS: f32 = 0.4; const MIN_DISTANCE: f32 = 8.0; @@ -17,23 +31,20 @@ pub fn dialog_camera_system(world: &mut World, delta: f32) let player_pos = world.player_position(); - let character_positions: Vec = world + let bubble_positions: Vec = world .bubble_tags .all() .iter() - .filter_map(|&bubble| { - let char_entity = world.dialog_bubbles.with(bubble, |b| b.character_entity)?; - world.transforms.with(char_entity, |t| t.position) - }) + .filter_map(|&bubble| world.transforms.with(bubble, |t| t.position)) .collect(); - if character_positions.is_empty() + if bubble_positions.is_empty() { return; } let all_positions: Vec = std::iter::once(player_pos) - .chain(character_positions.iter().copied()) + .chain(bubble_positions.iter().copied()) .collect(); let centroid = @@ -63,7 +74,7 @@ pub fn dialog_camera_system(world: &mut World, delta: f32) t.position = smoothed; }); - let look_target = centroid + Vec3::Y * 1.0; + let look_target = centroid; if let Some(camera) = world.cameras.get_mut(camera_entity) { diff --git a/src/systems/input.rs b/src/systems/input.rs index 0c5c885..f314e09 100644 --- a/src/systems/input.rs +++ b/src/systems/input.rs @@ -5,6 +5,11 @@ use crate::world::World; pub fn player_input_system(world: &mut World, input_state: &InputState) { + if !world.camera_is_following() + { + return; + } + let Some((_, camera)) = world.active_camera() else { diff --git a/src/systems/mod.rs b/src/systems/mod.rs index d16d613..137540a 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,8 +1,8 @@ pub mod camera; -pub mod dialog_system; pub mod dialog_camera; pub mod dialog_projectile; pub mod dialog_render; +pub mod dialog_system; pub mod follow; pub mod input; pub mod physics_sync; @@ -15,13 +15,13 @@ pub mod tree_dissolve; pub mod trigger; pub use camera::{ - camera_follow_system, camera_input_system, camera_noclip_system, camera_view_matrix, - start_camera_following, + camera_follow_system, camera_ground_clamp_system, camera_input_system, camera_intent_system, + camera_noclip_system, camera_transition_system, camera_view_matrix, }; -pub use dialog_system::dialog_system; -pub use dialog_camera::dialog_camera_system; +pub use dialog_camera::{dialog_camera_system, dialog_camera_transition_system}; pub use dialog_projectile::dialog_projectile_system; pub use dialog_render::dialog_bubble_render_system; +pub use dialog_system::dialog_system; pub use input::player_input_system; pub use physics_sync::physics_sync_system; pub use render::render_system; diff --git a/src/systems/snow.rs b/src/systems/snow.rs index b838e0d..8b9c7dd 100644 --- a/src/systems/snow.rs +++ b/src/systems/snow.rs @@ -1,12 +1,13 @@ use crate::world::World; -pub fn snow_system(world: &mut World, noclip: bool) +pub fn snow_system(world: &mut World) { let camera_pos = world.active_camera_position(); let player_pos = world.player_position(); + let is_following = world.camera_is_following(); if let Some(ref mut snow_layer) = world.snow_layer { - if !noclip + if is_following { snow_layer.deform_at_position(player_pos, 1.5, 10.0); } diff --git a/src/world.rs b/src/world.rs index 4560b1e..c2abbdf 100644 --- a/src/world.rs +++ b/src/world.rs @@ -5,6 +5,7 @@ use crate::components::dialog::{ }; use crate::components::dissolve::DissolveComponent; use crate::components::follow::FollowComponent; +use crate::components::intent::{CameraTransitionIntent, FollowPlayerIntent, StopFollowingIntent}; use crate::components::lights::spot::SpotlightComponent; use crate::components::player_states::{ FallingState, IdleState, JumpingState, LeapingState, RollingState, WalkingState, @@ -12,8 +13,8 @@ use crate::components::player_states::{ use crate::components::tree_instances::TreeInstancesComponent; use crate::components::trigger::{TriggerComponent, TriggerEvent}; use crate::components::{ - CameraComponent, InputComponent, JumpComponent, MeshComponent, MovementComponent, - PhysicsComponent, RotateComponent, + CameraComponent, CameraTransition, InputComponent, JumpComponent, MeshComponent, + MovementComponent, PhysicsComponent, RotateComponent, }; use crate::debug::DebugMode; use crate::entity::{EntityHandle, EntityManager}; @@ -94,6 +95,7 @@ pub struct World pub leaping_states: Storage, pub rolling_states: Storage, pub cameras: Storage, + pub camera_transitions: Storage, pub spotlights: Storage, pub tree_tags: Storage<()>, pub dissolves: Storage, @@ -110,9 +112,15 @@ pub struct World pub projectile_tags: Storage<()>, pub dialog_outcomes: Vec, + // --- intents (one-frame, consumed after processing) --- + pub follow_player_intents: Storage, + pub stop_following_intents: Storage, + pub camera_transition_intents: Storage, + // --- singleton state (not per-entity) --- pub snow_layer: Option, pub debug_mode: DebugMode, + pub was_dialog_active: bool, } impl World @@ -136,6 +144,7 @@ impl World leaping_states: Storage::new(), rolling_states: Storage::new(), cameras: Storage::new(), + camera_transitions: Storage::new(), spotlights: Storage::new(), tree_tags: Storage::new(), dissolves: Storage::new(), @@ -151,8 +160,12 @@ impl World bubble_tags: Storage::new(), projectile_tags: Storage::new(), dialog_outcomes: Vec::new(), + follow_player_intents: Storage::new(), + stop_following_intents: Storage::new(), + camera_transition_intents: Storage::new(), snow_layer: None, debug_mode: DebugMode::default(), + was_dialog_active: false, } } @@ -178,6 +191,7 @@ impl World self.leaping_states.remove(entity); self.rolling_states.remove(entity); self.cameras.remove(entity); + self.camera_transitions.remove(entity); self.spotlights.remove(entity); self.tree_tags.remove(entity); self.dissolves.remove(entity); @@ -191,6 +205,9 @@ impl World self.dialog_projectiles.remove(entity); self.bubble_tags.remove(entity); self.projectile_tags.remove(entity); + self.follow_player_intents.remove(entity); + self.stop_following_intents.remove(entity); + self.camera_transition_intents.remove(entity); self.entities.despawn(entity); } @@ -211,6 +228,13 @@ impl World .unwrap_or(glam::Vec3::ZERO) } + pub fn camera_is_following(&self) -> bool + { + self.active_camera() + .map(|(e, _)| self.follows.get(e).is_some()) + .unwrap_or(false) + } + pub fn player_position(&self) -> glam::Vec3 { self.player_tags