intent refactor

This commit is contained in:
Jonas H
2026-03-28 13:24:05 +01:00
parent c8142708f5
commit 75a046d92a
12 changed files with 310 additions and 70 deletions

View File

@@ -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

8
src/components/intent.rs Normal file
View File

@@ -0,0 +1,8 @@
pub struct FollowPlayerIntent;
pub struct StopFollowingIntent;
pub struct CameraTransitionIntent
{
pub duration: f32,
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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<Game, Box<dyn std::error::Error>>
.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<Game, Box<dyn std::error::Error>>
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,36 +395,28 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
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
{
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);
}
}
// --- fixed-step physics ---
let physics_start = Instant::now();
@@ -443,7 +446,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>>
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);

View File

@@ -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";

View File

@@ -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<glam::Mat4>
{
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<EntityHandle> = 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<EntityHandle> = 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<EntityHandle> = 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<EntityHandle> = 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
}

View File

@@ -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<Vec3> = world
let bubble_positions: Vec<Vec3> = 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<Vec3> = 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)
{

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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<LeapingState>,
pub rolling_states: Storage<RollingState>,
pub cameras: Storage<CameraComponent>,
pub camera_transitions: Storage<CameraTransition>,
pub spotlights: Storage<SpotlightComponent>,
pub tree_tags: Storage<()>,
pub dissolves: Storage<DissolveComponent>,
@@ -110,9 +112,15 @@ pub struct World
pub projectile_tags: Storage<()>,
pub dialog_outcomes: Vec<DialogOutcomeEvent>,
// --- intents (one-frame, consumed after processing) ---
pub follow_player_intents: Storage<FollowPlayerIntent>,
pub stop_following_intents: Storage<StopFollowingIntent>,
pub camera_transition_intents: Storage<CameraTransitionIntent>,
// --- singleton state (not per-entity) ---
pub snow_layer: Option<SnowLayer>,
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