diff options
| author | Kai Stevenson <kai@kaistevenson.com> | 2026-09-02 00:05:29 -0700 |
|---|---|---|
| committer | Kai Stevenson <kai@kaistevenson.com> | 2026-09-02 00:05:29 -0700 |
| commit | 51ecf9530dae94341e2ba96453a4bf7e376ad33b (patch) | |
| tree | 24b3ec0eee4729802cddbb6af4556999e3e42d9b | |
| parent | 5ebc92d5a6bbbe14e7c4c25b25a4ff8578ddcd5a (diff) | |
character controller
| -rw-r--r-- | src/camera.rs | 8 | ||||
| -rw-r--r-- | src/config.rs | 2 | ||||
| -rw-r--r-- | src/content/entities/entity_wizard.rs | 158 | ||||
| -rw-r--r-- | src/content/entities/mod.rs | 1 | ||||
| -rw-r--r-- | src/input.rs | 11 | ||||
| -rw-r--r-- | src/main.rs | 7 | ||||
| -rw-r--r-- | src/sim/entity/mod.rs | 41 | ||||
| -rw-r--r-- | src/sim/rb_manager/mod.rs | 4 |
8 files changed, 220 insertions, 12 deletions
diff --git a/src/camera.rs b/src/camera.rs index 8d1fe78..3de7ba7 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -33,16 +33,16 @@ impl Camera { pub fn handle_camera_input(&mut self, input_manager: &InputManager, delta_time: f32) { // wasd movement - let x: f32 = if input_manager.held(Input::Left) { + let x: f32 = if input_manager.held(Input::CameraLeft) { -1.0 - } else if input_manager.held(Input::Right) { + } else if input_manager.held(Input::CameraRight) { 1.0 } else { 0.0 }; - let y: f32 = if input_manager.held(Input::Down) { + let y: f32 = if input_manager.held(Input::CameraDown) { 1.0 - } else if input_manager.held(Input::Up) { + } else if input_manager.held(Input::CameraUp) { -1.0 } else { 0.0 diff --git a/src/config.rs b/src/config.rs index 2b60c50..4bb11e1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,3 +20,5 @@ pub const SETTLED_THRESOHLD: u8 = 7; pub const MIN_ENTITY_CELLS: usize = 9; pub const TILESET_SCALING: i32 = 8; + +pub const GRAVITY: f32 = 196.2; diff --git a/src/content/entities/entity_wizard.rs b/src/content/entities/entity_wizard.rs new file mode 100644 index 0000000..eea2a48 --- /dev/null +++ b/src/content/entities/entity_wizard.rs @@ -0,0 +1,158 @@ +use glam::Vec2; +use rapier2d::{ + control::{CharacterAutostep, CharacterLength, KinematicCharacterController}, + pipeline::QueryFilter, +}; + +use crate::{ + config::GRAVITY, + input::Input, + sim::{ + entity::{EntityBehaviour, EntityDef, EntityUpdateCtx}, + sim_manager::SimCtx, + }, +}; + +struct WizardEntityBehaviour { + movement_input_x: f32, + jump: f32, + + grounded: bool, + acc_vel: Vec2, + kinematic_controller: KinematicCharacterController, +} + +const JUMP_VELOCITY: f32 = 100.0; +const ACCEL: f32 = 1000.0; +const AIR_ACCEL: f32 = 350.0; +const MAX_SPEED: f32 = 75.0; + +impl EntityBehaviour for WizardEntityBehaviour { + fn update(&mut self, _: &mut EntityUpdateCtx, ctx: &mut SimCtx, _: f32) { + if ctx.input_manager.held(Input::Left) { + self.movement_input_x = -1.0; + } else if ctx.input_manager.held(Input::Right) { + self.movement_input_x = 1.0; + } else { + self.movement_input_x = 0.0; + } + + if ctx.input_manager.pressed(Input::Up) { + self.jump = 0.06; + } + } + + fn physics_update( + &mut self, + update_ctx: &mut EntityUpdateCtx, + ctx: &mut SimCtx, + delta_time: f32, + ) { + if self.grounded { + // TODO delta_time + if self.movement_input_x.abs() < 0.2 { + self.acc_vel.x = self.acc_vel.x * 0.7; + } + // if we're moving up, keep moving up + self.acc_vel.y = self.acc_vel.y.min(0.0); + if self.jump > 0.0 { + self.acc_vel.y -= JUMP_VELOCITY; + self.jump = 0.0; + } + } else { + self.acc_vel.x = self.acc_vel.x * 0.95; + self.acc_vel.y += GRAVITY * delta_time; + + if self.jump > 0.0 { + self.jump -= delta_time; + } + } + + if self.acc_vel.x.abs() < 1.0 { + self.acc_vel.x = 0.0; + } + + let flip_adj = if self.acc_vel.x.signum() != self.movement_input_x.signum() { + 3.0 + } else { + 1.0 + }; + self.acc_vel.x += self.movement_input_x + * if self.grounded { ACCEL } else { AIR_ACCEL } + * delta_time + * flip_adj; + self.acc_vel.x = self.acc_vel.x.clamp(-MAX_SPEED, MAX_SPEED); + + let query_pipeline = ctx + .rb_manager + .physics_manager + .world + .query_pipeline_with_filter( + QueryFilter::default().exclude_rigid_body(update_ctx.entity_data.rb_h.unwrap()), + ); + + let rb = ctx + .rb_manager + .physics_manager + .world + .bodies + .get(update_ctx.entity_data.rb_h.unwrap()) + .unwrap(); + + let collider = ctx + .rb_manager + .physics_manager + .world + .colliders + .get(update_ctx.entity_data.collider_h.unwrap()) + .unwrap(); + + // move_shape works in translations, not velocities: feed it the distance we + // want to cover this step, and convert the allowed distance back to a velocity. + let movement = self.kinematic_controller.move_shape( + delta_time, + &query_pipeline, + collider.shape(), + rb.position(), + self.acc_vel * delta_time, + |_| {}, + ); + + let rb = ctx + .rb_manager + .physics_manager + .world + .bodies + .get_mut(update_ctx.entity_data.rb_h.unwrap()) + .unwrap(); + + rb.set_linvel(movement.translation / delta_time, true); + + self.grounded = movement.grounded; + } +} + +pub fn entity_wizard_def(position: Vec2) -> EntityDef { + let mut kinematic_controller = KinematicCharacterController::default(); + kinematic_controller.up = -Vec2::Y; + kinematic_controller.autostep = Some(CharacterAutostep { + max_height: CharacterLength::Absolute(4.0), + min_width: CharacterLength::Absolute(8.0), + include_dynamic_bodies: false, + }); + kinematic_controller.offset = CharacterLength::Absolute(0.1); + kinematic_controller.normal_nudge_factor = 1.0e-3; + kinematic_controller.max_slope_climb_angle = 45f32.to_radians(); + + EntityDef::kinematic_from_sprite( + position, + "assets/sprites/wizard", + Some(Box::new(WizardEntityBehaviour { + movement_input_x: 0.0, + jump: 0.0, + grounded: false, + acc_vel: Vec2::ZERO, + kinematic_controller, + })), + ) +} diff --git a/src/content/entities/mod.rs b/src/content/entities/mod.rs index 0ec45ce..11c1706 100644 --- a/src/content/entities/mod.rs +++ b/src/content/entities/mod.rs @@ -2,3 +2,4 @@ pub mod entity_bullet_emitter; pub mod entity_cube; pub mod entity_grenade; pub mod entity_tnt; +pub mod entity_wizard; diff --git a/src/input.rs b/src/input.rs index 30f3834..e795a61 100644 --- a/src/input.rs +++ b/src/input.rs @@ -14,6 +14,11 @@ pub enum Input { Down, Right, + CameraUp, + CameraLeft, + CameraDown, + CameraRight, + // sim management Pause, Step, @@ -34,13 +39,17 @@ pub enum Input { Action6, } -const INPUT_VAR_LEN: usize = 17; +const INPUT_VAR_LEN: usize = 21; const KEYMAP: [(KeyCode, Input); INPUT_VAR_LEN] = [ (KeyCode::KeyW, Input::Up), (KeyCode::KeyA, Input::Left), (KeyCode::KeyS, Input::Down), (KeyCode::KeyD, Input::Right), + (KeyCode::ArrowUp, Input::CameraUp), + (KeyCode::ArrowLeft, Input::CameraLeft), + (KeyCode::ArrowDown, Input::CameraDown), + (KeyCode::ArrowRight, Input::CameraRight), (KeyCode::Space, Input::Pause), (KeyCode::KeyX, Input::Step), (KeyCode::KeyC, Input::ClearGrid), diff --git a/src/main.rs b/src/main.rs index 019c8fa..4d62f8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ use crate::{ entities::{ entity_bullet_emitter::entity_bullet_emitter_def, entity_cube::entity_cube_def, entity_grenade::entity_grenade_def, entity_tnt::entity_tnt_def, + entity_wizard::entity_wizard_def, }, materials::MaterialId, world::mines::MinesBiome, @@ -107,11 +108,7 @@ impl App { } if self.input_manager.pressed(Input::Action5) { - sim.create_entity(EntityDef::from_sprite( - self.input_manager.world_mouse_pos, - "assets/sprites/wizard", - None, - )); + sim.create_entity(entity_wizard_def(self.input_manager.world_mouse_pos)); } if self.input_manager.pressed(Input::Action3) { diff --git a/src/sim/entity/mod.rs b/src/sim/entity/mod.rs index 9425f9b..4a8eda4 100644 --- a/src/sim/entity/mod.rs +++ b/src/sim/entity/mod.rs @@ -2,6 +2,7 @@ use glam::{IVec2, Vec2}; use rapier2d::{ dynamics::{RigidBody, RigidBodyBuilder, RigidBodyHandle}, geometry::{Collider, ColliderHandle}, + math::Pose2, }; use crate::{ @@ -123,6 +124,46 @@ impl EntityDef { EntityDef::from_cells(position, entity_cells, behaviour) } + + pub fn kinematic_from_cells( + position: Vec2, + cells: EntityCells, + behaviour: Option<Box<dyn EntityBehaviour>>, + ) -> Self { + let rb = RigidBodyBuilder::kinematic_velocity_based() + .translation(position) + .build(); + + let collider = RbManager::convex_hull_collider_from_marchable(&cells, Some(10)) + .mass(mass_from_cells(&cells.cells)) + .build(); + + EntityDef { + rb: Some(rb), + collider: Some(collider), + cells: Some(cells), + behaviour, + } + } + + pub fn kinematic_from_sprite( + position: Vec2, + path: &str, + behaviour: Option<Box<dyn EntityBehaviour>>, + ) -> Self { + let sprite_cells = load_sprite_to_cells(path); + let mut entity_cells = EntityCells { + cells: sprite_cells.cells, + size: IVec2::new(sprite_cells.width as i32, sprite_cells.height as i32), + }; + + entity_cells + .cells + .iter_mut() + .for_each(|c| c.set_entity_integrated(true)); + + EntityDef::kinematic_from_cells(position, entity_cells, behaviour) + } } #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] diff --git a/src/sim/rb_manager/mod.rs b/src/sim/rb_manager/mod.rs index 2b476d5..1be024a 100644 --- a/src/sim/rb_manager/mod.rs +++ b/src/sim/rb_manager/mod.rs @@ -11,7 +11,7 @@ use rapier2d::{ }; use crate::{ - config::{CELLS_TO_METRES, CHUNK_SIZE, PHYSICS_DELTA_TIME}, + config::{CELLS_TO_METRES, CHUNK_SIZE, GRAVITY, PHYSICS_DELTA_TIME}, sim::{ cell_manager::chunk::Chunk, lib::marching_squares::{Marchable, marching_squares_vertex_trace}, @@ -29,7 +29,7 @@ pub struct PhysicsManager { impl PhysicsManager { pub fn new() -> Self { let mut world = PhysicsWorld::new(); - let gravity = vec2(0.0, 9.81 * CELLS_TO_METRES); + let gravity = vec2(0.0, GRAVITY); world.gravity = gravity; world.integration_parameters = IntegrationParameters { // 20 pixels <-> 1 meter |
