Skip to content

Commit

Permalink
Close #42 - Add max_slope field for TnuaBuiltinWalk
Browse files Browse the repository at this point in the history
  slopes as walls.
  • Loading branch information
idanarye committed May 17, 2024
1 parent c5123bc commit d0f55c5
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
NOTE: Subcrates have their own changelogs: [bevy-tnua-physics-integration-layer](physics-integration-layer/CHANGELOG.md), [bevy-tnua-rapier](rapier3d/CHANGELOG.md), [bevy-tnua-xpbd](xpbd3d/CHANGELOG.md).

## [Unreleased]
### Added
- `max_slope` field for `TnuaBuiltinWalk` to make the character treat too steep
slopes as walls.

## 0.17.0 - 2024-05-07
### Removed
- [**BREAKING**] `TnuaBuiltinWalk` no longer has an `up` field. The up
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ Note that:
[XPBD](https://idanarye.github.io/bevy-tnua/demos/shooter_like-xpbd),
[XPBD (f64 version)](https://idanarye.github.io/bevy-tnua/demos/shooter_like-xpbd-64)

The basis and actions in the demos can be tweaked with a GUI. They are initialized to the `Default::default()` provided in Tnua, with the following exceptions:

* `TnuaBuiltinWalk::desired_velocity` defaults to the zero vector, but when the user walks the character it is set to a vector of length 20.0 (40.0 in the 2D demo)
* `TnuaBuiltinWalk::float_height` is set to 2.0 even though it defaults to 0.0. User code should always set the float height based on the model's geometrics.
* `TnuaBuiltinWalk::max_slope` is set to $\frac{\pi}{4}$ even though it defaults to $\frac{\pi}{2}$ (which disables the slipping behavior, since this is the slope angle of a wall)
* `TnuaBuiltinJump::height` is set to 4.0 even though it defaults to 0.0. User code should always set the jump height based on the game's requirements (a jump action of zero height is useless)
* `TnuaBuiltinCrouch::float_offset` is set to -0.9 even though it defaults to 0.0. Just like `float_height`, this value should always be set by user code based on the model's geometric.
* `TnuaBuiltinDash::displacement` defaults to 0.0, but when the user inputs the command to dash it gets set to a vector of length 10.0.

### Running the Demos Locally

```sh
Expand Down
3 changes: 2 additions & 1 deletion demos/src/bin/platformer_2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bevy_tnua::control_helpers::{
TnuaSimpleFallThroughPlatformsHelper,
};
#[allow(unused_imports)]
use bevy_tnua::math::{AsF32, Vector3};
use bevy_tnua::math::{float_consts, AsF32, Vector3};
use bevy_tnua::prelude::*;
use bevy_tnua::{TnuaGhostSensor, TnuaToggle};
#[cfg(feature = "rapier2d")]
Expand Down Expand Up @@ -182,6 +182,7 @@ fn setup_player(mut commands: Commands) {
speed: 40.0,
walk: TnuaBuiltinWalk {
float_height: 2.0,
max_slope: float_consts::FRAC_PI_4,
..Default::default()
},
actions_in_air: 1,
Expand Down
3 changes: 2 additions & 1 deletion demos/src/bin/platformer_3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bevy_tnua::control_helpers::{
TnuaSimpleFallThroughPlatformsHelper,
};
#[allow(unused_imports)]
use bevy_tnua::math::{AsF32, Vector3};
use bevy_tnua::math::{float_consts, AsF32, Vector3};
use bevy_tnua::prelude::*;
use bevy_tnua::{TnuaAnimatingState, TnuaGhostSensor, TnuaToggle};
#[cfg(feature = "rapier3d")]
Expand Down Expand Up @@ -190,6 +190,7 @@ fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
speed: 20.0,
walk: TnuaBuiltinWalk {
float_height: 2.0,
max_slope: float_consts::FRAC_PI_4,
..Default::default()
},
actions_in_air: 1,
Expand Down
1 change: 1 addition & 0 deletions demos/src/bin/shooter_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
speed: 20.0,
walk: TnuaBuiltinWalk {
float_height: 2.0,
max_slope: float_consts::FRAC_PI_4,
turning_angvel: Float::INFINITY,
..Default::default()
},
Expand Down
8 changes: 6 additions & 2 deletions demos/src/levels_setup/for_2d_platformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ pub fn setup_level(mut commands: Commands, asset_server: Res<AssetServer>) {

for ([width, height], transform) in [
(
[20.0, 0.1],
Transform::from_xyz(10.0, 10.0, 0.0).with_rotation(Quat::from_rotation_z(0.6)),
[10.0, 0.1],
Transform::from_xyz(7.0, 7.0, 0.0).with_rotation(Quat::from_rotation_z(0.6)),
),
(
[10.0, 0.1],
Transform::from_xyz(14.0, 14.0, 0.0).with_rotation(Quat::from_rotation_z(1.0)),
),
([4.0, 2.0], Transform::from_xyz(-4.0, 1.0, 0.0)),
([6.0, 1.0], Transform::from_xyz(-10.0, 4.0, 0.0)),
Expand Down
8 changes: 6 additions & 2 deletions demos/src/levels_setup/for_3d_platformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@ pub fn setup_level(
let obstacles_material = materials.add(Color::GRAY);
for ([width, height, depth], transform) in [
(
[20.0, 0.1, 2.0],
Transform::from_xyz(10.0, 10.0, 0.0).with_rotation(Quat::from_rotation_z(0.6)),
[10.0, 0.1, 2.0],
Transform::from_xyz(7.0, 7.0, 0.0).with_rotation(Quat::from_rotation_z(0.6)),
),
(
[10.0, 0.1, 2.0],
Transform::from_xyz(14.0, 14.0, 0.0).with_rotation(Quat::from_rotation_z(1.0)),
),
([4.0, 2.0, 2.0], Transform::from_xyz(-4.0, 1.0, 0.0)),
([6.0, 1.0, 2.0], Transform::from_xyz(-10.0, 4.0, 0.0)),
Expand Down
7 changes: 6 additions & 1 deletion demos/src/ui/tuning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use std::ops::RangeInclusive;

use bevy_tnua::builtins::{TnuaBuiltinCrouch, TnuaBuiltinDash};
use bevy_tnua::math::Float;
use bevy_tnua::math::{float_consts, Float};
use bevy_tnua::prelude::*;

#[cfg(feature = "egui")]
Expand Down Expand Up @@ -137,6 +137,11 @@ impl UiTunable for TnuaBuiltinWalk {
&mut self.turning_angvel,
0.0..=70.0,
);

ui.add(
egui::Slider::new(&mut self.max_slope, 0.0..=float_consts::FRAC_PI_2)
.text("Max Slope (in radians)"),
);
}
}

Expand Down
127 changes: 103 additions & 24 deletions src/builtins/walk.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::time::Duration;

use crate::math::{AdjustPrecision, Float, Quaternion, Vector3};
use crate::math::{float_consts, AdjustPrecision, Float, Quaternion, Vector3};
use bevy::prelude::*;
use bevy_tnua_physics_integration_layer::math::AsF32;

use crate::util::rotation_arc_around_axis;
use crate::TnuaBasisContext;
Expand Down Expand Up @@ -116,6 +117,9 @@ pub struct TnuaBuiltinWalk {

/// The maximum angular velocity used for turning the character when the direction changes.
pub turning_angvel: Float,

/// The maximum slope, in radians, that the character can stand on without slipping.
pub max_slope: Float,
}

impl Default for TnuaBuiltinWalk {
Expand All @@ -134,6 +138,7 @@ impl Default for TnuaBuiltinWalk {
tilt_offset_angvel: 5.0,
tilt_offset_angacl: 500.0,
turning_angvel: 10.0,
max_slope: float_consts::FRAC_PI_2,
}
}
}
Expand All @@ -151,6 +156,7 @@ impl TnuaBasis for TnuaBuiltinWalk {
let climb_vectors: Option<ClimbVectors>;
let considered_in_air: bool;
let impulse_to_offset: Vector3;
let slipping_vector: Option<Vector3>;

if let Some(sensor_output) = &ctx.proximity_sensor.output {
state.effective_velocity = ctx.tracker.velocity - sensor_output.entity_linvel;
Expand All @@ -169,8 +175,26 @@ impl TnuaBasis for TnuaBuiltinWalk {
sideways: sideways_unnormalized.normalize_or_zero().adjust_precision(),
});
}
considered_in_air = state.airborne_timer.is_some();
if considered_in_air {

slipping_vector = {
let angle_with_floor = sensor_output
.normal
.angle_between(*ctx.up_direction())
.adjust_precision();
if angle_with_floor <= self.max_slope {
None
} else {
Some(
sensor_output
.normal
.reject_from(*ctx.up_direction())
.adjust_precision(),
)
}
};

if state.airborne_timer.is_some() {
considered_in_air = true;
impulse_to_offset = Vector3::ZERO;
state.standing_on = None;
} else {
Expand All @@ -184,16 +208,24 @@ impl TnuaBasis for TnuaBuiltinWalk {
} else {
impulse_to_offset = Vector3::ZERO;
}
state.standing_on = Some(StandingOnState {
entity: sensor_output.entity,
entity_linvel: sensor_output.entity_linvel,
});

if slipping_vector.is_none() {
considered_in_air = false;
state.standing_on = Some(StandingOnState {
entity: sensor_output.entity,
entity_linvel: sensor_output.entity_linvel,
});
} else {
considered_in_air = true;
state.standing_on = None;
}
}
} else {
state.effective_velocity = ctx.tracker.velocity;
climb_vectors = None;
considered_in_air = true;
impulse_to_offset = Vector3::ZERO;
slipping_vector = None;
state.standing_on = None;
}
state.effective_velocity += impulse_to_offset;
Expand All @@ -217,7 +249,17 @@ impl TnuaBasis for TnuaBuiltinWalk {
};
let max_acceleration = direction_change_factor * relevant_acceleration_limit;

let walk_vel_change = if self.desired_velocity == Vector3::ZERO {
state.vertical_velocity = if let Some(climb_vectors) = &climb_vectors {
state.effective_velocity.dot(climb_vectors.direction)
* climb_vectors
.direction
.dot(ctx.up_direction().adjust_precision())
} else {
0.0
};

let walk_vel_change = if self.desired_velocity == Vector3::ZERO && slipping_vector.is_none()
{
// When stopping, prefer a boost to be able to reach a precise stop (see issue #39)
let walk_boost = desired_boost.clamp_length_max(ctx.frame_duration * max_acceleration);
let walk_boost = if let Some(climb_vectors) = &climb_vectors {
Expand All @@ -231,29 +273,63 @@ impl TnuaBasis for TnuaBuiltinWalk {
// better (see issue #34)
let walk_acceleration =
(desired_boost / ctx.frame_duration).clamp_length_max(max_acceleration);
let walk_acceleration = if let Some(climb_vectors) = &climb_vectors {
climb_vectors.project(walk_acceleration)
} else {
walk_acceleration
};
TnuaVelChange::acceleration(walk_acceleration)
};
let walk_acceleration =
if let (Some(climb_vectors), None) = (&climb_vectors, slipping_vector) {
climb_vectors.project(walk_acceleration)
} else {
walk_acceleration
};

let slipping_boost = 'slipping_boost: {
let Some(slipping_vector) = slipping_vector else {
break 'slipping_boost Vector3::ZERO;
};
let vertical_velocity = if 0.0 <= state.vertical_velocity {
ctx.tracker
.gravity
.dot(ctx.up_direction().adjust_precision())
* ctx.frame_duration
} else {
state.vertical_velocity
};

let Ok((slipping_direction, slipping_per_vertical_unit)) =
Direction3d::new_and_length(slipping_vector.f32())
else {
break 'slipping_boost Vector3::ZERO;
};

let required_veloicty_in_slipping_direction =
slipping_per_vertical_unit.adjust_precision() * -vertical_velocity;
let expected_velocity = velocity_on_plane + walk_acceleration * ctx.frame_duration;
let expected_velocity_in_slipping_direction =
expected_velocity.dot(slipping_direction.adjust_precision());

let diff = required_veloicty_in_slipping_direction
- expected_velocity_in_slipping_direction;

if diff <= 0.0 {
break 'slipping_boost Vector3::ZERO;
}

state.vertical_velocity = if let Some(climb_vectors) = &climb_vectors {
state.effective_velocity.dot(climb_vectors.direction)
* climb_vectors
.direction
.dot(ctx.up_direction().adjust_precision())
} else {
0.0
slipping_direction.adjust_precision() * diff
};
TnuaVelChange {
acceleration: walk_acceleration,
boost: slipping_boost,
}
};

let upward_impulse: TnuaVelChange = 'upward_impulse: {
let should_disable_due_to_slipping =
slipping_vector.is_some() && state.vertical_velocity <= 0.0;
for _ in 0..2 {
#[allow(clippy::unnecessary_cast)]
match &mut state.airborne_timer {
None => {
if let Some(sensor_output) = &ctx.proximity_sensor.output {
if let (false, Some(sensor_output)) =
(should_disable_due_to_slipping, &ctx.proximity_sensor.output)
{
// not doing the jump calculation here
let spring_offset =
self.float_height - sensor_output.proximity.adjust_precision();
Expand All @@ -272,7 +348,9 @@ impl TnuaBasis for TnuaBuiltinWalk {
}
}
Some(_) => {
if let Some(sensor_output) = &ctx.proximity_sensor.output {
if let (false, Some(sensor_output)) =
(should_disable_due_to_slipping, &ctx.proximity_sensor.output)
{
if sensor_output.proximity.adjust_precision() <= self.float_height {
state.airborne_timer = None;
continue;
Expand All @@ -292,6 +370,7 @@ impl TnuaBasis for TnuaBuiltinWalk {
error!("Tnua could not decide on jump state");
TnuaVelChange::ZERO
};

motor.lin = walk_vel_change + TnuaVelChange::boost(impulse_to_offset) + upward_impulse;
let new_velocity = state.effective_velocity
+ motor.lin.boost
Expand Down

0 comments on commit d0f55c5

Please sign in to comment.