From 713d865dd7799c59c2a5ec018258ce00d260fd1d Mon Sep 17 00:00:00 2001 From: reo Date: Sun, 26 Oct 2025 18:29:59 +0300 Subject: [PATCH] MASSIVE Kinematic Character Controller Update - NEW kinematic character controller powered by rapier3d at kinematic_character_controller.rs - NEW camera modes. The ability to switch between the free debug camera and new character controller. - NEW keybinds system to support the camera mode swap --- Cargo.lock | 2 + ecs/src/components.rs | 10 + engine/Cargo.toml | 1 + engine/src/lib.rs | 1 + engine/src/systems/fps_camera.rs | 29 +-- game/Cargo.toml | 1 + game/src/main.rs | 35 ++- game/src/systems/keybinds.rs | 52 +++++ .../systems/kinematic_character_controller.rs | 200 ++++++++++++++++++ game/src/systems/mod.rs | 4 + game/src/systems/physics.rs | 6 +- physics/src/physics.rs | 8 +- 12 files changed, 323 insertions(+), 26 deletions(-) create mode 100644 game/src/systems/keybinds.rs create mode 100644 game/src/systems/kinematic_character_controller.rs diff --git a/Cargo.lock b/Cargo.lock index 525e6ad..91f50f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1799,6 +1799,7 @@ dependencies = [ "indexmap", "raidillon_assets", "raidillon_core", + "raidillon_ecs", "raidillon_platform", "winit", ] @@ -1808,6 +1809,7 @@ name = "raidillon_game" version = "0.1.0" dependencies = [ "glam 0.30.8", + "hecs", "raidillon_assets", "raidillon_core", "raidillon_ecs", diff --git a/ecs/src/components.rs b/ecs/src/components.rs index fa30cc2..3c20ec7 100644 --- a/ecs/src/components.rs +++ b/ecs/src/components.rs @@ -18,3 +18,13 @@ pub struct ModelHandle(pub ModelID); #[derive(Copy, Clone)] pub struct RigidBodyComponent(pub rapier3d::dynamics::RigidBodyHandle); + +#[derive(Copy, Clone)] +pub struct CharacterBodyComponent(pub rapier3d::dynamics::RigidBodyHandle); + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum CameraMode { + #[default] + Kinematic, + Debug, +} diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 98202e4..79a2de9 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" raidillon_assets = { path = "../asset" } raidillon_core = { path = "../core" } raidillon_platform = { path = "../platform" } +raidillon_ecs = { path = "../ecs" } winit = "0.30.12" hecs = "0.10.5" indexmap = "2.10.0" diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 9a87556..a7ad724 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -6,3 +6,4 @@ mod resources; pub use crate::engine::Engine; pub use crate::resources::EngineResources; +pub use input::InputState; diff --git a/engine/src/systems/fps_camera.rs b/engine/src/systems/fps_camera.rs index 0d98577..5ff9cc7 100644 --- a/engine/src/systems/fps_camera.rs +++ b/engine/src/systems/fps_camera.rs @@ -9,6 +9,7 @@ use raidillon_platform::{Camera, PlatformContext}; use crate::input::InputState; use crate::resources::EngineResources; use raidillon_core::scene::Scene; +use raidillon_ecs::components::CameraMode; pub struct FPSDebugCameraSystem { mouse_delta: (f64, f64), @@ -35,20 +36,10 @@ impl Default for FPSDebugCameraSystem { } impl System for FPSDebugCameraSystem { - fn load_world(&mut self, res: &mut EngineResources, scene: &mut Scene) { - let pctx = res.get::().unwrap(); - scene.world.spawn((Camera { - eye: Vec3::new(0.0, 0.0, 2.0), - center: Vec3::ZERO, - up: Vec3::Y, - fovy: 60_f32.to_radians(), - aspect: pctx.frame_width / pctx.frame_height, - znear: 0.1, - zfar: 100.0, - },)); - } - fn handle_event(&mut self, res: &mut EngineResources, scene: &mut Scene) { + if !self.is_camera_mode_valid(scene) { + return + } let pctx = res.get::().unwrap(); let event2 = pctx.current_event.clone(); match event2 { @@ -92,6 +83,9 @@ impl System for FPSDebugCameraSystem { } fn frame_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { + if !self.is_camera_mode_valid(scene) { + return + } let (pctx, input) = res.get_many::<(PlatformContext, InputState)>().unwrap(); if self.mouse_enabled { @@ -134,4 +128,13 @@ impl FPSDebugCameraSystem { yaw_rad.sin() * pitch_rad.cos(), ).normalize() } + + fn is_camera_mode_valid(&self, scene: &mut Scene) -> bool { + let mut q = scene.world.query::<(&Camera, &CameraMode)>(); + let (cam_ent, (cam, cam_mode)) = q + .iter() + .next() + .unwrap(); + *cam_mode == CameraMode::Debug + } } diff --git a/game/Cargo.toml b/game/Cargo.toml index 9bb72c4..9579d22 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -18,3 +18,4 @@ raidillon_glium = { path = "../glium_platform", optional = true } glam = "0.30.5" winit = "0.30.12" rapier3d = "0.30.1" +hecs = "0.10.5" diff --git a/game/src/main.rs b/game/src/main.rs index 8ddce6a..a5a3d9c 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -1,14 +1,14 @@ mod systems; use std::fmt::format; use glam::{Quat, Vec3}; -use rapier3d::dynamics::RigidBodyType; +use rapier3d::dynamics::{CoefficientCombineRule, RigidBodyType}; use rapier3d::prelude::ColliderBuilder; -use raidillon_engine::{Engine, system::System, EngineResources}; +use raidillon_engine::{Engine, system::System, EngineResources, InputState}; use raidillon_engine::system::SystemContext; use raidillon_platform::{Platform, Camera, PlatformContext}; use raidillon_assets::model_path; use raidillon_core::engine::EngineTrait; -use raidillon_ecs::components::{ModelHandle, RigidBodyComponent}; +use raidillon_ecs::components::{CameraMode, CharacterBodyComponent, ModelHandle, RigidBodyComponent}; use raidillon_ecs::Transform; use raidillon_core::scene::Scene; #[cfg(feature = "glium")] @@ -18,8 +18,9 @@ use winit::event::DeviceEvent::MouseMotion; use winit::keyboard::{KeyCode, PhysicalKey}; use raidillon_core::DebugUIBuffer; use raidillon_engine::systems::fps_camera::FPSDebugCameraSystem; +use raidillon_glium::RenderingSystem; use raidillon_physics::Physics; -use crate::systems::PhysicsSystem; +use crate::systems::{KeybindsSystem, KinematicCharacterController, PhysicsSystem}; const TEST_GLTF: &str = "sphere.glb"; const PLANE_GLTF: &str = "plane.glb"; @@ -50,7 +51,7 @@ struct RenderingTestSystem; impl System for RenderingTestSystem { fn load_world(&mut self, res: &mut EngineResources, scene: &mut Scene) { let pctx = res.get::().expect("PlatformContext missing").clone(); - let physics = res.get_mut::().expect("Physics missing"); + let physics = scene.resources.get_mut::().expect("Physics missing"); // Spawn Sphere { @@ -59,7 +60,10 @@ impl System for RenderingTestSystem { rotation: Quat::IDENTITY, scale: Vec3::new(1.0, 1.0, 1.0), }; - let collider = ColliderBuilder::ball(1.0).build(); + let collider = ColliderBuilder::ball(1.0) + .restitution(0.7) + .restitution_combine_rule(CoefficientCombineRule::Max) + .build(); let rb_handle = physics.add_rigid_body(RigidBodyType::Dynamic, tr, collider); pctx.asset_manager.borrow_mut().load_gltf(TEST_GLTF, &model_path(TEST_GLTF)); scene.world.spawn(( @@ -84,16 +88,32 @@ impl System for RenderingTestSystem { RigidBodyComponent(rb_handle), )); } + + scene.world.spawn((Camera { + eye: Vec3::new(0.0, 2.0, 3.0), + center: Vec3::ZERO, + up: Vec3::Y, + fovy: 60_f32.to_radians(), + aspect: pctx.frame_width / pctx.frame_height, + znear: 0.1, + zfar: 100.0}, + CameraMode::default(), + )); } fn frame_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { let pctx = res.get::().unwrap(); + let input = res.get::().unwrap(); let dbg_ui = scene.resources.get_mut::().unwrap(); dbg_ui.text("Hello World!".to_owned()); dbg_ui.text(format!("Frame Delta: {}", pctx.time_ctx.frame_dt)); dbg_ui.text(format!("Fixed Delta: {}", pctx.time_ctx.fixed_dt)); dbg_ui.text(format!("FPS: {}", 1.0 / pctx.time_ctx.frame_dt)); + + for (_ent, (tr, ch_component)) in scene.world.query::<(&Transform, &CharacterBodyComponent)>().iter() { + dbg_ui.text(format!("Character POS: {}", tr.translation)); + } } } @@ -101,7 +121,8 @@ fn main() { let mut engine = Engine::new(); // Define systems engine.system_manager.add::(); - engine.system_manager.add::(); + engine.system_manager.add::(); + engine.system_manager.add::(); engine.system_manager.add::(); engine.system_manager.add::(); // engine.system_manager.add::(); diff --git a/game/src/systems/keybinds.rs b/game/src/systems/keybinds.rs new file mode 100644 index 0000000..6bc3e70 --- /dev/null +++ b/game/src/systems/keybinds.rs @@ -0,0 +1,52 @@ +use winit::keyboard::KeyCode; +use raidillon_core::DebugUIBuffer; +use raidillon_core::scene::Scene; +use raidillon_ecs::components::CameraMode; +use raidillon_engine::{EngineResources, InputState}; +use raidillon_engine::system::System; +use raidillon_platform::Camera; + +#[derive(Default)] +pub struct KeybindsSystem; + +impl System for KeybindsSystem { + fn fixed_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { + let input = res.get::().unwrap(); + + if input.key_held(KeyCode::F5) { + self.toggle_camera_mode(scene); + } + } + + fn frame_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { + let dbg_ui = scene.resources.get_mut::().unwrap(); + + dbg_ui.text("F5 to switch camera".to_owned()); + + let mut q = scene.world.query::<(&Camera, &CameraMode)>(); + let (cam_ent, (cam, cam_mode)) = q + .iter() + .next() + .unwrap(); + dbg_ui.text(format!("Camera Mode: {:?}", cam_mode)); + } +} + +impl KeybindsSystem { + fn toggle_camera_mode(&mut self, scene: &mut Scene) { + let q = scene.world.query_mut::<(&mut Camera, &mut CameraMode)>(); + let (cam_ent, (cam, cam_mode)) = q + .into_iter() + .next() + .unwrap(); + + match *cam_mode { + CameraMode::Kinematic => { + *cam_mode = CameraMode::Debug; + } + CameraMode::Debug => { + *cam_mode = CameraMode::Kinematic; + } + } + } +} diff --git a/game/src/systems/kinematic_character_controller.rs b/game/src/systems/kinematic_character_controller.rs new file mode 100644 index 0000000..4608e67 --- /dev/null +++ b/game/src/systems/kinematic_character_controller.rs @@ -0,0 +1,200 @@ +use glam::{Quat, Vec3}; +use hecs::Entity; +use rapier3d::prelude::{nalgebra, ColliderBuilder, QueryFilter, QueryPipeline, RigidBodyBuilder}; +use rapier3d::prelude::vector; +use raidillon_core::scene::Scene; +use raidillon_engine::{EngineResources, InputState}; +use raidillon_engine::system::System; +use rapier3d::control::KinematicCharacterController as RapierKinematicCharacterController; +use rapier3d::math::Isometry; +use rapier3d::na::{Isometry3, Vector3}; +use winit::event::DeviceEvent::MouseMotion; +use winit::event::{ElementState, Event, MouseButton, WindowEvent}; +use winit::keyboard::KeyCode; +use winit::window::CursorGrabMode; +use raidillon_core::DebugUIBuffer; +use raidillon_ecs::components::{CameraMode, CharacterBodyComponent}; +use raidillon_ecs::Transform; +use raidillon_engine::systems::fps_camera::FPSDebugCameraSystem; +use raidillon_physics::Physics; +use raidillon_platform::{Camera, PlatformContext}; + +#[derive(Default)] +pub struct KinematicCharacterController { + character_controller: RapierKinematicCharacterController, + character_collider: ColliderBuilder, + + desired_movement: Vec3, + last_position: Vector3, + yaw: f32, + pitch: f32, + speed: f32, + sensitivity: f32, + mouse_delta: (f64, f64), + + vertical_velocity: f32, + gravity: f32, + max_fall_speed: f32, +} + +impl System for KinematicCharacterController { + fn load_world(&mut self, res: &mut EngineResources, scene: &mut Scene) { + // create the rigid body, add it to the body set + let p = scene.resources.get_mut::().expect("Physics missing"); + let rb = RigidBodyBuilder::kinematic_position_based().build(); + let rb_handle = p.rigid_body_set.insert(rb); + self.character_collider = ColliderBuilder::capsule_z(1.5, 1.0); + p.collider_set.insert_with_parent(self.character_collider.build(), rb_handle, &mut p.rigid_body_set); + let tr = Transform { + translation: Vec3::new(0.0, 2.0, 3.0), + rotation: Quat::IDENTITY, + scale: Vec3::new(1.0, 1.0, 1.0), + }; + self.last_position = vector![ + tr.translation.x, + tr.translation.y, + tr.translation.z, + ]; + scene.world.spawn(( + tr, + CharacterBodyComponent(rb_handle), + )); + + self.speed = 5.0; + self.sensitivity = 0.1; + self.gravity = -9.81; + self.max_fall_speed = -50.0; + self.vertical_velocity = 0.0; + } + + fn handle_event(&mut self, res: &mut EngineResources, scene: &mut Scene) { + if !self.is_camera_mode_valid(scene) { + return + } + + let pctx = res.get::().unwrap(); + let event2 = pctx.current_event.clone(); + match event2 { + Event::DeviceEvent { device_id, event } => { + match event { + MouseMotion { delta } => { + self.mouse_delta.0 += delta.0; + self.mouse_delta.1 += delta.1; + }, + _ => {} + } + }, + _ => {}, + } + } + + fn frame_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { + if !self.is_camera_mode_valid(scene) { + return + } + let (pctx, input) = res.get_many::<(PlatformContext, InputState)>().unwrap(); + + self.yaw += (self.mouse_delta.0 as f32) * self.sensitivity; + self.pitch -= (self.mouse_delta.1 as f32) * self.sensitivity; + self.pitch = self.pitch.clamp(-89.0, 89.0); + + let front = self.front(); + let right_vec = front.cross(Vec3::Y).normalize(); + + if input.key_held(KeyCode::KeyW) { + self.desired_movement += front * pctx.time_ctx.frame_dt * self.speed; + } + if input.key_held(KeyCode::KeyS) { + self.desired_movement -= front * pctx.time_ctx.frame_dt * self.speed; + } + if input.key_held(KeyCode::KeyA) { + self.desired_movement -= right_vec * pctx.time_ctx.frame_dt * self.speed; + } + if input.key_held(KeyCode::KeyD) { + self.desired_movement += right_vec * pctx.time_ctx.frame_dt * self.speed; + } + + let pos = Physics::rapier_translation_to_glam(&self.last_position); + + scene.world.query_mut::<&mut Camera>().into_iter().for_each(|(_, camera)| { + // INTERPOLATION NEEDED. + camera.eye = pos; + camera.center = pos + front; + }); + self.mouse_delta = (0.0, 0.0); + } + + fn fixed_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { + if !self.is_camera_mode_valid(scene) { + return + } + let p = scene.resources.get_mut::().unwrap(); + let (pctx, input) = res.get_many::<(PlatformContext, InputState)>().unwrap(); + + let (ch_ent, (ch_tr, ch_component)) = scene + .world + .query_mut::<(&mut Transform, &mut CharacterBodyComponent)>() + .into_iter() + .next() + .expect("no character entity in world"); + + let query_pipeline = p.broad_phase.as_query_pipeline( + p.narrow_phase.query_dispatcher(), + &p.rigid_body_set, + &p.collider_set, + QueryFilter::default().exclude_rigid_body(ch_component.0), + ); + + self.vertical_velocity = (self.vertical_velocity + self.gravity * pctx.time_ctx.fixed_dt) + .max(self.max_fall_speed); + let mut total_displacement = self.desired_movement; + total_displacement.y += self.vertical_velocity * pctx.time_ctx.fixed_dt; + + let corrected_movement = self.character_controller.move_shape( + pctx.time_ctx.fixed_dt, + &query_pipeline, + &*self.character_collider.shape, + &Isometry3::from(self.last_position), + vector![total_displacement.x, total_displacement.y, total_displacement.z], + |_| {}, + ); + + // update character rigid body with the new translation. + if let Some(body) = p.get_rigid_body_mut(ch_component.0) { + self.last_position = vector![ + self.last_position.x + corrected_movement.translation.x, + self.last_position.y + corrected_movement.translation.y, + self.last_position.z + corrected_movement.translation.z, + ]; + body.set_next_kinematic_position(Isometry3::from(self.last_position)); + ch_tr.translation = Physics::rapier_translation_to_glam(&self.last_position); + // reset vertical velocity if grounded + if corrected_movement.grounded { + self.vertical_velocity = 0.0; + } + } + + self.desired_movement = Vec3::ZERO; + } +} + +impl KinematicCharacterController { + pub fn front(&self) -> Vec3 { + let yaw_rad = self.yaw.to_radians(); + let pitch_rad = self.pitch.to_radians(); + Vec3::new( + yaw_rad.cos() * pitch_rad.cos(), + pitch_rad.sin(), + yaw_rad.sin() * pitch_rad.cos(), + ).normalize() + } + + fn is_camera_mode_valid(&self, scene: &mut Scene) -> bool { + let mut q = scene.world.query::<(&Camera, &CameraMode)>(); + let (cam_ent, (cam, cam_mode)) = q + .iter() + .next() + .unwrap(); + *cam_mode == CameraMode::Kinematic + } +} diff --git a/game/src/systems/mod.rs b/game/src/systems/mod.rs index 0ab4645..f975aff 100644 --- a/game/src/systems/mod.rs +++ b/game/src/systems/mod.rs @@ -1,3 +1,7 @@ mod physics; +mod kinematic_character_controller; +mod keybinds; pub use physics::PhysicsSystem; +pub use kinematic_character_controller::KinematicCharacterController; +pub use keybinds::KeybindsSystem; diff --git a/game/src/systems/physics.rs b/game/src/systems/physics.rs index c5d4807..a77c75c 100644 --- a/game/src/systems/physics.rs +++ b/game/src/systems/physics.rs @@ -6,20 +6,22 @@ use raidillon_engine::system::System; use raidillon_physics::Physics; use raidillon_platform::PlatformContext; +/// Do physics calculations and apply to world. #[derive(Default)] pub struct PhysicsSystem; impl System for PhysicsSystem { fn load_world(&mut self, res: &mut EngineResources, scene: &mut Scene) { let p = Physics::default(); - res.insert(p); + scene.resources.insert(p); } fn fixed_update(&mut self, res: &mut EngineResources, scene: &mut Scene) { let pctx = res.get::().expect("PlatformContext missing").clone(); - let physics = res.get_mut::().expect("Physics missing"); + let physics = scene.resources.get_mut::().expect("Physics missing"); physics.step(pctx.time_ctx.fixed_dt); + // apply calculations to dynamic bodies let mut query = scene.world.query::<(&mut Transform, &RigidBodyComponent)>(); for (_ent, (tr, rb_component)) in query.iter() { if let Some(body) = physics.get_rigid_body(rb_component.0) { diff --git a/physics/src/physics.rs b/physics/src/physics.rs index 009984c..5eff699 100644 --- a/physics/src/physics.rs +++ b/physics/src/physics.rs @@ -5,12 +5,12 @@ use raidillon_ecs::Transform; /// Tiny wrapper around rapier3d. pub struct Physics { - rigid_body_set: RigidBodySet, - collider_set: ColliderSet, + pub rigid_body_set: RigidBodySet, + pub collider_set: ColliderSet, physics_pipeline: PhysicsPipeline, island_manager: IslandManager, - broad_phase: DefaultBroadPhase, - narrow_phase: NarrowPhase, + pub broad_phase: DefaultBroadPhase, + pub narrow_phase: NarrowPhase, impulse_joint_set: ImpulseJointSet, multibody_joint_set: MultibodyJointSet, ccd_solver: CCDSolver,