bevy-ichun 0.3.0

A simple kinematic character controller for avian3d
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! # Character Movement Systems
//!
//! This module provides systems and components that handle character movement based on input events.
//! It translates event data into velocity changes, handles jumping, running, and camera rotation.
//!
//! The movement systems work by responding to events dispatched by the input systems (whether your own or the ichun input module),
//! applying appropriate velocity changes to the character.
//!
//! ## Features
//!
//! * Smooth ground movement with acceleration-based controls
//! * Configurable running mode with increased speed
//! * Jumping mechanics with customizable impulse
//! * Floating / gliding mechanics with reduced gravity
//! * Controlled air movement with reduced control
//! * Camera rotation from mouse input
//!
//! ## Components
//!
//! * [`KccMovementConfig`]: Configures movement parameters like speed, jumping strength, etc.
//!
//! ## Events
//!
//! * [`IchunMoveEvent`]: Triggered when movement input is detected
//! * [`IchunRunEvent`]: Triggered when run input is detected
//! * [`IchunJumpEvent`]: Triggered when jump input is detected
//! * [`IchunFloatEvent`]: Triggered when float/glide input is detected
//! * [`IchunRotateEvent`]: Triggered when rotation input is detected
#![cfg(feature = "kcc_movement")]
use std::f32::consts::FRAC_PI_2;

use avian3d::math::{AdjustPrecision, Scalar, Vector, Vector2};
use bevy::prelude::*;

use crate::{
    kcc::{JumpedRecently, Kcc, KccVelocity},
    system_sets::IchunSystemSet,
};

/// Configuration component for character movement capabilities
///
/// This component stores the parameters that control how a character moves,
/// including movement speed, running speed, jumping strength, camera sensitivity,
/// and air control parameters.
///
/// It requires the [`Kcc`] component to be present on the same entity.
///
/// # Example
///
/// ```rust
/// use ichun::kcc::Kcc;
/// use bevy_ichun::movement::KccMovementConfig;
///
/// // Create a custom movement configuration
/// let movement_config = KccMovementConfig {
///     movement_acceleration: 25.0,
///     running_acceleration: 50.0,
///     jump_impulse: 12.0,
///     camera_sensitivity: Vector2::new(0.002, 0.001),
///     ..Default::default()
/// };
/// ```
#[derive(Component)]
#[require(Kcc)]
pub struct KccMovementConfig {
    /// The acceleration used for character movement.
    /// Higher values make the character accelerate faster.
    pub movement_acceleration: Scalar,
    /// The acceleration used for running.
    /// Higher values make the character run faster.
    pub running_acceleration: Scalar,
    /// How much control the character has in the air (0.0 - 1.0).
    /// Lower values give less control, higher values give more control.
    pub air_control_factor: Scalar,
    /// How quickly air control takes effect.
    /// Higher values make air movement more responsive.
    pub air_acceleration_factor: Scalar,
    /// Max speed in air relative to ground speed.
    /// Values above 1.0 allow faster movement in air than on ground.
    pub air_max_speed_multiplier: Scalar,
    /// The sensitivity used for character rotation (camera movement).
    /// The X component affects horizontal rotation, Y affects vertical.
    pub camera_sensitivity: Vector2,
    /// The strength of a jump impulse.
    /// Higher values make the character jump higher.
    pub jump_impulse: Scalar,
    /// Whether the character is currently running.
    pub is_running: bool,
    /// Whether the character is currently floating / gliding.
    pub is_floating: bool,
    /// The gravity being applied when KCC is floating.
    /// This is typically less than normal gravity to slow falling.
    pub floating_gravity: Vector,
}

impl Default for KccMovementConfig {
    fn default() -> Self {
        Self {
            movement_acceleration: 30.0,
            running_acceleration: 60.0,
            air_control_factor: 0.5,
            air_acceleration_factor: 1.5,
            air_max_speed_multiplier: 1.1,
            camera_sensitivity: Vector2::new(0.003, 0.002),
            jump_impulse: 7.0,
            is_running: false,
            is_floating: false,
            floating_gravity: Vector::NEG_Y * 4.0,
        }
    }
}

pub struct IchunMovementPlugin;

impl Plugin for IchunMovementPlugin {
    fn build(&self, app: &mut bevy::app::App) {
        app.add_event::<IchunRotateEvent>()
            .add_event::<IchunMoveEvent>()
            .add_event::<IchunRunEvent>()
            .add_event::<IchunJumpEvent>()
            .add_event::<IchunFloatEvent>()
            .add_systems(
                Update,
                (
                    apply_rotation_sys,
                    apply_run_sys,
                    apply_velocity_sys,
                    apply_jump_sys,
                    apply_float_sys,
                    cap_gravity_sys,
                )
                    .chain()
                    .in_set(IchunSystemSet::MovementSet),
            );
    }
}

