bevy-ichun 0.5.0

A simple kinematic character controller for avian3d
Documentation
//! # Rotate Action
//!
//! This module provides rotation functionality for kinematic character controllers.
//! It handles camera-style rotation with pitch clamping to prevent gimbal lock issues.

use bevy::{
    math::{EulerRot, Quat, Vec3},
    transform::components::Transform,
};

/// Action for applying rotation to a character's transform.
///
/// This action handles first-person or third-person camera-style rotation with:
///
/// * **Yaw Rotation**: Horizontal rotation (looking left/right)
/// * **Pitch Rotation**: Vertical rotation (looking up/down)
/// * **Pitch Clamping**: Prevents camera flipping at extreme angles
/// * **Frame-rate Independence**: Designed for mouse input that doesn't need delta time
///
/// The rotation system uses Euler angles internally but converts to quaternions
/// for the final transform to avoid gimbal lock during normal gameplay.
///
/// ## Input Handling
///
/// This action is designed primarily for mouse input, where the rotation values
/// represent the actual movement that occurred since the last frame. For gamepad
/// or keyboard input that should be frame-rate independent, you'll need to
/// multiply the rotation values by delta time before creating the action.
///
/// ## Example
///
/// ```rust
/// use bevy::math::Vec3;
/// use your_crate::actions::RotateAction;
///
/// // Mouse input (full movement since last frame)
/// let mouse_rotation = RotateAction::new(Vec3::new(mouse_delta.x, mouse_delta.y, 0.0));
///
/// // Gamepad input (should be multiplied by delta time)
/// let stick_input = Vec3::new(stick.x * delta_time, stick.y * delta_time, 0.0);
/// let gamepad_rotation = RotateAction::new(stick_input);
/// ```
#[derive(Default, Debug)]
pub struct RotateAction {
    /// The rotation to apply as a 3D vector.
    ///
    /// * **X component**: Yaw rotation (horizontal, left/right)
    /// * **Y component**: Pitch rotation (vertical, up/down)
    /// * **Z component**: Roll rotation (typically unused for character controllers, also not supported yet)
    ///
    /// For mouse input, these values typically represent the mouse movement delta
    /// since the last frame. Positive X rotates right, positive Y rotates up.
    pub rotation: Vec3,
}

impl RotateAction {
    /// Creates a new rotation action with the specified rotation vector.
    ///
    /// # Arguments
    ///
    /// * `rotation` - The rotation to apply. X controls yaw (horizontal),
    ///   Y controls pitch (vertical), Z controls roll (rarely used).
    ///
    /// # Examples
    ///
    /// ```rust
    /// use bevy::math::Vec3;
    /// use your_crate::actions::RotateAction;
    ///
    /// // Rotate right and up slightly
    /// let rotate_right_up = RotateAction::new(Vec3::new(0.1, 0.05, 0.0));
    ///
    /// // Pure horizontal rotation
    /// let rotate_left = RotateAction::new(Vec3::new(-0.2, 0.0, 0.0));
    ///
    /// // Pure vertical rotation
    /// let look_up = RotateAction::new(Vec3::new(0.0, 0.15, 0.0));
    /// ```
    pub fn new(rotation: Vec3) -> Self {
        Self { rotation }
    }

    pub(super) fn apply(&mut self, transform: &mut Transform) {
        // 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.

        // Extract rotation components and negate for intuitive mouse control
        let delta_yaw = -self.rotation.x; // Negative for right-positive mouse movement
        let delta_pitch = -self.rotation.y; // Negative for up-positive mouse movement

        // Convert current rotation to Euler angles for easier manipulation
        let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);

        // Apply yaw rotation (horizontal turning)
        let yaw = yaw + delta_yaw;

        // Apply pitch rotation with clamping to prevent gimbal lock
        // 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 = std::f32::consts::FRAC_PI_2 - 0.01; // ~89.4 degrees
        let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);

        // Convert back to quaternion and apply to transform
        // YXZ order: Yaw (around Y), then Pitch (around X), then Roll (around Z)
        transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
    }
}