/// Event triggered when character movement input is detected
///
/// This event is dispatched by input systems when the player presses
/// movement keys or uses an analog stick. The direction is a normalized
/// 2D vector representing the input direction.
///
/// # Fields
///
/// * `entity`: The entity (character) that should move
/// * `direction`: A normalized 2D vector representing movement direction
#[derive(Event)]
pub struct IchunMoveEvent {
    pub entity: Entity,
    pub direction: Vector2,
}

/// Event triggered when character run input is detected
///
/// This event is dispatched when the player activates or deactivates
/// the run mode. It can toggle the running state or set it explicitly.
///
/// # Fields
///
/// * `entity`: The entity (character) that should run
/// * `is_running`:
///   - `Some(true)`: Enable running
///   - `Some(false)`: Disable running
///   - `None`: Toggle running state
#[derive(Event)]
pub struct IchunRunEvent {
    pub entity: Entity,
    pub is_running: Option<bool>,
}

/// Event triggered when character rotation input is detected
///
/// This event is dispatched when the player moves the mouse
/// to rotate the character or camera.
///
/// # Fields
///
/// * `entity`: The entity (character) that should rotate
/// * `rotation`: A 2D vector representing the rotation amount
///   (typically derived from mouse movement)
#[derive(Event)]
pub struct IchunRotateEvent {
    pub entity: Entity,
    pub rotation: Vector2,
}

/// Event triggered when character jump input is detected
///
/// This event is dispatched when the player presses the jump button.
/// The jump will only occur if the character is grounded.
///
/// # Fields
///
/// * `entity`: The entity (character) that should jump
#[derive(Event)]
pub struct IchunJumpEvent {
    pub entity: Entity,
}

/// Event triggered when character float/glide input is detected
///
/// This event is dispatched when the player activates or deactivates
/// the floating / gliding mode. It can toggle the floating state or set it explicitly.
///
/// # Fields
///
/// * `entity`: The entity (character) that should float
/// * `is_floating`:
///   - `Some(true)`: Enable floating
///   - `Some(false)`: Disable floating
///   - `None`: Toggle floating state
#[derive(Event)]
pub struct IchunFloatEvent {
    pub entity: Entity,
    pub is_floating: Option<bool>,
}

/// Responds to [`IchunMoveEvent`] events and moves character controllers accordingly.
fn apply_velocity_sys(
    time: Res<Time>,
    mut movement_event_reader: EventReader<IchunMoveEvent>,
    mut controllers_qry: Query<(
        Entity,
        &KccMovementConfig,
        &mut KccVelocity,
        &Transform,
        &Kcc,
    )>,
) {
    let delta_time = time.delta_secs_f64().adjust_precision();

    for event in movement_event_reader.read() {
        let Some((_, kcc_movement, mut kcc_vel, transform, kcc)) =
            controllers_qry.iter_mut().find(|c| c.0 == event.entity)
        else {
            continue;
        };

        // Determine acceleration depending on Kcc state (mid air or running)
        let base_acceleration = if kcc_movement.is_running {
            kcc_movement.running_acceleration
        } else {
            kcc_movement.movement_acceleration
        };

        // Apply air control factor if not grounded
        let adjusted_acceleration = if kcc.is_grounded {
            base_acceleration
        } else {
            // Apply reduced control in air
            base_acceleration
                * kcc_movement.air_control_factor
                * kcc_movement.air_acceleration_factor
        };

        let rotated_velocity = transform.rotation.mul_vec3(Vec3::new(
            event.direction.x * adjusted_acceleration * delta_time,
            0.0,
            event.direction.y * adjusted_acceleration * delta_time,
        ));

        kcc_vel.x += rotated_velocity.x;
        kcc_vel.z += rotated_velocity.z;

        // Apply max air speed limit if airborne
        if !kcc.is_grounded {
            let max_ground_speed = if kcc_movement.is_running {
                kcc_movement.running_acceleration
            } else {
                kcc_movement.movement_acceleration
            };

            let max_air_speed = max_ground_speed * kcc_movement.air_max_speed_multiplier;
            let current_horizontal_speed = Vec2::new(kcc_vel.x, kcc_vel.z).length();

            if current_horizontal_speed > max_air_speed {
                let normalized = Vec2::new(kcc_vel.x, kcc_vel.z).normalize() * max_air_speed;
                kcc_vel.x = normalized.x;
                kcc_vel.z = normalized.y;
            }
        }
    }
}

/// Responds to [`IchunRotateEvent`] events and moves character controllers accordingly.
fn apply_rotation_sys(
    mut movement_event_reader: EventReader<IchunRotateEvent>,
    mut controllers_qry: Query<(Entity, &KccMovementConfig, &mut Transform), With<Kcc>>,
) {
    for event in movement_event_reader.read() {
        let Some((_, kcc, mut transform)) =
            controllers_qry.iter_mut().find(|c| c.0 == event.entity)
        else {
            continue;
        };

        let camera_sensitivity = kcc.camera_sensitivity;

        // Note that we are not multiplying by delta_time here.
        // The reason is that for mouse movement, we already get the full movement that happened since the last frame.
        // This means that if we multiply by delta_time, we will get a smaller rotation than intended by the user.
        // This situation is reversed when reading e.g. analog input from a gamepad however, where the same rules
        // as for keyboard input apply. Such an input should be multiplied by delta_time to get the intended rotation
        // independent of the framerate.
        let delta_yaw = -event.rotation.x * camera_sensitivity.x;
        let delta_pitch = 0.0; // -delta.y * camera_sensitivity.y;

        let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);
        let yaw = yaw + delta_yaw;

        // If the pitch was ±¹⁄₂ π, the camera would look straight up or down.
        // When the user wants to move the camera back to the horizon, which way should the camera face?
        // The camera has no way of knowing what direction was "forward" before landing in that extreme position,
        // so the direction picked will for all intents and purposes be arbitrary.
        // Another issue is that for mathematical reasons, the yaw will effectively be flipped when the pitch is at the extremes.
        // To not run into these issues, we clamp the pitch to a safe range.
        const PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
        let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);

        transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
    }
}

/// Responds to [`IchunJumpEvent`] events and moves character controllers accordingly.
fn apply_jump_sys(
    mut cmd: Commands,
    mut movement_event_reader: EventReader<IchunJumpEvent>,
    mut controllers_qry: Query<(Entity, &KccMovementConfig, &mut KccVelocity, &mut Kcc)>,
) {
    for event in movement_event_reader.read() {
        let Some((entity, kcc_movement, mut kcc_vel, mut kcc)) =
            controllers_qry.iter_mut().find(|c| c.0 == event.entity)
        else {
            continue;
        };

        if !kcc.is_grounded {
            continue;
        }

        if kcc.external_force.y < 0.0 {
            kcc.external_force.y *= 0.3;
        }

        // adds the jump impulse to the external force to also consider down forces when jumping
        // the external force is used for the calculation bc. the KCC vel is changed in many systems
        // and not consistent in this case
        kcc_vel.0.y = kcc_movement.jump_impulse;

        // mark the entity as having jumped recently to temporarily disable platform syncing
        // (prevents platform from pulling the KCC down immediately after a jump)
        cmd.entity(entity).insert(JumpedRecently::default());
    }
}

/// Responds to [`IchunRunEvent`] events and changes run state accordingly.
fn apply_run_sys(
    mut movement_event_reader: EventReader<IchunRunEvent>,
    mut controllers_qry: Query<(Entity, &mut KccMovementConfig), With<Kcc>>,
) {
    for event in movement_event_reader.read() {
        let Some((_, mut kcc)) = controllers_qry.iter_mut().find(|c| c.0 == event.entity) else {
            continue;
        };

        // If is_running is Some, set to that value
        // If is_running is None, toggle the current state
        kcc.is_running = match event.is_running {
            Some(running) => running,
            None => !kcc.is_running,
        };
    }
}

/// Responds to [`IchunFloatEvent`] events and changes float state accordingly.
fn apply_float_sys(
    mut movement_event_reader: EventReader<IchunFloatEvent>,
    mut controllers_qry: Query<(Entity, &mut KccMovementConfig), With<Kcc>>,
) {
    for event in movement_event_reader.read() {
        let Some((_, mut movement)) = controllers_qry.iter_mut().find(|c| c.0 == event.entity)
        else {
            continue;
        };

        // If is_floating is Some, set to that value
        // If is_floating is None, toggle the current state
        movement.is_floating = match event.is_floating {
            Some(floating) => floating,
            None => !movement.is_floating,
        };
    }
}

/// System which caps the gravity to the floating gravity if Kcc is floating
fn cap_gravity_sys(mut controllers_qry: Query<(&mut KccVelocity, &KccMovementConfig)>) {
    for (mut kcc, movement) in controllers_qry.iter_mut() {
        if movement.is_floating {
            kcc.0.y = kcc.0.y.max(movement.floating_gravity.y);
        }
    }
}